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 @@
+
+
+
+
{{ $tc('message', duplicateProperties.length) }}
+
+ -
+ {{ $t('duplicateProperty', p) }}
+
+
+
+
+
+
+
+
+{
+ // @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' }] }
+ ]);
});
});
});