@projectcaluma/ember-form 14.8.0 → 14.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,7 +16,11 @@
16
16
  as |modal|
17
17
  >
18
18
  <modal.body>
19
- <MarkdownToHtml @markdown={{@text}} @openLinksInNewWindow={{true}} />
19
+ <MarkdownToHtml
20
+ @markdown={{@text}}
21
+ @openLinksInNewWindow={{true}}
22
+ @extensions="DOMPurify"
23
+ />
20
24
  </modal.body>
21
25
  </UkModal>
22
26
  </div>
@@ -1,4 +1,5 @@
1
1
  <MarkdownToHtml
2
2
  @markdown={{@field.question.raw.staticContent}}
3
3
  @openLinksInNewWindow={{true}}
4
+ @extensions="DOMPurify"
4
5
  />
@@ -1,8 +1,12 @@
1
1
  import { action } from "@ember/object";
2
+ import { service } from "@ember/service";
2
3
  import Component from "@glimmer/component";
4
+ import { queryManager } from "ember-apollo-client";
3
5
  import { task } from "ember-concurrency";
4
6
  import { cached } from "tracked-toolbox";
5
7
 
8
+ import documentValidityQuery from "@projectcaluma/ember-form/gql/queries/document-validity.graphql";
9
+
6
10
  /**
7
11
  * Component to check the validity of a document
8
12
  *
@@ -20,6 +24,10 @@ import { cached } from "tracked-toolbox";
20
24
  * @yield {Function} validate
21
25
  */
22
26
  export default class DocumentValidity extends Component {
27
+ @queryManager apollo;
28
+
29
+ @service calumaStore;
30
+
23
31
  /**
24
32
  * The document to be validated
25
33
  *
@@ -43,39 +51,64 @@ export default class DocumentValidity extends Component {
43
51
  return this._validate.isRunning;
44
52
  }
45
53
 
46
- _validateField = task(async (field) => {
47
- await field.validate.linked().perform();
54
+ _validate = task({ restartable: true }, async () => {
55
+ try {
56
+ const saveTasks = this.args.document.fields
57
+ .flatMap((field) => [
58
+ ...[...(field._components ?? [])].map((c) => c.save.last),
59
+ field.save?.last,
60
+ ])
61
+ .filter(Boolean);
48
62
 
49
- if (field.question.hasFormatValidators) {
50
- await field.save.linked().perform();
51
- }
52
- });
63
+ // Wait until all currently running save tasks in the UI and in the field
64
+ // itself are finished
65
+ await Promise.all(saveTasks);
53
66
 
54
- _validate = task({ restartable: true }, async () => {
55
- const saveTasks = this.args.document.fields
56
- .flatMap((field) => [
57
- ...[...(field._components ?? [])].map((c) => c.save.last),
58
- field.save?.last,
59
- ])
60
- .filter(Boolean);
61
-
62
- // Wait until all currently running save tasks in the UI and in the field
63
- // itself are finished
64
- await Promise.all(saveTasks);
65
-
66
- await Promise.all(
67
- this.args.document.fields.map((field) =>
68
- this._validateField.perform(field),
69
- ),
70
- );
71
-
72
- if (this.isValid) {
73
- this.args.onValid?.();
74
- } else {
75
- this.args.onInvalid?.();
76
- }
67
+ await Promise.all(
68
+ this.args.document.fields.map((field) =>
69
+ field.validate.linked().perform(),
70
+ ),
71
+ );
72
+
73
+ const { isValid, errors } = await this.apollo.query(
74
+ {
75
+ query: documentValidityQuery,
76
+ fetchPolicy: "network-only",
77
+ variables: { id: this.args.document.uuid },
78
+ },
79
+ "documentValidity.edges.0.node",
80
+ );
81
+
82
+ if (!isValid) {
83
+ errors
84
+ .filter(({ errorCode }) => errorCode === "format_validation_failed")
85
+ .forEach(({ slug, errorMsg, documentId }) => {
86
+ const pk = `Document:${documentId}:Question:${slug}`;
87
+ const field = this.calumaStore.findByPk(pk);
88
+ const parentField = field.document.parentField;
89
+
90
+ // Add the error manually as the frontend does not validate format
91
+ // validators - only the backend.
92
+ field.addManualError("format", { errorMsg }, field.value);
77
93
 
78
- return this.isValid;
94
+ if (parentField) {
95
+ // If the affected field is in a table row, we need to mark the
96
+ // table answer as invalid as well.
97
+ parentField.addManualError("table", {}, null);
98
+ }
99
+ });
100
+ }
101
+
102
+ if (this.isValid) {
103
+ this.args.onValid?.();
104
+ } else {
105
+ this.args.onInvalid?.();
106
+ }
107
+
108
+ return this.isValid;
109
+ } catch {
110
+ return false;
111
+ }
79
112
  });
80
113
 
81
114
  @action
@@ -0,0 +1,16 @@
1
+ query DocumentValidity($id: ID!, $dataSourceContext: JSONString) {
2
+ documentValidity(id: $id, dataSourceContext: $dataSourceContext) {
3
+ edges {
4
+ node {
5
+ id
6
+ isValid
7
+ errors {
8
+ slug
9
+ errorMsg
10
+ errorCode
11
+ documentId
12
+ }
13
+ }
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,20 @@
1
+ import DOMPurify from "dompurify";
2
+ import showdown from "showdown";
3
+
4
+ export function initialize() {
5
+ showdown.extension("DOMPurify", function () {
6
+ return [
7
+ {
8
+ type: "output",
9
+ filter(dirty) {
10
+ return DOMPurify.sanitize(dirty, { USE_PROFILES: { html: true } });
11
+ },
12
+ },
13
+ ];
14
+ });
15
+ }
16
+
17
+ export default {
18
+ name: "register-showdown-extensions",
19
+ initialize,
20
+ };
@@ -136,6 +136,7 @@ export default class Answer extends Base {
136
136
  new Document({
137
137
  raw: parseDocument(document),
138
138
  parentDocument: this.field.document,
139
+ parentField: this.field,
139
140
  owner,
140
141
  })
141
142
  );
@@ -22,7 +22,13 @@ const sum = (nums) => nums.reduce((num, base) => base + num, 0);
22
22
  * @class Document
23
23
  */
24
24
  export default class Document extends Base {
25
- constructor({ raw, parentDocument, dataSourceContext, ...args }) {
25
+ constructor({
26
+ raw,
27
+ parentDocument,
28
+ parentField,
29
+ dataSourceContext,
30
+ ...args
31
+ }) {
26
32
  assert(
27
33
  "A graphql document `raw` must be passed",
28
34
  raw?.__typename === "Document",
@@ -31,6 +37,7 @@ export default class Document extends Base {
31
37
  super({ raw, ...args });
32
38
 
33
39
  this.parentDocument = parentDocument;
40
+ this.parentField = parentField;
34
41
  this.dataSourceContext =
35
42
  dataSourceContext ?? parentDocument?.dataSourceContext;
36
43
 
@@ -75,6 +82,14 @@ export default class Document extends Base {
75
82
  */
76
83
  parentDocument = null;
77
84
 
85
+ /**
86
+ * The parent field of this document. If this is set, the document is most
87
+ * likely a table row.
88
+ *
89
+ * @property {Field} parentField
90
+ */
91
+ parentField = null;
92
+
78
93
  /**
79
94
  * The root form of this document
80
95
  *
@@ -665,14 +665,11 @@ export default class Field extends Base {
665
665
  await this.onSaveError(e);
666
666
 
667
667
  if (validationError) {
668
- this._errors = [
669
- ...this._errors,
670
- {
671
- type: "format",
672
- context: { errorMsg: validationError.message },
673
- value,
674
- },
675
- ];
668
+ this.addManualError(
669
+ "format",
670
+ { errorMsg: validationError.message },
671
+ value,
672
+ );
676
673
  } else {
677
674
  throw e;
678
675
  }
@@ -704,6 +701,18 @@ export default class Field extends Base {
704
701
  // eslint-disable-next-line no-unused-vars
705
702
  async onSaveError(error) {}
706
703
 
704
+ /**
705
+ * Add manual validation error message
706
+ *
707
+ * @method addManualError
708
+ * @param {String} type The error type (e.g. "table" or "format")
709
+ * @param {Object} context The error context object - mostly consists of `errorMsg`
710
+ * @param {*} value The invalid value
711
+ */
712
+ addManualError(type, context = null, value = null) {
713
+ this._errors = [...this._errors, { type, context, value }];
714
+ }
715
+
707
716
  /**
708
717
  * The translated error messages
709
718
  *
@@ -37,6 +37,10 @@ export default class CalumaStoreService extends Service {
37
37
  return this._store.get(storeKey) || null;
38
38
  }
39
39
 
40
+ findByPk(pk) {
41
+ return this._store.values().find((item) => item.pk === pk);
42
+ }
43
+
40
44
  delete(storeKey) {
41
45
  this._store.delete(storeKey);
42
46
  }
@@ -0,0 +1,4 @@
1
+ export {
2
+ default,
3
+ initialize,
4
+ } from "@projectcaluma/ember-form/initializers/register-showdown-extensions";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projectcaluma/ember-form",
3
- "version": "14.8.0",
3
+ "version": "14.8.2",
4
4
  "description": "Ember addon for rendering Caluma forms.",
5
5
  "keywords": [
6
6
  "ember-addon"
@@ -14,6 +14,7 @@
14
14
  "@ember/test-waiters": "^4.1.1",
15
15
  "@embroider/macros": "^1.16.10",
16
16
  "@embroider/util": "^1.13.2",
17
+ "dompurify": "^3.3.1",
17
18
  "ember-apollo-client": "^5.0.0",
18
19
  "ember-auto-import": "^2.10.0",
19
20
  "ember-autoresize-modifier": "^0.7.0 || ^0.8.0",
@@ -39,7 +40,7 @@
39
40
  "luxon": "^3.5.0",
40
41
  "reactiveweb": "^1.3.0",
41
42
  "tracked-toolbox": "^2.0.0",
42
- "@projectcaluma/ember-core": "^14.8.0"
43
+ "@projectcaluma/ember-core": "^14.8.2"
43
44
  },
44
45
  "devDependencies": {
45
46
  "@ember/optional-features": "2.3.0",
@@ -74,12 +75,12 @@
74
75
  "uikit": "3.25.6",
75
76
  "uuid": "13.0.0",
76
77
  "webpack": "5.104.1",
77
- "@projectcaluma/ember-workflow": "14.8.0",
78
- "@projectcaluma/ember-testing": "14.8.0"
78
+ "@projectcaluma/ember-testing": "14.8.2",
79
+ "@projectcaluma/ember-workflow": "14.8.2"
79
80
  },
80
81
  "peerDependencies": {
81
82
  "ember-source": ">= 4.0.0",
82
- "@projectcaluma/ember-workflow": "^14.8.0"
83
+ "@projectcaluma/ember-workflow": "^14.8.2"
83
84
  },
84
85
  "dependenciesMeta": {
85
86
  "@projectcaluma/ember-core": {