diff --git a/backend/database/bespoke.ts b/backend/database/bespoke.ts index f6b18181..1c17d85a 100644 --- a/backend/database/bespoke.ts +++ b/backend/database/bespoke.ts @@ -41,6 +41,7 @@ export class BespokeQueries { .sum({ total: 'baseGrandTotal' }) .sum({ outstanding: 'outstandingAmount' }) .where('submitted', true) + .where('cancelled', false) .whereBetween('date', [fromDate, toDate]) .first(); } diff --git a/fyo/README.md b/fyo/README.md index 5271c738..ec315a15 100644 --- a/fyo/README.md +++ b/fyo/README.md @@ -25,7 +25,7 @@ located in `model`, all classes exported from `books/models` extend this. ### Terminology -- **Schema**: object that defines shape of the data in the database +- **Schema**: object that defines shape of the data in the database. - **Model**: the controller class that extends the `Doc` class or the `Doc` class itself (if a controller doesn't exist). - **Doc**: instance of a Model, i.e. what has the data. @@ -95,3 +95,16 @@ This can be done using `fyo/utils/translation.ts/setLanguageMapOnTranslationStri Since translations are runtime, if the code is evaluated before the language map is loaded, translations won't work. To prevent this, don't maintain translation strings globally. + +## Observers + +The doc and db handlers have observers (instances of `Observable`) as +properties, these can be accessed using +- `fyo.db.observer` +- `fyo.doc.observer` + +The purpose of the observer is to trigger registered callbacks when some `doc` +operation or `db` operation takes place. + +These are schema level observers i.e. they are registered like so: +`method:schemaName`. The callbacks receive args passed to the functions. \ No newline at end of file diff --git a/fyo/core/dbHandler.ts b/fyo/core/dbHandler.ts index b5ee7750..09158cee 100644 --- a/fyo/core/dbHandler.ts +++ b/fyo/core/dbHandler.ts @@ -1,6 +1,7 @@ import { SingleValue } from 'backend/database/types'; import { Fyo } from 'fyo'; import { DatabaseDemux } from 'fyo/demux/db'; +import Observable from 'fyo/utils/observable'; import { Field, RawValue, SchemaMap } from 'schemas/types'; import { getMapFromList } from 'utils'; import { DatabaseBase, DatabaseDemuxBase, GetAllOptions } from 'utils/db/types'; @@ -23,6 +24,7 @@ export class DatabaseHandler extends DatabaseBase { #demux: DatabaseDemuxBase; dbPath?: string; schemaMap: Readonly = {}; + observer: Observable = new Observable(); fieldValueMap: Record> = {}; constructor(fyo: Fyo, Demux?: DatabaseDemuxConstructor) { @@ -62,6 +64,7 @@ export class DatabaseHandler extends DatabaseBase { const fields = this.schemaMap[schemaName]!.fields!; this.fieldValueMap[schemaName] = getMapFromList(fields, 'fieldname'); } + this.observer = new Observable(); } purgeCache() { @@ -83,6 +86,7 @@ export class DatabaseHandler extends DatabaseBase { schemaName, rawValueMap )) as RawValueMap; + this.observer.trigger(`insert:${schemaName}`, docValueMap); return this.converter.toDocValueMap(schemaName, rawValueMap) as DocValueMap; } @@ -98,6 +102,7 @@ export class DatabaseHandler extends DatabaseBase { name, fields )) as RawValueMap; + this.observer.trigger(`get:${schemaName}`, { name, fields }); return this.converter.toDocValueMap(schemaName, rawValueMap) as DocValueMap; } @@ -106,6 +111,7 @@ export class DatabaseHandler extends DatabaseBase { options: GetAllOptions = {} ): Promise { const rawValueMap = await this.#getAll(schemaName, options); + this.observer.trigger(`getAll:${schemaName}`, options); return this.converter.toDocValueMap( schemaName, rawValueMap @@ -116,7 +122,9 @@ export class DatabaseHandler extends DatabaseBase { schemaName: string, options: GetAllOptions = {} ): Promise { - return await this.#getAll(schemaName, options); + const all = await this.#getAll(schemaName, options); + this.observer.trigger(`getAllRaw:${schemaName}`, options); + return all; } async getSingleValues( @@ -139,6 +147,7 @@ export class DatabaseHandler extends DatabaseBase { }); } + this.observer.trigger(`getSingleValues`, fieldnames); return docSingleValue; } @@ -147,7 +156,9 @@ export class DatabaseHandler extends DatabaseBase { options: GetAllOptions = {} ): Promise { const rawValueMap = await this.#getAll(schemaName, options); - return rawValueMap.length; + const count = rawValueMap.length; + this.observer.trigger(`count:${schemaName}`, options); + return count; } // Update @@ -157,28 +168,37 @@ export class DatabaseHandler extends DatabaseBase { newName: string ): Promise { await this.#demux.call('rename', schemaName, oldName, newName); + this.observer.trigger(`rename:${schemaName}`, { oldName, newName }); } async update(schemaName: string, docValueMap: DocValueMap): Promise { const rawValueMap = this.converter.toRawValueMap(schemaName, docValueMap); await this.#demux.call('update', schemaName, rawValueMap); + this.observer.trigger(`update:${schemaName}`, docValueMap); } // Delete async delete(schemaName: string, name: string): Promise { await this.#demux.call('delete', schemaName, name); + this.observer.trigger(`delete:${schemaName}`, name); } // Other + async exists(schemaName: string, name?: string): Promise { + const doesExist = (await this.#demux.call( + 'exists', + schemaName, + name + )) as boolean; + this.observer.trigger(`exists:${schemaName}`, name); + return doesExist; + } + async close(): Promise { await this.#demux.call('close'); this.purgeCache(); } - async exists(schemaName: string, name?: string): Promise { - return (await this.#demux.call('exists', schemaName, name)) as boolean; - } - /** * Bespoke function * diff --git a/fyo/core/docHandler.ts b/fyo/core/docHandler.ts index 495b3816..76bba70c 100644 --- a/fyo/core/docHandler.ts +++ b/fyo/core/docHandler.ts @@ -1,6 +1,7 @@ import { Doc } from 'fyo/model/doc'; import { DocMap, ModelMap, SinglesMap } from 'fyo/model/types'; import { coreModels } from 'fyo/models'; +import { NotFoundError } from 'fyo/utils/errors'; import Observable from 'fyo/utils/observable'; import { Schema } from 'schemas/types'; import { getRandomString } from 'utils'; @@ -87,7 +88,7 @@ export class DocHandler { schema ??= this.fyo.schemaMap[schemaName]; if (schema === undefined) { - throw new Error(`Schema not found for ${schemaName}`); + throw new NotFoundError(`Schema not found for ${schemaName}`); } const doc = new Model!(schema, data, this.fyo); @@ -113,6 +114,7 @@ export class DocHandler { if (!this.docs[schemaName]) { this.docs.set(schemaName, {}); + this.#setCacheUpdationListeners(schemaName); } this.docs.get(schemaName)![name] = doc; @@ -127,11 +129,6 @@ export class DocHandler { this.docs!.trigger('change', params); }); - doc.on('afterRename', (names: { oldName: string }) => { - this.#removeFromCache(doc.schemaName, names.oldName); - this.#addToCache(doc); - }); - doc.on('afterSync', () => { if (doc.name === name) { return; @@ -140,10 +137,25 @@ export class DocHandler { this.#removeFromCache(doc.schemaName, name); this.#addToCache(doc); }); + } - doc.once('afterDelete', () => { - this.#removeFromCache(doc.schemaName, doc.name!); + #setCacheUpdationListeners(schemaName: string) { + this.fyo.db.observer.on(`delete:${schemaName}`, (name: string) => { + this.#removeFromCache(schemaName, name); }); + + this.fyo.db.observer.on( + `rename:${schemaName}`, + (names: { oldName: string; newName: string }) => { + const doc = this.#getFromCache(schemaName, names.oldName); + if (doc === undefined) { + return; + } + + this.#removeFromCache(schemaName, names.oldName); + this.#addToCache(doc); + } + ); } #removeFromCache(schemaName: string, name: string) { diff --git a/fyo/model/doc.ts b/fyo/model/doc.ts index 600e79bf..ee610310 100644 --- a/fyo/model/doc.ts +++ b/fyo/model/doc.ts @@ -374,7 +374,7 @@ export class Doc extends Observable { } _setBaseMetaValues() { - if (this.schema.isSubmittable && typeof this.submitted !== 'boolean') { + if (this.schema.isSubmittable) { this.submitted = false; this.cancelled = false; } @@ -602,7 +602,7 @@ export class Doc extends Observable { return this; } - async sync() { + async sync(): Promise { await this.trigger('beforeSync'); let doc; if (this.notInserted) { @@ -744,6 +744,11 @@ export class Doc extends Observable { * * Abstractish methods that are called using `this.trigger`. * These are to be overridden if required when subclassing. + * + * Refrain from running methods that call `this.sync` + * in the `beforeLifecycle` methods. + * + * This may cause the lifecycle function to execute incorrectly. */ async change(ch: ChangeArg) {} async validate() {} diff --git a/models/Transactional/LedgerPosting.ts b/models/Transactional/LedgerPosting.ts index 11274d94..008bcf3e 100644 --- a/models/Transactional/LedgerPosting.ts +++ b/models/Transactional/LedgerPosting.ts @@ -117,7 +117,8 @@ export class LedgerPosting { reverted: this.reverted, debit: this.fyo.pesa(0), credit: this.fyo.pesa(0), - } + }, + false ) as AccountingLedgerEntry; this.entries.push(ledgerEntry); diff --git a/models/Transactional/Transactional.ts b/models/Transactional/Transactional.ts index d304d7b6..fc0f2dc5 100644 --- a/models/Transactional/Transactional.ts +++ b/models/Transactional/Transactional.ts @@ -20,25 +20,32 @@ import { LedgerPosting } from './LedgerPosting'; */ export abstract class Transactional extends Doc { - isTransactional = true; + get isTransactional() { + return true; + } + abstract getPosting(): Promise; async validate() { + super.validate(); const posting = await this.getPosting(); posting.validate(); } async afterSubmit(): Promise { + super.afterSubmit(); const posting = await this.getPosting(); await posting.post(); } async afterCancel(): Promise { + super.afterCancel(); const posting = await this.getPosting(); await posting.postReverse(); } async afterDelete(): Promise { + super.afterDelete(); const ledgerEntryIds = (await this.fyo.db.getAll( ModelNameEnum.AccountingLedgerEntry, { @@ -51,7 +58,11 @@ export abstract class Transactional extends Doc { )) as { name: string }[]; for (const { name } of ledgerEntryIds) { - await this.fyo.db.delete(ModelNameEnum.AccountingLedgerEntry, name); + const ledgerEntryDoc = await this.fyo.doc.getDoc( + ModelNameEnum.AccountingLedgerEntry, + name + ); + await ledgerEntryDoc.delete(); } } } diff --git a/models/baseModels/AccountingLedgerEntry/AccountingLedgerEntry.ts b/models/baseModels/AccountingLedgerEntry/AccountingLedgerEntry.ts index 0c0b91c6..91318f94 100644 --- a/models/baseModels/AccountingLedgerEntry/AccountingLedgerEntry.ts +++ b/models/baseModels/AccountingLedgerEntry/AccountingLedgerEntry.ts @@ -41,7 +41,15 @@ export class AccountingLedgerEntry extends Doc { static getListViewSettings(): ListViewSettings { return { - columns: ['account', 'party', 'debit', 'credit', 'balance'], + columns: [ + 'account', + 'party', + 'debit', + 'credit', + 'balance', + 'referenceName', + 'reverted', + ], }; } } diff --git a/models/baseModels/Invoice/Invoice.ts b/models/baseModels/Invoice/Invoice.ts index affaf3a4..540ab938 100644 --- a/models/baseModels/Invoice/Invoice.ts +++ b/models/baseModels/Invoice/Invoice.ts @@ -43,8 +43,13 @@ export abstract class Invoice extends Transactional { await party.updateOutstandingAmount(); } - async beforeCancel() { - await super.beforeCancel(); + async afterCancel() { + await super.afterCancel(); + await this._cancelPayments(); + await this._updatePartyOutStanding(); + } + + async _cancelPayments() { const paymentIds = await this.getPaymentIds(); for (const paymentId of paymentIds) { const paymentDoc = (await this.fyo.doc.getDoc( @@ -55,8 +60,7 @@ export abstract class Invoice extends Transactional { } } - async afterCancel() { - await super.afterCancel(); + async _updatePartyOutStanding() { const partyDoc = (await this.fyo.doc.getDoc( ModelNameEnum.Party, this.party! @@ -69,7 +73,8 @@ export abstract class Invoice extends Transactional { await super.afterDelete(); const paymentIds = await this.getPaymentIds(); for (const name of paymentIds) { - await this.fyo.db.delete(ModelNameEnum.AccountingLedgerEntry, name); + const paymentDoc = await this.fyo.doc.getDoc(ModelNameEnum.Payment, name); + await paymentDoc.delete(); } } diff --git a/models/baseModels/Payment/Payment.ts b/models/baseModels/Payment/Payment.ts index 4419107f..88345efd 100644 --- a/models/baseModels/Payment/Payment.ts +++ b/models/baseModels/Payment/Payment.ts @@ -91,6 +91,10 @@ export class Payment extends Transactional { async validate() { await super.validate(); + if (this.submitted) { + return; + } + this.validateAccounts(); this.validateTotalReferenceAmount(); this.validateWriteOffAccount(); @@ -243,7 +247,8 @@ export class Payment extends Transactional { throw new ValidationError(message); } - async beforeSubmit() { + async afterSubmit() { + await super.afterSubmit(); await this.updateReferenceDocOutstanding(); await this.updatePartyOutstanding(); } @@ -281,6 +286,7 @@ export class Payment extends Transactional { const outstandingAmount = (refDoc.outstandingAmount as Money).add( ref.amount! ); + await refDoc.setAndSync({ outstandingAmount }); } } diff --git a/schemas/app/Account.json b/schemas/app/Account.json index 4e61ae7a..651a57bb 100644 --- a/schemas/app/Account.json +++ b/schemas/app/Account.json @@ -37,13 +37,14 @@ "label": "Expense" } ], - "required": true + "readOnly": true }, { "fieldname": "parentAccount", "label": "Parent Account", "fieldtype": "Link", - "target": "Account" + "target": "Account", + "readOnly": true }, { "fieldname": "accountType", @@ -138,9 +139,17 @@ { "fieldname": "isGroup", "label": "Is Group", - "fieldtype": "Check" + "fieldtype": "Check", + "readOnly": true } ], - "quickEditFields": ["name", "rootType", "parentAccount", "accountType"], + "quickEditFields": [ + "name", + "rootType", + "parentAccount", + "accountType", + "isGroup", + "balance" + ], "keywordFields": ["name", "rootType", "accountType"] } diff --git a/src/components/Dropdown.vue b/src/components/Dropdown.vue index 10e1469f..56d593de 100644 --- a/src/components/Dropdown.vue +++ b/src/components/Dropdown.vue @@ -183,16 +183,16 @@ export default { return emptyMessage; }, - selectItem(d) { + async selectItem(d) { if (!d.action) { return; } if (this.doc) { - return d.action(this.doc, this.$router); + return await d.action(this.doc, this.$router); } - - d.action() + + await d.action(); }, toggleDropdown(flag) { if (flag == null) { @@ -201,11 +201,11 @@ export default { this.isShown = Boolean(flag); } }, - selectHighlightedItem() { + async selectHighlightedItem() { if (![-1, this.items.length].includes(this.highlightedIndex)) { // valid selection let item = this.items[this.highlightedIndex]; - this.selectItem(item); + await this.selectItem(item); } }, highlightItemUp() { diff --git a/src/components/TwoColumnForm.vue b/src/components/TwoColumnForm.vue index db5c25c5..13e62801 100644 --- a/src/components/TwoColumnForm.vue +++ b/src/components/TwoColumnForm.vue @@ -10,7 +10,7 @@ :df="df" :value="doc[df.fieldname]" :read-only="evaluateReadOnly(df)" - @change="(value) => onChange(df, value)" + @change="async (value) => await onChange(df, value)" /> @@ -71,9 +71,9 @@ :value="getRegularValue(df)" :class="{ 'p-2': df.fieldtype === 'Check' }" :read-only="evaluateReadOnly(df)" - @change="(value) => onChange(df, value)" + @change="async (value) => await onChange(df, value)" @focus="activateInlineEditing(df)" - @new-doc="(newdoc) => onChange(df, newdoc.name)" + @new-doc="async (newdoc) => await onChange(df, newdoc.name)" />
{ - this.errors[df.fieldname] = getErrorMessage(e, this.doc); - }) - .then((success) => { - if (!success) { - return; - } + async onChangeCommon(df, value, oldValue) { + let isSet = false; + try { + isSet = this.doc.set(df.fieldname, value); + } catch (err) { + this.errors[df.fieldname] = getErrorMessage(err, this.doc); + } - this.handlePostSet(df, value, oldValue); - }); + if (!isSet) { + return; + } + + await this.handlePostSet(df, value, oldValue); }, - handlePostSet(df, value, oldValue) { + async handlePostSet(df, value, oldValue) { this.setFormFields(); if (this.emitChange) { this.$emit('change', df, value, oldValue); @@ -212,14 +212,22 @@ export default { return; } - this.doc.sync(); + await this.doc.sync(); } }, - sync() { - return this.doc.sync().catch((e) => handleErrorWithDialog(e, this.doc)); + async sync() { + try { + await this.doc.sync(); + } catch (err) { + await handleErrorWithDialog(err, this.doc); + } }, - submit() { - return this.doc.submit().catch((e) => handleErrorWithDialog(e, this.doc)); + async submit() { + try { + await this.doc.submit(); + } catch (err) { + await handleErrorWithDialog(err, this.doc); + } }, async activateInlineEditing(df) { if (!df.inline) { @@ -229,8 +237,8 @@ export default { this.inlineEditField = df; if (!this.doc[df.fieldname]) { this.inlineEditDoc = await fyo.doc.getNewDoc(df.target); - this.inlineEditDoc.once('afterSync', () => { - this.onChangeCommon(df, this.inlineEditDoc.name); + this.inlineEditDoc.once('afterSync', async () => { + await this.onChangeCommon(df, this.inlineEditDoc.name); }); } else { this.inlineEditDoc = this.doc.getLink(df.fieldname); diff --git a/src/errorHandling.ts b/src/errorHandling.ts index ec6ca811..4979816a 100644 --- a/src/errorHandling.ts +++ b/src/errorHandling.ts @@ -102,12 +102,12 @@ export function handleError( } } -export function handleErrorWithDialog(error: Error, doc?: Doc) { +export async function handleErrorWithDialog(error: Error, doc?: Doc) { const errorMessage = getErrorMessage(error, doc); handleError(false, error, { errorMessage, doc }); const name = (error as BaseError).label ?? error.name; - showMessageDialog({ message: name, detail: errorMessage }); + await showMessageDialog({ message: name, detail: errorMessage }); throw error; } diff --git a/src/pages/ChartOfAccounts.vue b/src/pages/ChartOfAccounts.vue index 092933a4..0a32ae76 100644 --- a/src/pages/ChartOfAccounts.vue +++ b/src/pages/ChartOfAccounts.vue @@ -276,7 +276,7 @@ export default { await this.fetchChildren(parentAccount, true); // open quick edit - openQuickEdit({ + await openQuickEdit({ schemaName: 'Account', name: account.name, }); @@ -285,7 +285,7 @@ export default { } catch (e) { // unfreeze input this.insertingAccount = false; - handleErrorWithDialog(e, account); + await handleErrorWithDialog(e, account); } }, isQuickEditOpen(account) { diff --git a/src/pages/DataImport.vue b/src/pages/DataImport.vue index 6a9becc5..f5a7e08d 100644 --- a/src/pages/DataImport.vue +++ b/src/pages/DataImport.vue @@ -533,33 +533,30 @@ export default { } if (this.isRequiredUnassigned) { - showMessageDialog({ + return await showMessageDialog({ message: this.t`Required Fields not Assigned`, detail: this .t`Please assign the following fields ${this.requiredUnassigned.join( ', ' )}`, }); - return; } if (this.importer.assignedMatrix.length === 0) { - showMessageDialog({ + return await showMessageDialog({ message: this.t`No Data to Import`, detail: this.t`Please select a file with data to import.`, }); - return; } const { success, names, message } = await this.importer.importData( this.setLoadingStatus ); if (!success) { - showMessageDialog({ + return await showMessageDialog({ message: this.t`Import Failed`, detail: message, }); - return; } this.names = names; @@ -593,7 +590,9 @@ export default { await ipcRenderer.invoke(IPC_ACTIONS.GET_FILE, options); if (!success && !canceled) { - showMessageDialog({ message: this.t`File selection failed.` }); + return await showMessageDialog({ + message: this.t`File selection failed.`, + }); } if (!success || canceled) { @@ -603,11 +602,10 @@ export default { const text = new TextDecoder().decode(data); const isValid = this.importer.selectFile(text); if (!isValid) { - showMessageDialog({ + return await showMessageDialog({ message: this.t`Bad import data.`, detail: this.t`Could not select file.`, }); - return; } this.file = { diff --git a/src/pages/InvoiceForm.vue b/src/pages/InvoiceForm.vue index b9ebebf9..0f11d010 100644 --- a/src/pages/InvoiceForm.vue +++ b/src/pages/InvoiceForm.vue @@ -276,7 +276,7 @@ export default { routeTo(`/list/${this.schemaName}`); return; } - this.handleError(error); + await this.handleError(error); } this.printSettings = await fyo.doc.getSingle('PrintSettings'); this.companyName = ( @@ -305,20 +305,29 @@ export default { return fyo.getField(this.schemaName, fieldname); }, async sync() { - return this.doc.sync().catch(this.handleError); + try { + await this.doc.sync(); + } catch (err) { + await this.handleError(err); + } }, - submit() { + async submit() { const message = this.schemaName === ModelNameEnum.SalesInvoice ? this.t`Submit Sales Invoice?` : this.t`Submit Purchase Invoice?`; - showMessageDialog({ + const ref = this + await showMessageDialog({ message, buttons: [ { label: this.t`Yes`, - action: () => { - this.doc.submit().catch(this.handleError); + async action() { + try { + await ref.doc.submit(); + } catch (err) { + await ref.handleError(err); + } }, }, { @@ -328,8 +337,8 @@ export default { ], }); }, - handleError(e) { - handleErrorWithDialog(e, this.doc); + async handleError(e) { + await handleErrorWithDialog(e, this.doc); }, openInvoiceSettings() { openSettings('Invoice'); diff --git a/src/pages/JournalEntryForm.vue b/src/pages/JournalEntryForm.vue index 4b62ef1c..d751cb86 100644 --- a/src/pages/JournalEntryForm.vue +++ b/src/pages/JournalEntryForm.vue @@ -200,7 +200,7 @@ export default { return; } - this.handleError(error); + await this.handleError(error); } if (fyo.store.isDevelopment) { @@ -238,16 +238,25 @@ export default { return fyo.getField(ModelNameEnum.JournalEntry, fieldname); }, async sync() { - return this.doc.sync().catch(this.handleError); + try { + await this.doc.sync(); + } catch (err) { + this.handleError(err); + } }, async submit() { - showMessageDialog({ + const ref = this; + await showMessageDialog({ message: this.t`Submit Journal Entry?`, buttons: [ { label: this.t`Yes`, - action: () => { - this.doc.submit().catch(this.handleError); + async action() { + try { + await ref.doc.submit(); + } catch (err) { + await ref.handleError(err); + } }, }, { @@ -257,8 +266,8 @@ export default { ], }); }, - handleError(e) { - handleErrorWithDialog(e, this.doc); + async handleError(e) { + await handleErrorWithDialog(e, this.doc); }, }, }; diff --git a/src/pages/ListView/List.vue b/src/pages/ListView/List.vue index 9868260d..d17baf94 100644 --- a/src/pages/ListView/List.vue +++ b/src/pages/ListView/List.vue @@ -120,7 +120,7 @@ export default { }, methods: { setUpdateListeners() { - const listener = () => { + const listener = (name) => { this.updateData(); }; @@ -130,7 +130,7 @@ export default { } fyo.doc.observer.on(`sync:${this.schemaName}`, listener); - fyo.doc.observer.on(`delete:${this.schemaName}`, listener); + fyo.db.observer.on(`delete:${this.schemaName}`, listener); fyo.doc.observer.on(`rename:${this.schemaName}`, listener); }, openForm(doc) { diff --git a/src/pages/SetupWizard/SetupWizard.vue b/src/pages/SetupWizard/SetupWizard.vue index bc3af5df..64cf4977 100644 --- a/src/pages/SetupWizard/SetupWizard.vue +++ b/src/pages/SetupWizard/SetupWizard.vue @@ -180,8 +180,9 @@ export default { }, async submit() { if (!this.allValuesFilled()) { - showMessageDialog({ message: this.t`Please fill all values` }); - return; + return await showMessageDialog({ + message: this.t`Please fill all values`, + }); } this.loading = true; diff --git a/src/utils/types.ts b/src/utils/types.ts index 4aa2b153..d9809bb1 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -1,6 +1,6 @@ export interface MessageDialogButton { label: string; - action: () => void; + action: () => Promise | unknown; } export interface MessageDialogOptions { diff --git a/src/utils/ui.ts b/src/utils/ui.ts index 731912d7..52ade811 100644 --- a/src/utils/ui.ts +++ b/src/utils/ui.ts @@ -87,9 +87,11 @@ export async function showMessageDialog({ )) as { response: number }; const button = buttons[response]; - if (button && button.action) { - button.action(); + if (!button?.action) { + return null; } + + return await button.action(); } export async function showToast(options: ToastOptions) { @@ -138,37 +140,36 @@ export async function routeTo(route: string | RouteLocationRaw) { await router.push(routeOptions); } -export function deleteDocWithPrompt(doc: Doc) { +export async function deleteDocWithPrompt(doc: Doc) { const schemaLabel = fyo.schemaMap[doc.schemaName]!.label; let detail = t`This action is permanent.`; if (doc.isTransactional) { - detail = t`This action is permanent and will delete all ledger entries.`; + detail = t`This action is permanent and will delete associated ledger entries.`; } - return new Promise((resolve) => { - showMessageDialog({ - message: t`Delete ${schemaLabel} ${doc.name!}?`, - detail, - buttons: [ - { - label: t`Delete`, - action: () => { - doc - .delete() - .then(() => resolve(true)) - .catch((e: Error) => { - handleErrorWithDialog(e, doc); - }); - }, + return await showMessageDialog({ + message: t`Delete ${schemaLabel} ${doc.name!}?`, + detail, + buttons: [ + { + label: t`Delete`, + async action() { + try { + await doc.delete(); + return true; + } catch (err) { + handleErrorWithDialog(err as Error, doc); + return false; + } }, - { - label: t`Cancel`, - action() { - resolve(false); - }, + }, + { + label: t`Cancel`, + action() { + return false; }, - ], - }); + }, + ], }); } @@ -204,30 +205,30 @@ export async function cancelDocWithPrompt(doc: Doc) { } } - return new Promise((resolve) => { - const schemaLabel = fyo.schemaMap[doc.schemaName]!.label; - showMessageDialog({ - message: t`Cancel ${schemaLabel} ${doc.name!}?`, - detail, - buttons: [ - { - label: t`Yes`, - async action() { - const entryDoc = await fyo.doc.getDoc(doc.schemaName, doc.name!); - entryDoc - .cancel() - .then(() => resolve(true)) - .catch((e) => handleErrorWithDialog(e, doc)); - }, + 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; + } }, - { - label: t`No`, - action() { - resolve(false); - }, + }, + { + label: t`No`, + action() { + return false; }, - ], - }); + }, + ], }); } @@ -258,13 +259,13 @@ function getCancelAction(doc: Doc): Action { component: { template: '{{ t`Cancel` }}', }, - condition: (doc: Doc) => !!doc.submitted && !doc.cancelled, - action: () => { - cancelDocWithPrompt(doc).then((res) => { - if (res) { - router.push(`/list/${doc.schemaName}`); - } - }); + condition: (doc: Doc) => doc.isSubmitted, + async action() { + const res = await cancelDocWithPrompt(doc); + + if (res) { + router.push(`/list/${doc.schemaName}`); + } }, }; } @@ -278,12 +279,12 @@ function getDeleteAction(doc: Doc): Action { condition: (doc: Doc) => (!doc.notInserted && !doc.schema.isSubmittable && !doc.schema.isSingle) || doc.isCancelled, - action: () => - deleteDocWithPrompt(doc).then((res) => { - if (res) { - routeTo(`/list/${doc.schemaName}`); - } - }), + async action() { + const res = await deleteDocWithPrompt(doc); + if (res) { + routeTo(`/list/${doc.schemaName}`); + } + }, }; } @@ -297,20 +298,26 @@ function getDuplicateAction(doc: Doc): Action { !doc.notInserted && !(doc.cancelled || false) ), - action: () => { - showMessageDialog({ + async action() { + await showMessageDialog({ message: t`Duplicate ${doc.schemaName} ${doc.name!}?`, buttons: [ { label: t`Yes`, async action() { - doc.duplicate(); + try { + doc.duplicate(); + return true; + } catch (err) { + handleErrorWithDialog(err as Error, doc); + return false; + } }, }, { label: t`No`, action() { - // no-op + return false; }, }, ],