2
0
mirror of https://github.com/frappe/books.git synced 2025-02-04 13:08:29 +00:00
books/src/utils/ui.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

479 lines
11 KiB
TypeScript
Raw Normal View History

2022-04-20 12:08:47 +05:30
/**
* Utils to do UI stuff such as opening dialogs, toasts, etc.
* Basically anything that may directly or indirectly import a Vue file.
*/
import { ipcRenderer } from 'electron';
import { t } from 'fyo';
import type { Doc } from 'fyo/model/doc';
2022-04-20 12:08:47 +05:30
import { Action } from 'fyo/model/types';
import { getActions } from 'fyo/utils';
2022-11-22 14:42:49 +05:30
import { getDbError, LinkValidationError, ValueError } from 'fyo/utils/errors';
import { ModelNameEnum } from 'models/types';
2023-02-20 10:22:19 +05:30
import { Schema } from 'schemas/types';
2022-04-20 12:08:47 +05:30
import { handleErrorWithDialog } from 'src/errorHandling';
import { fyo } from 'src/initFyo';
import router from 'src/router';
import { IPC_ACTIONS } from 'utils/messages';
import { App, createApp, h } from 'vue';
2022-04-20 12:08:47 +05:30
import { RouteLocationRaw } from 'vue-router';
import { stringifyCircular } from './';
import { evaluateHidden } from './doc';
2022-04-20 12:08:47 +05:30
import {
2023-02-20 10:22:19 +05:30
ActionGroup,
2022-04-20 12:08:47 +05:30
MessageDialogOptions,
QuickEditOptions,
SettingsTab,
ToastOptions,
2023-02-20 10:22:19 +05:30
UIGroupedFields,
2022-04-20 12:08:47 +05:30
} from './types';
export async function openQuickEdit({
2022-11-22 14:42:49 +05:30
doc,
2022-04-20 12:08:47 +05:30
schemaName,
name,
2022-04-28 12:04:55 +05:30
hideFields = [],
showFields = [],
2022-04-20 12:08:47 +05:30
defaults = {},
listFilters = {},
2022-04-20 12:08:47 +05:30
}: QuickEditOptions) {
2022-11-22 14:42:49 +05:30
if (doc) {
schemaName = doc.schemaName;
name = doc.name;
}
if (!doc && (!schemaName || !name)) {
throw new ValueError(t`Schema Name or Name not passed to Open Quick Edit`);
}
2022-04-20 12:08:47 +05:30
const currentRoute = router.currentRoute.value;
const query = currentRoute.query;
let method: 'push' | 'replace' = 'push';
2022-04-28 12:04:55 +05:30
if (query.edit && query.schemaName === schemaName) {
2022-04-20 12:08:47 +05:30
method = 'replace';
}
2022-04-28 12:04:55 +05:30
if (query.name === name) {
return;
}
2022-04-20 12:08:47 +05:30
const forWhat = (defaults?.for ?? []) as string[];
if (forWhat[0] === 'not in') {
const purpose = forWhat[1]?.[0];
defaults = Object.assign({
for:
purpose === 'Sales'
? 'Purchases'
: purpose === 'Purchases'
? 'Sales'
: 'Both',
2022-04-20 12:08:47 +05:30
});
}
if (forWhat[0] === 'not in' && forWhat[1] === 'Sales') {
defaults = Object.assign({ for: 'Purchases' });
2022-04-20 12:08:47 +05:30
}
router[method]({
query: {
edit: 1,
2022-04-28 12:04:55 +05:30
schemaName,
2022-04-20 12:08:47 +05:30
name,
2022-04-28 12:04:55 +05:30
showFields,
2022-04-20 12:08:47 +05:30
hideFields,
2022-04-28 12:04:55 +05:30
defaults: stringifyCircular(defaults),
filters: JSON.stringify(listFilters),
2022-04-20 12:08:47 +05:30
},
});
}
2022-11-22 14:42:49 +05:30
// @ts-ignore
window.openqe = openQuickEdit;
2022-04-20 12:08:47 +05:30
export async function showMessageDialog({
message,
detail,
buttons = [],
}: MessageDialogOptions) {
const options = {
message,
detail,
buttons: buttons.map((a) => a.label),
};
const { response } = (await ipcRenderer.invoke(
IPC_ACTIONS.GET_DIALOG_RESPONSE,
options
)) as { response: number };
const button = buttons[response];
if (!button?.action) {
return null;
2022-04-20 12:08:47 +05:30
}
return await button.action();
2022-04-20 12:08:47 +05:30
}
export async function showToast(options: ToastOptions) {
const Toast = (await import('src/components/Toast.vue')).default;
const toast = createApp({
render() {
return h(Toast, { ...options });
},
});
replaceAndAppendMount(toast, 'toast-target');
}
function replaceAndAppendMount(app: App<Element>, replaceId: string) {
const fragment = document.createDocumentFragment();
const target = document.getElementById(replaceId);
if (target === null) {
return;
}
const parent = target.parentElement;
const clone = target.cloneNode();
// @ts-ignore
app.mount(fragment);
target.replaceWith(fragment);
parent!.append(clone);
}
export function openSettings(tab: SettingsTab) {
routeTo({ path: '/settings', query: { tab } });
}
export async function routeTo(route: RouteLocationRaw) {
2022-04-20 12:08:47 +05:30
if (
typeof route === 'string' &&
route === router.currentRoute.value.fullPath
) {
return;
}
return await router.push(route);
2022-04-20 12:08:47 +05:30
}
export async function deleteDocWithPrompt(doc: Doc) {
const schemaLabel = fyo.schemaMap[doc.schemaName]!.label;
let detail = t`This action is permanent.`;
2022-07-28 14:29:04 +05:30
if (doc.isTransactional && doc.isSubmitted) {
detail = t`This action is permanent and will delete associated ledger entries.`;
}
return await showMessageDialog({
message: t`Delete ${schemaLabel} ${doc.name!}?`,
detail,
buttons: [
{
label: t`Delete`,
async action() {
try {
await doc.delete();
return true;
} catch (err) {
if (getDbError(err as Error) === LinkValidationError) {
showMessageDialog({
message: t`Delete Failed`,
detail: t`Cannot delete ${schemaLabel} ${doc.name!} because of linked entries.`,
});
} else {
handleErrorWithDialog(err as Error, doc);
}
return false;
}
2022-04-20 12:08:47 +05:30
},
},
{
label: t`Cancel`,
action() {
return false;
2022-04-20 12:08:47 +05:30
},
},
],
2022-04-20 12:08:47 +05:30
});
}
export async function cancelDocWithPrompt(doc: Doc) {
let detail = t`This action is permanent`;
if (['SalesInvoice', 'PurchaseInvoice'].includes(doc.schemaName)) {
const payments = (
await fyo.db.getAll('Payment', {
fields: ['name'],
filters: { cancelled: false },
})
).map(({ name }) => name);
const query = (
await fyo.db.getAll('PaymentFor', {
fields: ['parent'],
filters: {
referenceName: doc.name!,
},
})
).filter(({ parent }) => payments.includes(parent));
const paymentList = [...new Set(query.map(({ parent }) => parent))];
if (paymentList.length === 1) {
detail = t`This action is permanent and will cancel the following payment: ${
paymentList[0] as string
}`;
} else if (paymentList.length > 1) {
detail = t`This action is permanent and will cancel the following payments: ${paymentList.join(
', '
)}`;
}
}
const schemaLabel = fyo.schemaMap[doc.schemaName]!.label;
return await showMessageDialog({
message: t`Cancel ${schemaLabel} ${doc.name!}?`,
detail,
buttons: [
{
label: t`Yes`,
async action() {
try {
await doc.cancel();
return true;
} catch (err) {
handleErrorWithDialog(err as Error, doc);
return false;
}
2022-04-20 12:08:47 +05:30
},
},
{
label: t`No`,
action() {
return false;
2022-04-20 12:08:47 +05:30
},
},
],
2022-04-20 12:08:47 +05:30
});
}
export function getActionsForDoc(doc?: Doc): Action[] {
2022-04-20 12:08:47 +05:30
if (!doc) return [];
const actions: Action[] = [
...getActions(doc),
2022-04-20 12:08:47 +05:30
getDuplicateAction(doc),
getDeleteAction(doc),
getCancelAction(doc),
];
return actions
.filter((d) => d.condition?.(doc) ?? true)
.map((d) => {
return {
2022-11-30 12:29:52 +05:30
group: d.group,
2022-04-20 12:08:47 +05:30
label: d.label,
component: d.component,
action: d.action,
};
});
}
2023-02-20 10:22:19 +05:30
export function getGroupedActionsForDoc(doc?: Doc): ActionGroup[] {
const actions = getActionsForDoc(doc);
2022-11-30 12:29:52 +05:30
const actionsMap = actions.reduce((acc, ac) => {
if (!ac.group) {
ac.group = '';
}
acc[ac.group] ??= {
group: ac.group,
label: ac.label ?? '',
type: ac.type ?? 'secondary',
actions: [],
};
acc[ac.group].actions.push(ac);
return acc;
2023-02-20 10:22:19 +05:30
}, {} as Record<string, ActionGroup>);
2022-11-30 12:29:52 +05:30
const grouped = Object.keys(actionsMap)
.filter(Boolean)
.sort()
.map((k) => actionsMap[k]);
return [grouped, actionsMap['']].flat().filter(Boolean);
2022-11-30 12:29:52 +05:30
}
2022-04-20 12:08:47 +05:30
function getCancelAction(doc: Doc): Action {
return {
label: t`Cancel`,
component: {
template: '<span class="text-red-700">{{ t`Cancel` }}</span>',
},
condition: (doc: Doc) => doc.canCancel,
async action() {
const res = await cancelDocWithPrompt(doc);
if (res) {
router.push(`/list/${doc.schemaName}`);
}
2022-04-20 12:08:47 +05:30
},
};
}
function getDeleteAction(doc: Doc): Action {
return {
label: t`Delete`,
component: {
template: '<span class="text-red-700">{{ t`Delete` }}</span>',
},
2022-07-28 14:29:04 +05:30
condition: (doc: Doc) => doc.canDelete,
async action() {
const res = await deleteDocWithPrompt(doc);
if (res) {
2022-05-18 20:28:35 +05:30
router.back();
}
},
2022-04-20 12:08:47 +05:30
};
}
async function openEdit(doc: Doc) {
2023-02-23 18:06:50 +05:30
const listConfig = fyo.models[doc.schemaName]?.getListViewSettings?.(fyo);
const formRoute = listConfig?.formRoute;
if (formRoute) {
return await routeTo(formRoute(doc));
}
const isFormEdit = [
ModelNameEnum.SalesInvoice,
ModelNameEnum.PurchaseInvoice,
ModelNameEnum.JournalEntry,
2023-02-23 18:06:50 +05:30
ModelNameEnum.Shipment,
ModelNameEnum.PurchaseReceipt,
ModelNameEnum.StockMovement,
ModelNameEnum.Payment,
ModelNameEnum.Item,
].includes(doc.schemaName as ModelNameEnum);
if (isFormEdit) {
return await routeTo(`/edit/${doc.schemaName}/${doc.name!}`);
}
await openQuickEdit({ schemaName: doc.schemaName, name: doc.name! });
}
2022-04-20 12:08:47 +05:30
function getDuplicateAction(doc: Doc): Action {
const isSubmittable = !!doc.schema.isSubmittable;
return {
label: t`Duplicate`,
2022-11-30 12:29:52 +05:30
group: t`Create`,
2022-04-20 12:08:47 +05:30
condition: (doc: Doc) =>
!!(
((isSubmittable && doc.submitted) || !isSubmittable) &&
!doc.notInserted
2022-04-20 12:08:47 +05:30
),
async action() {
await showMessageDialog({
2022-04-20 12:08:47 +05:30
message: t`Duplicate ${doc.schemaName} ${doc.name!}?`,
buttons: [
{
label: t`Yes`,
async action() {
try {
2023-02-23 18:06:50 +05:30
const dupe = doc.duplicate();
await openEdit(dupe);
return true;
} catch (err) {
handleErrorWithDialog(err as Error, doc);
return false;
}
2022-04-20 12:08:47 +05:30
},
},
{
label: t`No`,
action() {
return false;
2022-04-20 12:08:47 +05:30
},
},
],
});
},
};
}
2023-02-20 10:22:19 +05:30
export function getFieldsGroupedByTabAndSection(
schema: Schema,
doc: Doc
2023-02-20 10:22:19 +05:30
): UIGroupedFields {
const grouped: UIGroupedFields = new Map();
for (const field of schema?.fields ?? []) {
const tab = field.tab ?? 'Default';
const section = field.section ?? 'Default';
if (!grouped.has(tab)) {
grouped.set(tab, new Map());
}
const tabbed = grouped.get(tab)!;
if (!tabbed.has(section)) {
tabbed.set(section, []);
}
if (field.meta) {
continue;
}
if (evaluateHidden(field, doc)) {
continue;
}
2023-02-20 10:22:19 +05:30
tabbed.get(section)!.push(field);
}
2023-02-20 10:22:19 +05:30
return grouped;
}
export function getFormRoute(
schemaName: string,
name: string
): RouteLocationRaw {
const route = fyo.models[schemaName]
?.getListViewSettings(fyo)
?.formRoute?.(name);
if (typeof route === 'string') {
return route;
}
if (
[
ModelNameEnum.SalesInvoice,
ModelNameEnum.PurchaseInvoice,
ModelNameEnum.JournalEntry,
ModelNameEnum.Shipment,
ModelNameEnum.PurchaseReceipt,
ModelNameEnum.StockMovement,
ModelNameEnum.Payment,
ModelNameEnum.Item,
].includes(schemaName as ModelNameEnum)
) {
return `/edit/${schemaName}/${name}`;
}
return `/list/${schemaName}?edit=1&schemaName=${schemaName}&name=${name}`;
}
export async function getDocFromNameIfExistsElseNew(
schemaName: string,
name?: string
) {
if (!name) {
return fyo.doc.getNewDoc(schemaName);
}
try {
return await fyo.doc.getDoc(schemaName, name);
} catch {
return fyo.doc.getNewDoc(schemaName);
}
}