import { EditIcon, PinIcon, UnpinIcon } from "@/components/icons"; import { useAuthStore } from "@/stores/auth"; import { globalStore } from "@/stores/globalStore"; import { __ } from "@/translation"; import { View } from "@/types"; import { getIcon } from "@/utils"; import { useDebounceFn } from "@vueuse/core"; import { call, createListResource, createResource, toast, } from "frappe-ui"; import { Icon } from "frappe-ui/experimental"; import { computed, h, ref, unref } from "vue"; import { useRoute, useRouter } from "vue-router"; const debouncedSetValue = useDebounceFn( (doctype: string, name: string, fieldname: any, cb?: Function) => { call("frappe.client.set_value", { doctype, name, fieldname }).then(() => { cb?.(); views.reload(); }); }, 300 ); export const views = createListResource({ doctype: "OD View", fields: ["*"], transform: (data: any) => { data.forEach((view: View) => { view.filters = JSON.parse(view.filters) || {}; view.columns = JSON.parse(view.columns) || []; view.rows = JSON.parse(view.rows) || []; }); }, pageLength: 1000, }); export const currentView = ref({ label: "List", icon: LucideAlignJustify, }); export function useView(dt: string = null) { const auth = useAuthStore(); const router = useRouter(); const route = useRoute(); const { $dialog } = globalStore(); function callGetViews() { if ( (views.filters?.dt === dt && views.data?.length > 0) || views.list?.promise ) { return; } const filters = { is_customer_portal: false, }; if (dt) { filters["dt"] = dt; } views.update({ filters }); views.fetch(); } callGetViews(); const getCurrentUserViews = computed(() => views.data ?.filter( (view: View) => !view.is_default && view.user === auth.userId && !view.public && !view.pinned && !view.is_standard ) .map(parseView) ); async function createView( view: View, successCB: Function = () => {}, errorCB: Function = () => {} ) { createResource({ url: "frappe.client.insert", params: { doc: { doctype: "OD View", ...view, user: auth.userId, }, }, auto: true, onSuccess: (d) => { views.reload().then(() => { successCB(d); }); }, onError: (e) => { console.log("error", e); errorCB(e); }, }); } function findView(viewName: string) { return computed(() => views.data?.find((v: View) => v.name === viewName)); } const pinnedViews = computed(() => views.data ?.filter((view: View) => view.pinned && view.user === auth.userId) .map(parseView) ); const publicViews = computed(() => views.data?.filter((view: View) => view.public).map(parseView) ); const defaultView = computed(() => views.data?.find( (v: View) => v.is_default && v.user === auth.userId && v.dt === dt ) ); const standardViews = computed(() => views.data?.filter((view: View) => view.is_standard).map(parseView) ); function updateView(view: any, successCB: Function = () => {}) { if (view.name !== "default") { debouncedSetValue("OD View", view.name, view, successCB); } else { // handle default view createOrUpdateDefaultView(view); successCB(); } } function deleteView(viewName: string) { call("frappe.client.delete", { doctype: "OD View", name: viewName, }).then(() => { views.reload(); }); } function createOrUpdateDefaultView(view: View) { const defaultView = views.data?.find( (v: View) => v.is_default && v.user === auth.userId && v.dt === view.dt ); view.is_customer_portal = false; if (defaultView) { delete view["name"]; debouncedSetValue("OD View", defaultView.name, view); } else { view["doctype"] = "OD View"; // create default view createView({ ...view, is_default: true, }); } } function parseView(view: View) { return { label: view.label, name: view.name, icon: getIcon(view.icon), route_name: view.route_name, is_standard: view.is_standard || false, onClick: () => { router.push({ name: view.route_name, query: { view: view.name, }, }); }, }; } // Kebab-menu actions for a single view. Shared between the breadcrumb dropdown // and the sidebar so both expose the same options. `viewDialogConfig` is the caller's // reactive modal state, populated here for the edit/duplicate flows. function viewActions(view: View, viewDialogConfig: any) { const _view = findView(view.name).value; const actions: any[] = [ { group: __("Default Views"), hideLabel: true, options: [ { label: __("Duplicate"), icon: h(Icon, { name: "copy" }), onClick: () => { viewDialogConfig.view.label = _view.label + " (New)"; viewDialogConfig.view.icon = _view.icon; viewDialogConfig.view.name = _view.name; viewDialogConfig.mode = "duplicate"; viewDialogConfig.show = true; }, }, ], }, ]; if (!_view.public || auth.isManager) { if (!_view.public && !_view.is_standard) { actions[0].options.push({ label: _view?.pinned ? __("Unpin View") : __("Pin View"), icon: h(_view?.pinned ? UnpinIcon : PinIcon, { class: "h-4 w-4" }), onClick: () => updateView({ name: _view.name, pinned: !_view.pinned }), }); } if (_view?.is_standard && auth.isManager) { actions[0].options.push({ label: _view?.public ? __("Hide from sidebar") : __("Show in sidebar"), icon: h(Icon, { name: _view?.public ? "eye-off" : "eye", class: "h-4 w-4", }), onClick: () => toggleViewVisibility( _view, __("Hide view from sidebar"), __( "{0} view is currently visible in the sidebar. Hiding it will remove it from the sidebar.", [_view.label] ) ), }); } if (!_view.is_standard) { if (auth.isManager) { actions[0].options.push({ label: _view?.public ? __("Make Private") : __("Make Public"), icon: h(Icon, { name: _view?.public ? "lock" : "lock-open", class: "h-4 w-4", }), onClick: () => toggleViewVisibility( _view, __("Make view private"), __( "{0} view is currently public. Changing it to private will hide it for all the users.", [_view.label] ) ), }); } actions[0].options.push({ label: __("Edit"), icon: h(EditIcon, { class: "h-4 w-4" }), onClick: () => { viewDialogConfig.view.label = _view.label; viewDialogConfig.view.icon = _view.icon; viewDialogConfig.view.name = _view.name; viewDialogConfig.mode = "edit"; viewDialogConfig.show = true; }, }); actions.push({ group: __("Delete View"), hideLabel: true, options: [ { label: __("Delete"), icon: "lucide-trash-2", theme: "red", onClick: () => confirmDeleteView(_view), }, ], }); } } return actions; } function confirmDeleteView(_view: View) { $dialog({ title: __("Delete {0}", [_view.label]), message: __("Are you sure you want to delete this view?") + (_view.public ? " " + __("This view is public, and will be removed for all users.") : ""), actions: [ { label: __("Confirm"), variant: "solid", iconLeft: "lucide-trash-2", theme: "red", onClick({ close }: any) { if (route.query.view === _view.name) { router.push({ name: "TicketsAgent", }); } deleteView(_view.name); toast.success(__("View {0}", [__("deleted")])); close(); }, }, ], }); } function toggleViewVisibility(_view: any, title: string, message: string) { const newView: any = { name: _view.name, public: !_view.public }; if (_view.public) { $dialog({ title, message, actions: [ { label: __("Confirm"), variant: "solid", onClick({ close }: any) { close(); updateView(newView); }, }, ], }); } else { updateView(newView); } } // Submit handler for the view modal (create/edit/duplicate). `getList` lets the // create flow capture the current list's filters/columns/rows; it is omitted by // callers (e.g. the sidebar) that never create a view. function handleView( viewInfo: any, action: string, viewDialogConfig: any, getList: () => any = () => null ) { let view: View; if (action === "update") { updateView({ name: viewInfo.name, label: viewInfo.label, icon: viewInfo.icon, }); handleViewSuccess(viewDialogConfig, __("updated")); currentView.value = { label: viewInfo.label, icon: getIcon(viewInfo.icon), }; return; } else if (action === "duplicate") { const source = findView(viewDialogConfig.view.name).value; view = { ...source, filters: JSON.stringify(source.filters), columns: JSON.stringify(source.columns), rows: JSON.stringify(source.rows), label: viewInfo.label, icon: viewInfo.icon, public: false, pinned: false, }; } else { const list = getList(); view = { dt, type: "list", label: viewInfo.label ?? __("List"), icon: viewInfo.icon ?? "", route_name: router.currentRoute.value.name as string, order_by: list?.params.order_by, filters: JSON.stringify(list?.params.filters), // a grouped list never loads its records, so fall back to the view's own columns columns: JSON.stringify(list?.data?.columns ?? list?.params.columns), rows: JSON.stringify(list?.data?.rows ?? list?.params.rows), // a reactive list unwraps the grouping ref, so read it either way list_group_by: unref(list?.groupBy) ? JSON.stringify(unref(list.groupBy)) : "", pinned: viewInfo.pinned ?? false, public: viewInfo.public ?? false, is_customer_portal: false, }; } createView(view, (d) => { currentView.value = { label: d.label || __("List"), icon: getIcon(d.icon), }; router.push({ name: "TicketsAgent", query: { view: d.name }, }); handleViewSuccess(viewDialogConfig); }); } function handleViewSuccess( viewDialogConfig: any, msg: string = __("created") ) { toast.success(__("View {0}", [msg])); resetViewDialog(viewDialogConfig); } function resetViewDialog(viewDialogConfig: any) { viewDialogConfig.show = false; viewDialogConfig.view.label = ""; viewDialogConfig.view.icon = ""; viewDialogConfig.view.name = ""; viewDialogConfig.mode = null; } return { views, getCurrentUserViews, pinnedViews, publicViews, defaultView, standardViews, findView, createView, updateView, deleteView, createOrUpdateDefaultView, viewActions, handleView, resetViewDialog, }; }