2
0
mirror of https://github.com/frappe/books.git synced 2025-01-03 15:17:30 +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 { ModelNameEnum } from 'models/types';
import { codeStateMap } from 'regional/in'; import { codeStateMap } from 'regional/in';
import { ExportExtention } from 'reports/types'; import { ExportExtention } from 'reports/types';
import { showDialog } from 'src/utils/interactive';
import { getSavePath } from 'src/utils/ipcCalls'; import { getSavePath } from 'src/utils/ipcCalls';
import { showMessageDialog } from 'src/utils/ui';
import { invertMap } from 'utils'; import { invertMap } from 'utils';
import { getCsvData, saveExportData } from '../commonExporter'; import { getCsvData, saveExportData } from '../commonExporter';
import { BaseGSTR } from './BaseGSTR'; import { BaseGSTR } from './BaseGSTR';
@ -175,9 +175,10 @@ async function getCanExport(report: BaseGSTR) {
return true; return true;
} }
showMessageDialog({ showDialog({
message: 'Cannot Export', title: report.fyo.t`Cannot Export`,
detail: 'Please set GSTIN in General Settings.', detail: report.fyo.t`Please set GSTIN in General Settings.`,
type: 'error',
}); });
return false; return false;

View File

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

View File

@ -20,9 +20,9 @@
style="pointer-events: auto" style="pointer-events: auto"
> >
<feather-icon <feather-icon
:name="iconName" :name="config.iconName"
class="w-6 h-6 me-3" class="w-6 h-6 me-3"
:class="iconColor" :class="config.iconColor"
/> />
<div @click="actionClicked" :class="actionText ? 'cursor-pointer' : ''"> <div @click="actionClicked" :class="actionText ? 'cursor-pointer' : ''">
<p class="text-base">{{ message }}</p> <p class="text-base">{{ message }}</p>
@ -51,6 +51,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { getColorClass } from 'src/utils/colors'; import { getColorClass } from 'src/utils/colors';
import { getIconConfig } from 'src/utils/interactive';
import { ToastDuration, ToastType } from 'src/utils/types'; import { ToastDuration, ToastType } from 'src/utils/types';
import { toastDurationMap } from 'src/utils/ui'; import { toastDurationMap } from 'src/utils/ui';
import { defineComponent, nextTick, PropType } from 'vue'; import { defineComponent, nextTick, PropType } from 'vue';
@ -73,26 +74,8 @@ export default defineComponent({
duration: { type: String as PropType<ToastDuration>, default: 'long' }, duration: { type: String as PropType<ToastDuration>, default: 'long' },
}, },
computed: { computed: {
iconName() { config() {
switch (this.type) { return getIconConfig(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);
}, },
}, },
async mounted() { async mounted() {

View File

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

View File

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

View File

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

View File

@ -76,11 +76,9 @@ import Button from 'src/components/Button.vue';
import FormContainer from 'src/components/FormContainer.vue'; import FormContainer from 'src/components/FormContainer.vue';
import FormHeader from 'src/components/FormHeader.vue'; import FormHeader from 'src/components/FormHeader.vue';
import { getErrorMessage } from 'src/utils'; import { getErrorMessage } from 'src/utils';
import { showDialog } from 'src/utils/interactive';
import { getSetupWizardDoc } from 'src/utils/misc'; import { getSetupWizardDoc } from 'src/utils/misc';
import { import { getFieldsGroupedByTabAndSection } from 'src/utils/ui';
getFieldsGroupedByTabAndSection,
showMessageDialog,
} from 'src/utils/ui';
import { computed, defineComponent } from 'vue'; import { computed, defineComponent } from 'vue';
import CommonFormSection from '../CommonForm/CommonFormSection.vue'; import CommonFormSection from '../CommonForm/CommonFormSection.vue';
@ -155,8 +153,10 @@ export default defineComponent({
} }
if (!this.areAllValuesFilled) { if (!this.areAllValuesFilled) {
return await showMessageDialog({ return await showDialog({
message: this.t`Please fill all values`, 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 ShortcutKeys from 'src/components/ShortcutKeys.vue';
import { handleErrorWithDialog } from 'src/errorHandling'; import { handleErrorWithDialog } from 'src/errorHandling';
import { shortcutsKey } from 'src/utils/injectionKeys'; 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 { getSavePath } from 'src/utils/ipcCalls';
import { docsPathMap } from 'src/utils/misc'; import { docsPathMap } from 'src/utils/misc';
import { import {
@ -237,7 +237,6 @@ import {
openSettings, openSettings,
selectTextFile, selectTextFile,
ShortcutKey, ShortcutKey,
showMessageDialog,
} from 'src/utils/ui'; } from 'src/utils/ui';
import { useDocShortcuts } from 'src/utils/vueUtils'; import { useDocShortcuts } from 'src/utils/vueUtils';
import { getMapFromList } from 'utils/index'; import { getMapFromList } from 'utils/index';
@ -460,10 +459,11 @@ export default defineComponent({
const name = names[0]?.name; const name = names[0]?.name;
if (!name) { if (!name) {
const label = this.fyo.schemaMap[schemaName]?.label ?? schemaName; const label = this.fyo.schemaMap[schemaName]?.label ?? schemaName;
await showMessageDialog({ await showDialog({
message: this.t`No Display Entries Found`, title: this.t`No Display Entries Found`,
detail: this detail: this
.t`Please create a ${label} entry to view Template Preview`, .t`Please create a ${label} entry to view Template Preview`,
type: 'warning',
}); });
return; return;

View File

@ -1,36 +1,38 @@
import { t } from 'fyo';
import Dialog from 'src/components/Dialog.vue'; import Dialog from 'src/components/Dialog.vue';
import Toast from 'src/components/Toast.vue'; import Toast from 'src/components/Toast.vue';
import { t } from 'fyo';
import { App, createApp, h } from 'vue'; 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 { type DialogReturn<DO extends DialogOptions> = DO['buttons'] extends {
handler: () => Promise<infer O> | infer O; action: () => Promise<infer O> | infer O;
}[] }[]
? O ? O
: void; : void;
export async function showDialog<DO extends DialogOptions>(options: DO) { export async function showDialog<DO extends DialogOptions>(options: DO) {
const { title, description } = options;
const preWrappedButtons: DialogButton[] = options.buttons ?? [ const preWrappedButtons: DialogButton[] = options.buttons ?? [
{ label: t`Okay`, handler: () => {} }, { label: t`Okay`, action: () => {}, isEscape: true },
]; ];
return new Promise(async (resolve) => { return new Promise(async (resolve, reject) => {
const buttons = preWrappedButtons!.map(({ label, handler, isPrimary }) => { const buttons = preWrappedButtons!.map((config) => {
return { return {
label, ...config,
handler: async () => { action: async () => {
resolve(await handler()); try {
resolve(await config.action());
} catch (error) {
reject(error);
}
}, },
isPrimary,
}; };
}); });
const dialogApp = createApp({ const dialogApp = createApp({
render() { render() {
return h(Dialog, { title, description, buttons }); return h(Dialog, { ...options, buttons });
}, },
}); });
@ -55,3 +57,23 @@ function fragmentMountComponent(app: App<Element>) {
app.mount(fragment); app.mount(fragment);
document.body.append(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 { BackendResponse } from 'utils/ipc/types';
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages'; import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
import { SelectFileOptions, SelectFileReturn, TemplateFile } from 'utils/types'; import { SelectFileOptions, SelectFileReturn, TemplateFile } from 'utils/types';
import { showToast } from './interactive'; import { showDialog, showToast } from './interactive';
import { setLanguageMap } from './language'; import { setLanguageMap } from './language';
import { showMessageDialog } from './ui';
export function reloadWindow() { export function reloadWindow() {
return ipcRenderer.send(IPC_MESSAGES.RELOAD_MAIN_WINDOW); return ipcRenderer.send(IPC_MESSAGES.RELOAD_MAIN_WINDOW);
@ -41,19 +40,22 @@ export async function deleteDb(filePath: string) {
)) as BackendResponse; )) as BackendResponse;
if (error?.code === 'EBUSY') { if (error?.code === 'EBUSY') {
showMessageDialog({ showDialog({
message: t`Delete Failed`, title: t`Delete Failed`,
detail: t`Please restart and try again`, detail: t`Please restart and try again`,
type: 'error',
}); });
} else if (error?.code === 'ENOENT') { } else if (error?.code === 'ENOENT') {
showMessageDialog({ showDialog({
message: t`Delete Failed`, title: t`Delete Failed`,
detail: t`File ${filePath} does not exist`, detail: t`File ${filePath} does not exist`,
type: 'error',
}); });
} else if (error?.code === 'EPERM') { } else if (error?.code === 'EPERM') {
showMessageDialog({ showDialog({
message: t`Cannot Delete`, title: t`Cannot Delete`,
detail: t`Close Frappe Books and try manually`, detail: t`Close Frappe Books and try manually`,
type: 'error',
}); });
} else if (error) { } else if (error) {
const err = new BaseError(500, error.message); const err = new BaseError(500, error.message);

View File

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

View File

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

View File

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