diff --git a/client/web/compose/src/components/Common/RecordToolbar.vue b/client/web/compose/src/components/Common/RecordToolbar.vue index 126db4dc7..cdb7cc45a 100644 --- a/client/web/compose/src/components/Common/RecordToolbar.vue +++ b/client/web/compose/src/components/Common/RecordToolbar.vue @@ -57,7 +57,7 @@ findLabel(v, this.field.options.options) || v) } else { return findLabel(v, this.field.options.options) || v diff --git a/client/web/compose/src/components/PageBlocks/RecordBase.vue b/client/web/compose/src/components/PageBlocks/RecordBase.vue index 31d13e844..7ab167ba8 100644 --- a/client/web/compose/src/components/PageBlocks/RecordBase.vue +++ b/client/web/compose/src/components/PageBlocks/RecordBase.vue @@ -193,7 +193,9 @@ export default { ...resolutions, this.evaluateExpressions(), ]).finally(() => { - this.evaluating = false + setTimeout(() => { + this.evaluating = false + }, 300) }) if (this.options.referenceModuleID) { diff --git a/client/web/compose/src/components/PageBlocks/RecordEditor.vue b/client/web/compose/src/components/PageBlocks/RecordEditor.vue index 4ccac8f33..267622e6b 100644 --- a/client/web/compose/src/components/PageBlocks/RecordEditor.vue +++ b/client/web/compose/src/components/PageBlocks/RecordEditor.vue @@ -170,7 +170,9 @@ export default { ...resolutions, this.evaluateExpressions(), ]).finally(() => { - this.evaluating = false + setTimeout(() => { + this.evaluating = false + }, 300) }) }, }, diff --git a/client/web/compose/src/components/PageBlocks/RecordListBase.vue b/client/web/compose/src/components/PageBlocks/RecordListBase.vue index 11b441132..33d64d9da 100644 --- a/client/web/compose/src/components/PageBlocks/RecordListBase.vue +++ b/client/web/compose/src/components/PageBlocks/RecordListBase.vue @@ -971,7 +971,7 @@ export default { } else { // Record list block does not have any configured fields // Use first five fields from the module. - fields = this.recordListModule.fields.slice(0, 5) + fields = [...this.recordListModule.fields.slice(0, 5), ...this.recordListModule.systemFields()] } const configured = fields.map(mf => ({ @@ -1366,7 +1366,7 @@ export default { } const filter = [] - let sort = '' + let sort = 'createdAt DESC' if (presort) { sort = presort diff --git a/client/web/compose/src/mixins/record.js b/client/web/compose/src/mixins/record.js index ca7c4d1c6..14ff1def4 100644 --- a/client/web/compose/src/mixins/record.js +++ b/client/web/compose/src/mixins/record.js @@ -176,7 +176,7 @@ export default { notPageID: this.page.pageID, }) } else { - this.$router.push({ name: route, params: { ...this.$route.params, recordID: this.record.recordID } }) + this.$router.push({ name: route, params: { ...this.$route.params, recordID: this.record.recordID, edit: false } }) } } @@ -185,7 +185,6 @@ export default { .catch(this.toastErrorHandler(this.$t(`notification:record.${isNew ? 'create' : 'update'}Failed`))) .finally(() => { this.processingSubmit = false - this.processing = false }) }, 500), @@ -193,7 +192,7 @@ export default { * Handle form submit for record browser * @returns {Promise} */ - handleFormSubmitSimple: throttle(function (route = 'page.record') { + handleFormSubmitSimple: throttle(function (route = 'admin.modules.record.view') { this.processingSubmit = true this.processing = true @@ -229,11 +228,10 @@ export default { if (this.record.valueErrors.set) { this.toastWarning(this.$t('notification:record.validationWarnings')) } else { - this.edit = false this.record = record this.initialRecordState = this.record.clone() - this.$router.push({ name: route, params: { ...this.$route.params, recordID: record.recordID } }) + this.$router.push({ name: route, params: { ...this.$route.params, recordID: record.recordID, edit: false } }) } }) .catch(this.toastErrorHandler(this.$t( @@ -257,19 +255,20 @@ export default { return this .dispatchUiEvent('beforeDelete') - .then(this.$ComposeAPI.recordDelete(this.record)) - .then(() => { - this.record.deletedAt = (new Date()).toISOString() - }) + .then(() => this.$ComposeAPI.recordDelete(this.record)) .then(this.dispatchUiEvent('afterDelete')) .then(this.updatePrompts()) - .then(this.loadRecord) - .then(this.toastSuccess(this.$t('notification:record.deleteSuccess'))) - .catch(this.toastErrorHandler(this.$t('notification:record.deleteFailed'))) - .finally(() => { - this.processing = false + .then(() => { + this.record = undefined + this.initialRecordState = undefined + + return this.refresh() + }).then(() => { + this.toastSuccess(this.$t('notification:record.deleteSuccess')) + }).finally(() => { this.processingDelete = false - }) + this.processing = false + }).catch(this.toastErrorHandler(this.$t('notification:record.deleteFailed'))) }, 500), handleUndelete: throttle(function () { @@ -278,16 +277,20 @@ export default { return this .dispatchUiEvent('beforeUndelete') - .then(this.$ComposeAPI.recordUndelete(this.record)) + .then(() => this.$ComposeAPI.recordUndelete(this.record)) .then(this.dispatchUiEvent('afterUndelete')) .then(this.updatePrompts()) - .then(this.loadRecord) - .then(this.toastSuccess(this.$t('notification:record.restoreSuccess'))) - .catch(this.toastErrorHandler(this.$t('notification:record.restoreFailed'))) - .finally(() => { + .then(() => { + this.record = undefined + this.initialRecordState = undefined + + return this.refresh() + }).then(() => { + this.toastSuccess(this.$t('notification:record.restoreSuccess')) + }).finally(() => { this.processingUndelete = false this.processing = false - }) + }).catch(this.toastErrorHandler(this.$t('notification:record.restoreFailed'))) }, 500), handleBulkUpdateSelectedRecords: throttle(function (query) { diff --git a/client/web/compose/src/views/Admin/Modules/Records/Create.vue b/client/web/compose/src/views/Admin/Modules/Records/Create.vue deleted file mode 100644 index d07543249..000000000 --- a/client/web/compose/src/views/Admin/Modules/Records/Create.vue +++ /dev/null @@ -1,20 +0,0 @@ - diff --git a/client/web/compose/src/views/Admin/Modules/Records/Edit.vue b/client/web/compose/src/views/Admin/Modules/Records/Edit.vue deleted file mode 100644 index 4ead75430..000000000 --- a/client/web/compose/src/views/Admin/Modules/Records/Edit.vue +++ /dev/null @@ -1,20 +0,0 @@ - diff --git a/client/web/compose/src/views/Admin/Modules/Records/List.vue b/client/web/compose/src/views/Admin/Modules/Records/List.vue index 7a7325eca..a4c079b2e 100644 --- a/client/web/compose/src/views/Admin/Modules/Records/List.vue +++ b/client/web/compose/src/views/Admin/Modules/Records/List.vue @@ -29,7 +29,7 @@ { - this.toastSuccess(this.$t('notification:module.columns.saved')) - }).catch(this.toastErrorHandler(this.$t('notification:module.columns.saveFailed'))) + this.updateModule(this.module) }, setDefaultValues () { this.block = undefined - this.namespace = {} }, }, } diff --git a/client/web/compose/src/views/Admin/Modules/Records/View.vue b/client/web/compose/src/views/Admin/Modules/Records/View.vue index 9d3fe4bba..b65dfe1ad 100644 --- a/client/web/compose/src/views/Admin/Modules/Records/View.vue +++ b/client/web/compose/src/views/Admin/Modules/Records/View.vue @@ -63,7 +63,7 @@ @@ -128,14 +128,37 @@ export default { ], beforeRouteLeave (to, from, next) { - this.checkUnsavedChanges(next, to) + next(this.checkUnsavedChanges()) }, beforeRouteUpdate (to, from, next) { - this.checkUnsavedChanges(next, to) + next(this.checkUnsavedChanges()) }, props: { + namespace: { + type: Object, + required: false, + default: undefined, + }, + + moduleID: { + type: String, + required: false, + default: '', + }, + + recordID: { + type: String, + required: false, + default: '', + }, + + edit: { + type: Boolean, + default: false, + }, + // If component was called (via router) with some pre-seed values values: { type: Object, @@ -146,15 +169,9 @@ export default { data () { return { - inEditing: false, - inCreating: false, - blocks: [], - bindParams: { - page: new compose.Page(), - namespace: this.$attrs.namespace, - }, + page: new compose.Page(), recordNavigation: { prev: undefined, @@ -171,19 +188,19 @@ export default { }), isNew () { - return this.record.recordID === NoID + return !this.recordID || this.recordID === NoID }, title () { const { name, handle } = this.module - const titlePrefix = this.inCreating ? 'create' : this.inEditing ? 'edit' : 'view' + const titlePrefix = this.isNew ? 'create' : this.inEditing ? 'edit' : 'view' return this.$t(`allRecords.${titlePrefix}.title`, { name: name || handle, interpolation: { escapeValue: false } }) }, module () { - if (this.$attrs.moduleID) { - return this.getModuleByID(this.$attrs.moduleID) + if (this.moduleID) { + return this.getModuleByID(this.moduleID) } else { return undefined } @@ -239,12 +256,20 @@ export default { }, watch: { - '$attrs.recordID': { + recordID: { immediate: true, handler () { this.record = undefined this.initialRecordState = undefined - this.loadRecord() + + this.refresh() + }, + }, + + edit: { + immediate: true, + handler (edit) { + this.inEditing = edit }, }, @@ -279,21 +304,25 @@ export default { createBlocks () { this.fields.forEach(f => { const options = { - moduleID: this.$attrs.moduleID, + moduleID: this.moduleID, fields: f, } this.blocks.push(new compose.PageBlockRecord({ options })) }) }, + refresh () { + return this.loadRecord() + }, + loadRecord () { - const { moduleID = NoID, recordID = NoID } = this.$attrs + const { moduleID = NoID, recordID = NoID } = this if (!moduleID || moduleID === NoID) return const module = Object.freeze(this.getModuleByID(moduleID).clone()) if (recordID && recordID !== NoID) { - const { namespaceID } = this.$attrs.namespace + const { namespaceID } = this.namespace const { response, cancel } = this.$ComposeAPI .recordReadCancellable({ namespaceID, moduleID, recordID }) @@ -312,8 +341,7 @@ export default { }) } else { this.record = new compose.Record(module, { values: this.values }) - this.inEditing = true - this.inCreating = true + this.initialRecordState = undefined } }, @@ -322,21 +350,19 @@ export default { }, handleAdd () { - this.$router.push({ name: 'admin.modules.record.create', params: { moduleID: this.module.moduleID } }) + this.$router.push({ name: 'admin.modules.record.create', params: { moduleID: this.module.moduleID, edit: true } }) }, handleClone () { - this.$router.push({ name: 'admin.modules.record.create', params: { moduleID: this.module.moduleID, values: this.record.values } }) + this.$router.push({ name: 'admin.modules.record.create', params: { moduleID: this.module.moduleID, values: this.record.values, edit: true } }) }, handleEdit () { - this.inEditing = true - this.inCreating = false + this.$router.push({ name: 'admin.modules.record.edit', params: { moduleID: this.module.moduleID, edit: true } }) }, handleView () { - this.inEditing = false - this.inCreating = false + this.$router.push({ name: 'admin.modules.record.view', params: { moduleID: this.module.moduleID, edit: false } }) }, handleRedirectToPrevOrNext (recordID) { @@ -348,7 +374,6 @@ export default { }, setDefaultValues () { - this.inEditing = false this.blocks = [] this.bindParams = {} this.abortableRequests = [] @@ -360,15 +385,25 @@ export default { }) }, - checkUnsavedChanges (next, to) { - if (this.isNew) { - next(true) - } else { - const recordValues = JSON.parse(JSON.stringify(this.record.values)) - const initialRecordState = JSON.parse(JSON.stringify(this.initialRecordState.values)) + compareRecordValues () { + const recordValues = JSON.parse(JSON.stringify(this.record ? this.record.values : {})) + const initialRecordState = JSON.parse(JSON.stringify(this.initialRecordState ? this.initialRecordState.values : {})) - next(!isEqual(recordValues, initialRecordState) ? window.confirm(this.$t('general:editor.unsavedChanges')) : true) + return !isEqual(recordValues, initialRecordState) + }, + + checkUnsavedChanges () { + if (!this.edit) return true + + const recordStateChange = this.compareRecordValues() ? window.confirm(this.$t('general:record.unsavedChanges')) : true + + if (!recordStateChange) { + this.processing = false + } else { + this.record = this.initialRecordState ? this.initialRecordState.clone() : undefined } + + return recordStateChange }, }, } diff --git a/client/web/compose/src/views/Public/Pages/Records/View.vue b/client/web/compose/src/views/Public/Pages/Records/View.vue index db0ba8075..43ffd00c2 100644 --- a/client/web/compose/src/views/Public/Pages/Records/View.vue +++ b/client/web/compose/src/views/Public/Pages/Records/View.vue @@ -30,7 +30,6 @@ :blocks="blocks" :mode="inEditing ? 'editor' : 'base'" class="h-100" - @reload="loadRecord()" /> { return new Promise(resolve => setTimeout(resolve, 300)).then(() => { - this.record = new compose.Record(module, record) - this.initialRecordState = this.record.clone() + return new compose.Record(module, record) }) }) .catch(e => { @@ -421,8 +413,7 @@ export default { } } - this.record = new compose.Record(module, { values: this.values }) - this.initialRecordState = this.record.clone() + return new compose.Record(module, { values: this.values }) } } }, @@ -433,7 +424,7 @@ export default { * came from (and "where" is back). */ if (this.showRecordModal) { - if (this.checkUnsavedChangesOnModal()) { + if (this.checkUnsavedChanges()) { this.popModalPreviousPage().then(({ recordID, recordPageID, edit }) => { this.$emit('on-modal-back', { recordID, recordPageID, pushModalPreviousPage: false, edit }) }) @@ -454,11 +445,11 @@ export default { this.processing = true if (this.showRecordModal) { - if (this.checkUnsavedChangesOnModal()) { + if (this.checkUnsavedChanges()) { this.$emit('handle-record-redirect', { recordID: NoID, recordPageID: this.page.pageID, edit: true }) } } else { - this.$router.push({ name: 'page.record.create', params: this.newRouteParams }) + this.$router.push({ name: 'page.record.create', params: { pageID: this.page.pageID, edit: true } }) } }, @@ -466,11 +457,11 @@ export default { this.processing = true if (this.showRecordModal) { - if (this.checkUnsavedChangesOnModal()) { + if (this.checkUnsavedChanges()) { this.$emit('handle-record-redirect', { recordID: NoID, recordPageID: this.page.pageID, values: this.record.values, edit: true }) } } else { - this.$router.push({ name: 'page.record.create', params: { pageID: this.page.pageID, values: this.record.values } }) + this.$router.push({ name: 'page.record.create', params: { pageID: this.page.pageID, values: this.record.values, edit: true } }) } }, @@ -480,7 +471,7 @@ export default { if (this.showRecordModal) { this.$emit('handle-record-redirect', { recordID: this.recordID, recordPageID: this.page.pageID, edit: true }) } else { - this.$router.push({ name: 'page.record.edit', params: { recordID: this.recordID, pageID: this.page.pageID } }) + this.$router.push({ name: 'page.record.edit', params: { recordID: this.recordID, pageID: this.page.pageID, edit: true } }) } }, @@ -488,11 +479,11 @@ export default { this.processing = true if (this.showRecordModal) { - if (this.checkUnsavedChangesOnModal()) { + if (this.checkUnsavedChanges()) { this.$emit('handle-record-redirect', { recordID: this.recordID, recordPageID: this.page.pageID, edit: false }) } } else { - this.$router.push({ name: 'page.record', params: { recordID: this.recordID, pageID: this.page.pageID } }) + this.$router.push({ name: 'page.record', params: { recordID: this.recordID, pageID: this.page.pageID, edit: false } }) } }, @@ -502,7 +493,7 @@ export default { this.processing = true if (this.showRecordModal) { - if (this.checkUnsavedChangesOnModal()) { + if (this.checkUnsavedChanges()) { this.$emit('handle-record-redirect', { recordID, recordPageID: this.page.pageID }) } } else { @@ -549,7 +540,7 @@ export default { }) }, - async determineLayout (pageLayoutID, variables = {}) { + async determineLayout (pageLayoutID, variables = {}, redirectOnFail = true) { // Clear stored records so they can be refetched with latest values this.clearRecordSet() let expressions = {} @@ -576,7 +567,10 @@ export default { if (!matchedLayout) { this.toastWarning(this.$t('notification:page.page-layout.notFound.view')) - this.$router.go(-1) + + if (redirectOnFail) { + this.$router.go(-1) + } return } @@ -620,15 +614,21 @@ export default { } // Refetch the record and other page blocks that use records - this.loadRecord() + this.loadRecord().then(record => { + this.record = record + this.initialRecordState = record.clone() + }) this.$root.$emit(`refetch-non-record-blocks:${this.page.pageID}`) }, async refresh (variables = {}) { this.processing = true - return this.loadRecord().then(() => { - return this.determineLayout(undefined, variables) + return this.loadRecord().then(record => { + return this.determineLayout(undefined, variables).then(() => { + this.record = record + this.initialRecordState = record.clone() + }) }).finally(() => { this.processing = false }) @@ -638,7 +638,7 @@ export default { if (kind === 'toLayout') { this.processing = true - this.determineLayout(params.pageLayoutID).finally(() => { + this.determineLayout(params.pageLayoutID, {}, false).finally(() => { this.processing = false }) } else if (kind === 'toURL') { @@ -666,7 +666,7 @@ export default { this.$root.$off('refetch-record-blocks', this.refetchRecordBlocks) if (this.showRecordModal) { - this.$root.$off('bv::modal::hide', this.checkUnsavedChangesOnModal) + this.$root.$off('bv::modal::hide', this.checkUnsavedChanges) } }, @@ -677,26 +677,19 @@ export default { return !isEqual(recordValues, initialRecordState) }, - checkUnsavedChanges (next) { - if (!this.edit) { - next(true) - return - } - - next(this.compareRecordValues() ? window.confirm(this.$t('general:module.unsavedChanges')) : true) - }, - - checkUnsavedChangesOnModal (bvEvent, modalId) { + checkUnsavedChanges (bvEvent, modalId) { if ((bvEvent && modalId !== 'record-modal') || !this.edit) return true - const recordStateChange = this.compareRecordValues() ? window.confirm(this.$t('general:module.unsavedChanges')) : true + const recordStateChange = this.compareRecordValues() ? window.confirm(this.$t('general:record.unsavedChanges')) : true - if (bvEvent && !recordStateChange) { - bvEvent.preventDefault() - } + if (!recordStateChange) { + this.processing = false - if (recordStateChange) { - this.initialRecordState = this.record.clone() + if (bvEvent) { + bvEvent.preventDefault() + } + } else { + this.record = this.initialRecordState ? this.initialRecordState.clone() : undefined } return recordStateChange diff --git a/client/web/compose/src/views/routes.js b/client/web/compose/src/views/routes.js index 7df4ec720..506e44a9e 100644 --- a/client/web/compose/src/views/routes.js +++ b/client/web/compose/src/views/routes.js @@ -39,8 +39,8 @@ export default [ ...r('page', ':pageID?', 'Public/Pages/View'), children: [ - r('page.record.edit', 'record/:recordID/edit', 'Public/Pages/Records/View', { edit: true }), r('page.record', 'record/:recordID', 'Public/Pages/Records/View', { edit: false }), + r('page.record.edit', 'record/:recordID/edit', 'Public/Pages/Records/View', { edit: true }), r('page.record.create', 'record', 'Public/Pages/Records/View', { edit: true }), ], }, @@ -55,9 +55,9 @@ export default [ r('admin.modules.create', 'modules/new', 'Admin/Modules/Edit'), r('admin.modules.edit', 'modules/:moduleID/edit', 'Admin/Modules/Edit'), r('admin.modules.record.list', 'modules/:moduleID/record/list', 'Admin/Modules/Records/List'), - r('admin.modules.record.view', 'modules/:moduleID/record/:recordID', 'Admin/Modules/Records/View'), - r('admin.modules.record.create', 'modules/:moduleID/record', 'Admin/Modules/Records/Create'), - r('admin.modules.record.edit', 'modules/:moduleID/record/:recordID/edit', 'Admin/Modules/Records/Edit'), + r('admin.modules.record.view', 'modules/:moduleID/record/:recordID', 'Admin/Modules/Records/View', { edit: false }), + r('admin.modules.record.create', 'modules/:moduleID/record', 'Admin/Modules/Records/View', { edit: true }), + r('admin.modules.record.edit', 'modules/:moduleID/record/:recordID/edit', 'Admin/Modules/Records/View', { edit: true }), r('admin.pages', 'pages', 'Admin/Pages/List'), r('admin.pages.edit', 'pages/:pageID/edit', 'Admin/Pages/Edit'), diff --git a/lib/js/src/compose/types/module.ts b/lib/js/src/compose/types/module.ts index d129e99a0..633a6170b 100644 --- a/lib/js/src/compose/types/module.ts +++ b/lib/js/src/compose/types/module.ts @@ -87,12 +87,12 @@ interface RecordDeDupRule { */ export const systemFields = Object.freeze([ { isSystem: true, name: 'recordID', label: 'Record ID', kind: 'String' }, - { isSystem: true, name: 'revision', label: 'Revision', kind: 'Number' }, { isSystem: true, name: 'ownedBy', label: 'Owned by', kind: 'User' }, { isSystem: true, name: 'createdBy', label: 'Created by', kind: 'User' }, { isSystem: true, name: 'createdAt', label: 'Created at', kind: 'DateTime' }, { isSystem: true, name: 'updatedBy', label: 'Updated by', kind: 'User' }, { isSystem: true, name: 'updatedAt', label: 'Updated at', kind: 'DateTime' }, + { isSystem: true, name: 'revision', label: 'Revision', kind: 'Number' }, { isSystem: true, name: 'deletedBy', label: 'Deleted by', kind: 'User' }, { isSystem: true, name: 'deletedAt', label: 'Deleted at', kind: 'DateTime' }, ].map(f => ModuleFieldMaker(f))) diff --git a/locale/en/corteza-webapp-admin/general.yaml b/locale/en/corteza-webapp-admin/general.yaml index 9dde0b406..24adff57a 100644 --- a/locale/en/corteza-webapp-admin/general.yaml +++ b/locale/en/corteza-webapp-admin/general.yaml @@ -54,6 +54,7 @@ label: fileTypeNotAllowed: File type not allowed editor: unsavedChanges: Unsaved changes will be lost. Do you wish to leave the page? +record: unsavedChanges: Unsaved changes will be lost. Do you wish to close the record? resourceList: pagination: