2
0
mirror of https://github.com/frappe/books.git synced 2024-12-22 10:58:59 +00:00

refactor(ui): use showDialog for FB Dialog Component

- remove showMessageDialog using native component
- add icons to the Dialog component
This commit is contained in:
18alantom 2023-03-25 13:16:25 +05:30 committed by Alan
parent 524111418c
commit 080eaa5e4f
13 changed files with 167 additions and 144 deletions

View File

@ -4,8 +4,8 @@ import { DateTime } from 'luxon';
import { ModelNameEnum } from 'models/types';
import { codeStateMap } from 'regional/in';
import { ExportExtention } from 'reports/types';
import { showDialog } from 'src/utils/interactive';
import { getSavePath } from 'src/utils/ipcCalls';
import { showMessageDialog } from 'src/utils/ui';
import { invertMap } from 'utils';
import { getCsvData, saveExportData } from '../commonExporter';
import { BaseGSTR } from './BaseGSTR';
@ -175,9 +175,10 @@ async function getCanExport(report: BaseGSTR) {
return true;
}
showMessageDialog({
message: 'Cannot Export',
detail: 'Please set GSTIN in General Settings.',
showDialog({
title: report.fyo.t`Cannot Export`,
detail: report.fyo.t`Please set GSTIN in General Settings.`,
type: 'error',
});
return false;

View File

@ -18,9 +18,16 @@
inner
"
>
<h1 class="font-semibold">{{ title }}</h1>
<p v-if="description" class="text-base">{{ description }}</p>
<div class="flex justify-end gap-2">
<div class="flex justify-between items-center">
<h1 class="font-semibold">{{ title }}</h1>
<FeatherIcon
:name="config.iconName"
class="w-6 h-6"
:class="config.iconColor"
/>
</div>
<p v-if="detail" class="text-base">{{ detail }}</p>
<div class="flex justify-end gap-4 mt-4">
<Button
v-for="(b, index) of buttons"
:ref="b.isPrimary ? 'primary' : 'secondary'"
@ -38,9 +45,11 @@
</Teleport>
</template>
<script lang="ts">
import { DialogButton } from 'src/utils/types';
import { getIconConfig } from 'src/utils/interactive';
import { DialogButton, ToastType } from 'src/utils/types';
import { defineComponent, nextTick, PropType, ref } from 'vue';
import Button from './Button.vue';
import FeatherIcon from './FeatherIcon.vue';
export default defineComponent({
setup() {
@ -53,8 +62,9 @@ export default defineComponent({
return { open: false };
},
props: {
type: { type: String as PropType<ToastType>, default: 'info' },
title: { type: String, required: true },
description: {
detail: {
type: String,
required: false,
},
@ -79,6 +89,11 @@ export default defineComponent({
this.focusButton();
},
computed: {
config() {
return getIconConfig(this.type);
},
},
methods: {
focusButton() {
let button = this.primary?.[0];
@ -104,10 +119,7 @@ export default defineComponent({
return this.handleClick(0);
}
const index = this.buttons.findIndex(
({ isPrimary, isEscape }) =>
isEscape || (this.buttons.length === 2 && !isPrimary)
);
const index = this.buttons.findIndex(({ isEscape }) => isEscape);
if (index === -1) {
return;
@ -117,11 +129,11 @@ export default defineComponent({
},
handleClick(index: number) {
const button = this.buttons[index];
button.handler();
button.action();
this.open = false;
},
},
components: { Button },
components: { Button, FeatherIcon },
});
</script>
<style scoped>

View File

@ -20,9 +20,9 @@
style="pointer-events: auto"
>
<feather-icon
:name="iconName"
:name="config.iconName"
class="w-6 h-6 me-3"
:class="iconColor"
:class="config.iconColor"
/>
<div @click="actionClicked" :class="actionText ? 'cursor-pointer' : ''">
<p class="text-base">{{ message }}</p>
@ -51,6 +51,7 @@
</template>
<script lang="ts">
import { getColorClass } from 'src/utils/colors';
import { getIconConfig } from 'src/utils/interactive';
import { ToastDuration, ToastType } from 'src/utils/types';
import { toastDurationMap } from 'src/utils/ui';
import { defineComponent, nextTick, PropType } from 'vue';
@ -73,26 +74,8 @@ export default defineComponent({
duration: { type: String as PropType<ToastDuration>, default: 'long' },
},
computed: {
iconName() {
switch (this.type) {
case 'warning':
return 'alert-triangle';
case 'success':
return 'check-circle';
default:
return 'alert-circle';
}
},
color() {
return {
info: 'blue',
warning: 'orange',
error: 'red',
success: 'green',
}[this.type];
},
iconColor() {
return getColorClass(this.color ?? 'gray', 'text', 400);
config() {
return getIconConfig(this.type);
},
},
async mounted() {

View File

@ -5,12 +5,12 @@ import { Doc } from 'fyo/model/doc';
import { BaseError } from 'fyo/utils/errors';
import { ErrorLog } from 'fyo/utils/types';
import { truncate } from 'lodash';
import { showDialog } from 'src/utils/interactive';
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
import { fyo } from './initFyo';
import router from './router';
import { getErrorMessage, stringifyCircular } from './utils';
import { MessageDialogOptions, ToastOptions } from './utils/types';
import { showMessageDialog } from './utils/ui';
import { DialogOptions, ToastOptions } from './utils/types';
function shouldNotStore(error: Error) {
const shouldLog = (error as BaseError).shouldStore ?? true;
@ -108,9 +108,10 @@ export async function handleErrorWithDialog(
await handleError(false, error, { errorMessage, doc });
const label = getErrorLabel(error);
const options: MessageDialogOptions = {
message: label,
const options: DialogOptions = {
title: label,
detail: errorMessage,
type: 'error',
};
if (reportError) {
@ -121,12 +122,13 @@ export async function handleErrorWithDialog(
action() {
reportIssue(getErrorLogObject(error, { errorMessage }));
},
isPrimary: true,
},
{ label: t`Cancel`, action() {} },
{ label: t`Cancel`, action() {}, isEscape: true },
];
}
await showMessageDialog(options);
await showDialog(options);
if (dontThrow) {
if (fyo.store.isDevelopment) {
console.error(error);

View File

@ -223,9 +223,9 @@ import FeatherIcon from 'src/components/FeatherIcon.vue';
import Loading from 'src/components/Loading.vue';
import Modal from 'src/components/Modal.vue';
import { fyo } from 'src/initFyo';
import { showDialog } from 'src/utils/interactive';
import { deleteDb, getSavePath } from 'src/utils/ipcCalls';
import { updateConfigFiles } from 'src/utils/misc';
import { showMessageDialog } from 'src/utils/ui';
import { IPC_ACTIONS } from 'utils/messages';
export default {
@ -264,9 +264,10 @@ export default {
const file = this.files[i];
const vm = this;
await showMessageDialog({
message: t`Delete ${file.companyName}?`,
await showDialog({
title: t`Delete ${file.companyName}?`,
detail: t`Database file: ${file.dbPath}`,
type: 'warning',
buttons: [
{
label: this.t`Yes`,
@ -274,10 +275,12 @@ export default {
await deleteDb(file.dbPath);
await vm.setFiles();
},
isPrimary: true,
},
{
label: this.t`No`,
action() {},
isEscape: true,
},
],
});

View File

@ -383,10 +383,11 @@ import Modal from 'src/components/Modal.vue';
import PageHeader from 'src/components/PageHeader.vue';
import { getColumnLabel, Importer, TemplateField } from 'src/importer';
import { fyo } from 'src/initFyo';
import { showDialog } from 'src/utils/interactive';
import { getSavePath, saveData, selectFile } from 'src/utils/ipcCalls';
import { docsPathMap } from 'src/utils/misc';
import { docsPathRef } from 'src/utils/refs';
import { selectTextFile, showMessageDialog } from 'src/utils/ui';
import { selectTextFile } from 'src/utils/ui';
import { defineComponent } from 'vue';
import Loading from '../components/Loading.vue';
@ -780,10 +781,11 @@ export default defineComponent({
await saveData(template, filePath);
},
async preImportValidations(): Promise<boolean> {
const message = this.t`Cannot Import`;
const title = this.t`Cannot Import`;
if (this.errorMessage.length) {
await showMessageDialog({
message,
await showDialog({
title,
type: 'error',
detail: this.errorMessage,
});
return false;
@ -791,8 +793,9 @@ export default defineComponent({
const cellErrors = this.importer.checkCellErrors();
if (cellErrors.length) {
await showMessageDialog({
message,
await showDialog({
title,
type: 'error',
detail: this.t`Following cells have errors: ${cellErrors.join(', ')}`,
});
return false;
@ -800,8 +803,9 @@ export default defineComponent({
const absentLinks = await this.importer.checkLinks();
if (absentLinks.length) {
await showMessageDialog({
message,
await showDialog({
title,
type: 'error',
detail: this.t`Following links do not exist: ${absentLinks
.map((l) => `(${l.schemaLabel}, ${l.name})`)
.join(', ')}`,
@ -851,18 +855,22 @@ export default defineComponent({
}
let shouldSubmit = false;
await showMessageDialog({
message: this.t`Should entries be submitted after syncing?`,
await showDialog({
title: this.t`Submit entries?`,
type: 'info',
details: this.t`Should entries be submitted after syncing?`,
buttons: [
{
label: this.t`Yes`,
action() {
shouldSubmit = true;
},
isPrimary: true,
},
{
label: this.t`No`,
action() {},
isEscape: true,
},
],
});
@ -916,9 +924,10 @@ export default defineComponent({
const isValid = this.importer.selectFile(text);
if (!isValid) {
await showMessageDialog({
message: this.t`Bad import data`,
detail: this.t`Could not read file`,
await showDialog({
title: this.t`Cannot read file`,
detail: this.t`Bad import data, could not read file`,
type: 'error',
});
return;
}

View File

@ -76,11 +76,9 @@ import Button from 'src/components/Button.vue';
import FormContainer from 'src/components/FormContainer.vue';
import FormHeader from 'src/components/FormHeader.vue';
import { getErrorMessage } from 'src/utils';
import { showDialog } from 'src/utils/interactive';
import { getSetupWizardDoc } from 'src/utils/misc';
import {
getFieldsGroupedByTabAndSection,
showMessageDialog,
} from 'src/utils/ui';
import { getFieldsGroupedByTabAndSection } from 'src/utils/ui';
import { computed, defineComponent } from 'vue';
import CommonFormSection from '../CommonForm/CommonFormSection.vue';
@ -155,8 +153,10 @@ export default defineComponent({
}
if (!this.areAllValuesFilled) {
return await showMessageDialog({
message: this.t`Please fill all values`,
return await showDialog({
title: this.t`Mandatory Error`,
detail: this.t`Please fill all values`,
type: 'error',
});
}

View File

@ -220,7 +220,7 @@ import PageHeader from 'src/components/PageHeader.vue';
import ShortcutKeys from 'src/components/ShortcutKeys.vue';
import { handleErrorWithDialog } from 'src/errorHandling';
import { shortcutsKey } from 'src/utils/injectionKeys';
import { showToast } from 'src/utils/interactive';
import { showDialog, showToast } from 'src/utils/interactive';
import { getSavePath } from 'src/utils/ipcCalls';
import { docsPathMap } from 'src/utils/misc';
import {
@ -237,7 +237,6 @@ import {
openSettings,
selectTextFile,
ShortcutKey,
showMessageDialog,
} from 'src/utils/ui';
import { useDocShortcuts } from 'src/utils/vueUtils';
import { getMapFromList } from 'utils/index';
@ -460,10 +459,11 @@ export default defineComponent({
const name = names[0]?.name;
if (!name) {
const label = this.fyo.schemaMap[schemaName]?.label ?? schemaName;
await showMessageDialog({
message: this.t`No Display Entries Found`,
await showDialog({
title: this.t`No Display Entries Found`,
detail: this
.t`Please create a ${label} entry to view Template Preview`,
type: 'warning',
});
return;

View File

@ -1,36 +1,38 @@
import { t } from 'fyo';
import Dialog from 'src/components/Dialog.vue';
import Toast from 'src/components/Toast.vue';
import { t } from 'fyo';
import { App, createApp, h } from 'vue';
import { DialogButton, DialogOptions, ToastOptions } from './types';
import { getColorClass } from './colors';
import { DialogButton, DialogOptions, ToastOptions, ToastType } from './types';
type DialogReturn<DO extends DialogOptions> = DO['buttons'] extends {
handler: () => Promise<infer O> | infer O;
action: () => Promise<infer O> | infer O;
}[]
? O
: void;
export async function showDialog<DO extends DialogOptions>(options: DO) {
const { title, description } = options;
const preWrappedButtons: DialogButton[] = options.buttons ?? [
{ label: t`Okay`, handler: () => {} },
{ label: t`Okay`, action: () => {}, isEscape: true },
];
return new Promise(async (resolve) => {
const buttons = preWrappedButtons!.map(({ label, handler, isPrimary }) => {
return new Promise(async (resolve, reject) => {
const buttons = preWrappedButtons!.map((config) => {
return {
label,
handler: async () => {
resolve(await handler());
...config,
action: async () => {
try {
resolve(await config.action());
} catch (error) {
reject(error);
}
},
isPrimary,
};
});
const dialogApp = createApp({
render() {
return h(Dialog, { title, description, buttons });
return h(Dialog, { ...options, buttons });
},
});
@ -55,3 +57,23 @@ function fragmentMountComponent(app: App<Element>) {
app.mount(fragment);
document.body.append(fragment);
}
export function getIconConfig(type: ToastType) {
let iconName = 'alert-circle';
if (type === 'warning') {
iconName = 'alert-triangle';
} else if (type === 'success') {
iconName = 'check-circle';
}
const color = {
info: 'blue',
warning: 'orange',
error: 'red',
success: 'green',
}[type];
const iconColor = getColorClass(color ?? 'gray', 'text', 400);
return { iconName, color, iconColor };
}

View File

@ -7,9 +7,8 @@ import { BaseError } from 'fyo/utils/errors';
import { BackendResponse } from 'utils/ipc/types';
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
import { SelectFileOptions, SelectFileReturn, TemplateFile } from 'utils/types';
import { showToast } from './interactive';
import { showDialog, showToast } from './interactive';
import { setLanguageMap } from './language';
import { showMessageDialog } from './ui';
export function reloadWindow() {
return ipcRenderer.send(IPC_MESSAGES.RELOAD_MAIN_WINDOW);
@ -41,19 +40,22 @@ export async function deleteDb(filePath: string) {
)) as BackendResponse;
if (error?.code === 'EBUSY') {
showMessageDialog({
message: t`Delete Failed`,
showDialog({
title: t`Delete Failed`,
detail: t`Please restart and try again`,
type: 'error',
});
} else if (error?.code === 'ENOENT') {
showMessageDialog({
message: t`Delete Failed`,
showDialog({
title: t`Delete Failed`,
detail: t`File ${filePath} does not exist`,
type: 'error',
});
} else if (error?.code === 'EPERM') {
showMessageDialog({
message: t`Cannot Delete`,
showDialog({
title: t`Cannot Delete`,
detail: t`Close Frappe Books and try manually`,
type: 'error',
});
} else if (error) {
const err = new BaseError(500, error.message);

View File

@ -101,13 +101,14 @@ export type PrintValues = {
export interface DialogOptions {
title: string;
description?: string;
type?: ToastType;
detail?: string;
buttons?: DialogButton[];
}
export type DialogButton = {
label: string;
handler: () => any;
action: () => any;
isPrimary?: boolean;
isEscape?: boolean;
};

View File

@ -2,7 +2,6 @@
* 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';
import { Action } from 'fyo/model/types';
@ -16,17 +15,16 @@ import { Schema } from 'schemas/types';
import { handleErrorWithDialog } from 'src/errorHandling';
import { fyo } from 'src/initFyo';
import router from 'src/router';
import { IPC_ACTIONS } from 'utils/messages';
import { SelectFileOptions } from 'utils/types';
import { RouteLocationRaw } from 'vue-router';
import { stringifyCircular } from './';
import { evaluateHidden } from './doc';
import { showToast } from './interactive';
import { showDialog, showToast } from './interactive';
import { selectFile } from './ipcCalls';
import { showSidebar } from './refs';
import {
ActionGroup,
MessageDialogOptions,
DialogButton,
QuickEditOptions,
SettingsTab,
ToastOptions,
@ -96,33 +94,6 @@ export async function openQuickEdit({
});
}
// @ts-ignore
window.openqe = openQuickEdit;
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;
}
return await button.action();
}
export async function openSettings(tab: SettingsTab) {
await routeTo({ path: '/settings', query: { tab } });
}
@ -145,21 +116,22 @@ export async function deleteDocWithPrompt(doc: Doc) {
detail = t`This action is permanent and will delete associated ledger entries.`;
}
return await showMessageDialog({
message: t`Delete ${getActionLabel(doc)}?`,
return await showDialog({
title: t`Delete ${getActionLabel(doc)}?`,
detail,
type: 'warning',
buttons: [
{
label: t`Yes`,
async action() {
try {
await doc.delete();
return true;
} catch (err) {
if (getDbError(err as Error) === LinkValidationError) {
showMessageDialog({
message: t`Delete Failed`,
showDialog({
title: t`Delete Failed`,
detail: t`Cannot delete ${schemaLabel} ${doc.name!} because of linked entries.`,
type: 'error',
});
} else {
handleErrorWithDialog(err as Error, doc);
@ -167,13 +139,17 @@ export async function deleteDocWithPrompt(doc: Doc) {
return false;
}
return true;
},
isPrimary: true,
},
{
label: t`No`,
action() {
return false;
},
isEscape: true,
},
],
});
@ -211,27 +187,31 @@ export async function cancelDocWithPrompt(doc: Doc) {
}
}
return await showMessageDialog({
message: t`Cancel ${getActionLabel(doc)}?`,
return await showDialog({
title: t`Cancel ${getActionLabel(doc)}?`,
detail,
type: 'warning',
buttons: [
{
label: t`Yes`,
async action() {
try {
await doc.cancel();
return true;
} catch (err) {
handleErrorWithDialog(err as Error, doc);
return false;
}
return true;
},
isPrimary: true,
},
{
label: t`No`,
action() {
return false;
},
isEscape: true,
},
],
});
@ -596,9 +576,14 @@ export async function commonDocSubmit(doc: Doc): Promise<boolean> {
async function showSubmitOrSyncDialog(doc: Doc, type: 'submit' | 'sync') {
const label = getActionLabel(doc);
let message = t`Submit ${label}?`;
let title = t`Submit ${label}?`;
if (type === 'sync') {
message = t`Save ${label}?`;
title = t`Save ${label}?`;
}
let detail = t`Mark ${doc.schema.label} as submitted`;
if (type === 'sync') {
detail = t`Save ${doc.schema.label} to database`;
}
const yesAction = async () => {
@ -612,19 +597,22 @@ async function showSubmitOrSyncDialog(doc: Doc, type: 'submit' | 'sync') {
return true;
};
const buttons = [
const buttons: DialogButton[] = [
{
label: t`Yes`,
action: yesAction,
isPrimary: true,
},
{
label: t`No`,
action: () => false,
isEscape: true,
},
];
return await showMessageDialog({
message,
return await showDialog({
title,
detail,
buttons,
});
}

View File

@ -5,7 +5,7 @@ import {
onMounted,
onUnmounted,
reactive,
ref,
ref
} from 'vue';
import { getIsMac } from './misc';
import { Shortcuts } from './shortcuts';
@ -16,7 +16,7 @@ import {
commonDocSync,
commongDocDelete,
showCannotCancelOrDeleteToast,
showCannotSaveOrSubmitToast,
showCannotSaveOrSubmitToast
} from './ui';
export function useKeys() {
@ -107,35 +107,35 @@ export function useDocShortcuts(
context = name + '-' + Math.random().toString(36).slice(2, 6);
}
const syncOrSubmitCallback = () => {
const syncOrSubmitCallback = async () => {
const doc = docRef.value;
if (!doc) {
return;
}
if (doc.canSave) {
return commonDocSync(doc, true);
return await commonDocSync(doc, true);
}
if (doc.canSubmit) {
return commonDocSubmit(doc);
return await commonDocSubmit(doc);
}
showCannotSaveOrSubmitToast(doc);
};
const cancelOrDeleteCallback = () => {
const cancelOrDeleteCallback = async () => {
const doc = docRef.value;
if (!doc) {
return;
}
if (doc.canCancel) {
return commonDocCancel(doc);
return await commonDocCancel(doc);
}
if (doc.canDelete) {
return commongDocDelete(doc);
return await commongDocDelete(doc);
}
showCannotCancelOrDeleteToast(doc);