From f7140ec2ae0128624ce41186cbfe9abb402fe444 Mon Sep 17 00:00:00 2001 From: Matthew White Date: Fri, 20 Dec 2024 10:41:03 -0500 Subject: [PATCH] Add ability to show component in alert --- src/alert.js | 53 +++++++++++++--------- src/components/alert.vue | 7 ++- src/components/alert/40917.vue | 46 +++++++++++++++++++ src/locales/en.json5 | 9 +--- src/util/request.js | 9 ++-- test/assertions.js | 17 +++++-- test/components/form-draft/publish.spec.js | 9 ++-- test/components/form/new.spec.js | 9 ++-- 8 files changed, 112 insertions(+), 47 deletions(-) create mode 100644 src/components/alert/40917.vue diff --git a/src/alert.js b/src/alert.js index 152c577f6..41ac83844 100644 --- a/src/alert.js +++ b/src/alert.js @@ -12,40 +12,51 @@ except according to the terms contained in the LICENSE file. import { shallowReactive } from 'vue'; class AlertData { + #data; + constructor() { - this._data = shallowReactive({ + this.#data = shallowReactive({ // The alert's "contextual" type: 'success', 'info', 'warning', or // 'danger'. type: 'danger', - message: null, - // `true` if the alert should be visible and `false` if not. - state: false, - // The time at which the alert was last set + // Either a string or an array with a component and props for the + // component + content: null, + // The time at which the alert was last shown at: new Date() }); } - get type() { return this._data.type; } - get message() { return this._data.message; } - get state() { return this._data.state; } - get at() { return this._data.at; } + // Returns `true` if the alert should be visible and `false` if not. + get state() { return this.content != null; } + + get type() { return this.#data.type; } + + get content() { return this.#data.content; } - _set(type, message) { - this._data.type = type; - this._data.message = message; - this._data.state = true; - this._data.at = new Date(); + get message() { + const { content } = this.#data; + return typeof content === 'string' ? content : null; } - success(message) { this._set('success', message); } - info(message) { this._set('info', message); } - warning(message) { this._set('warning', message); } - danger(message) { this._set('danger', message); } + get at() { return this.#data.at; } - blank() { - this._data.state = false; - this._data.message = null; + // `content` can be a string, an array with a component and props for the + // component, or just a component. + #show(type, content) { + this.#data.type = type; + this.#data.content = typeof content === 'string' || Array.isArray(content) + ? content + : [content, {}]; + this.#data.at = new Date(); } + + success(content) { this.#show('success', content); } + info(content) { this.#show('info', content); } + warning(content) { this.#show('warning', content); } + danger(content) { this.#show('danger', content); } + + blank() { this.#data.content = null; } } // Only a single alert is shown at a time. This function returns an object that diff --git a/src/components/alert.vue b/src/components/alert.vue index eb01a2d46..f4c9532ed 100644 --- a/src/components/alert.vue +++ b/src/components/alert.vue @@ -16,7 +16,12 @@ except according to the terms contained in the LICENSE file. @click="alert.blank()"> - {{ alert.message }} + + + {{ alert.message }} + + diff --git a/src/components/alert/40917.vue b/src/components/alert/40917.vue new file mode 100644 index 000000000..039882883 --- /dev/null +++ b/src/components/alert/40917.vue @@ -0,0 +1,46 @@ + + + + + + +{ + // @transifexKey util.request.problem.409_17 + "en": { + "message": "This Form attempts to create a new Entity property that matches with an existing one except for capitalization: | This Form attempts to create new Entity properties that match with existing ones except for capitalization:", + // Error message format for the duplicate properties (different capitalization) in an Entity-list. + // {provided} is the property name in the uploaded Form. + // {current} is the existing property name in the Entity-list. + "duplicateProperty": "{provided} (existing: {current})" + } +} + diff --git a/src/locales/en.json5 b/src/locales/en.json5 index 30f3b439e..a0db5005d 100644 --- a/src/locales/en.json5 +++ b/src/locales/en.json5 @@ -485,14 +485,7 @@ "problem": { // A "resource" is a generic term for something in Central, for example, // a Project, a Web User, or a Form. - "404_1": "The resource you are looking for cannot be found. The resource may have been deleted.", - "409_17": { - "message": "This Form attempts to create a new Entity property that matches with an existing one except for capitalization: | This Form attempts to create new Entity properties that match with existing ones except for capitalization:", - // Error message format for the duplicate properties (different capitalization) in an Entity-list. - // {provided} is the property name in the uploaded Form. - // {current} is the existing property name in the Entity-list. - "duplicateProperty": "{provided} (existing: {current})" - } + "404_1": "The resource you are looking for cannot be found. The resource may have been deleted." } }, "session": { diff --git a/src/util/request.js b/src/util/request.js index e4ccefd4a..81cb0b934 100644 --- a/src/util/request.js +++ b/src/util/request.js @@ -9,6 +9,8 @@ https://www.apache.org/licenses/LICENSE-2.0. No part of ODK Central, including this file, may be copied, modified, propagated, or distributed except according to the terms contained in the LICENSE file. */ +import Alert40917 from '../components/alert/40917.vue'; + import { odataLiteral } from './odata'; // Returns `true` if `data` looks like a Backend Problem and `false` if not. @@ -245,12 +247,7 @@ export const requestAlertMessage = (i18n, axiosError, problemToAlert = undefined if (problem.code === 404.1) return i18n.t('util.request.problem.404_1'); if (problem.code === 409.17) { const { duplicateProperties } = problem.details; - // eslint-disable-next-line prefer-template - return i18n.tc('util.request.problem.409_17.message', duplicateProperties.length) + - '\n\n' + - duplicateProperties - .map(p => `• ${i18n.t('util.request.problem.409_17.duplicateProperty', p)}`) - .join('\n'); + return [Alert40917, { duplicateProperties }]; } return problem.message; }; diff --git a/test/assertions.js b/test/assertions.js index b01302029..78e40664e 100644 --- a/test/assertions.js +++ b/test/assertions.js @@ -316,7 +316,7 @@ addAsyncMethod('textTooltip', async function textTooltip() { //////////////////////////////////////////////////////////////////////////////// // OTHER -Assertion.addMethod('alert', function assertAlert(type = undefined, message = undefined) { +Assertion.addMethod('alert', function assertAlert(type = undefined, content = undefined) { expect(this._obj).to.be.instanceof(VueWrapper); const { alert } = this._obj.vm.$container; this.assert( @@ -324,7 +324,18 @@ Assertion.addMethod('alert', function assertAlert(type = undefined, message = un 'expected the component to show an alert', 'expected the component not to show an alert' ); - if (checkNegate(this, [type, message])) return; + if (checkNegate(this, [type, content])) return; if (type != null) alert.type.should.equal(type); - if (message != null) alert.message.should.stringMatch(message); + if (content != null) { + if (typeof content === 'string' || content instanceof RegExp || + typeof content === 'function') { + alert.message.should.stringMatch(content); + } else if (Array.isArray(content)) { + const [component, props = {}] = content; + alert.content[0].should.equal(component); + alert.content[1].should.eql(props); + } else { + alert.content.should.eql([content, {}]); + } + } }); diff --git a/test/components/form-draft/publish.spec.js b/test/components/form-draft/publish.spec.js index 20f624c6f..3a952d850 100644 --- a/test/components/form-draft/publish.spec.js +++ b/test/components/form-draft/publish.spec.js @@ -1,5 +1,6 @@ import { RouterLinkStub } from '@vue/test-utils'; +import Alert40917 from '../../../src/components/alert/40917.vue'; import FormDraftPublish from '../../../src/components/form-draft/publish.vue'; import FormVersionRow from '../../../src/components/form-version/row.vue'; @@ -297,10 +298,10 @@ describe('FormDraftPublish', () => { details: { duplicateProperties: [{ current: 'first_name', provided: 'FIRST_NAME' }] } }) .afterResponse(modal => { - modal.should.alert( - 'danger', - /This Form attempts to create a new Entity property that matches with an existing one except for capitalization:.*FIRST_NAME \(existing: first_name\)/s - ); + modal.should.alert('danger', [ + Alert40917, + { duplicateProperties: [{ current: 'first_name', provided: 'FIRST_NAME' }] } + ]); }); }); diff --git a/test/components/form/new.spec.js b/test/components/form/new.spec.js index 8a330d2fd..680a490ee 100644 --- a/test/components/form/new.spec.js +++ b/test/components/form/new.spec.js @@ -1,5 +1,6 @@ import { clone } from 'ramda'; +import Alert40917 from '../../../src/components/alert/40917.vue'; import ChecklistStep from '../../../src/components/checklist-step.vue'; import FileDropZone from '../../../src/components/file-drop-zone.vue'; import FormNew from '../../../src/components/form/new.vue'; @@ -501,10 +502,10 @@ describe('FormNew', () => { details: { duplicateProperties: [{ current: 'first_name', provided: 'FIRST_NAME' }] } }) .afterResponse(modal => { - modal.should.alert( - 'danger', - /This Form attempts to create a new Entity property that matches with an existing one except for capitalization:.*FIRST_NAME \(existing: first_name\)/s - ); + modal.should.alert('danger', [ + Alert40917, + { duplicateProperties: [{ current: 'first_name', provided: 'FIRST_NAME' }] } + ]); }); }); });