@projectcaluma/ember-form 10.0.1 → 11.0.0-beta.10

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +1181 -0
  2. package/addon/components/cf-content.hbs +36 -41
  3. package/addon/components/cf-content.js +48 -29
  4. package/addon/components/cf-field/info.hbs +2 -2
  5. package/addon/components/cf-field/info.js +0 -15
  6. package/addon/components/cf-field/input/action-button.hbs +18 -19
  7. package/addon/components/cf-field/input/action-button.js +9 -7
  8. package/addon/components/cf-field/input/checkbox.hbs +6 -2
  9. package/addon/components/cf-field/input/checkbox.js +9 -29
  10. package/addon/components/cf-field/input/date.hbs +8 -5
  11. package/addon/components/cf-field/input/date.js +28 -10
  12. package/addon/components/cf-field/input/file.hbs +2 -2
  13. package/addon/components/cf-field/input/file.js +10 -11
  14. package/addon/components/cf-field/input/float.hbs +4 -4
  15. package/addon/components/cf-field/input/integer.hbs +5 -5
  16. package/addon/components/cf-field/input/radio.hbs +4 -1
  17. package/addon/components/cf-field/input/static.hbs +1 -1
  18. package/addon/components/cf-field/input/table.hbs +24 -24
  19. package/addon/components/cf-field/input/table.js +12 -10
  20. package/addon/components/cf-field/input/text.hbs +5 -5
  21. package/addon/components/cf-field/input/textarea.hbs +6 -5
  22. package/addon/components/cf-field/input.hbs +10 -1
  23. package/addon/components/cf-field/input.js +1 -1
  24. package/addon/components/cf-field/label.hbs +1 -1
  25. package/addon/components/cf-field-value.hbs +22 -7
  26. package/addon/components/cf-field-value.js +8 -38
  27. package/addon/components/cf-field.hbs +14 -6
  28. package/addon/components/cf-field.js +22 -8
  29. package/addon/components/cf-navigation-item.hbs +2 -2
  30. package/addon/components/cf-navigation.hbs +4 -1
  31. package/addon/components/document-validity.js +17 -2
  32. package/addon/gql/fragments/field.graphql +45 -0
  33. package/addon/gql/mutations/save-document-table-answer.graphql +1 -1
  34. package/addon/gql/mutations/save-document.graphql +1 -0
  35. package/addon/gql/queries/{get-document-answers.graphql → document-answers.graphql} +2 -1
  36. package/addon/gql/queries/{get-document-forms.graphql → document-forms.graphql} +2 -1
  37. package/addon/gql/queries/{get-document-used-dynamic-options.graphql → document-used-dynamic-options.graphql} +2 -1
  38. package/addon/gql/queries/{get-dynamic-options.graphql → dynamic-options.graphql} +2 -1
  39. package/addon/gql/queries/{get-fileanswer-info.graphql → fileanswer-info.graphql} +2 -1
  40. package/addon/helpers/get-widget.js +50 -0
  41. package/addon/lib/answer.js +108 -72
  42. package/addon/lib/base.js +32 -23
  43. package/addon/lib/dependencies.js +36 -71
  44. package/addon/lib/document.js +92 -96
  45. package/addon/lib/field.js +374 -407
  46. package/addon/lib/fieldset.js +46 -47
  47. package/addon/lib/form.js +27 -15
  48. package/addon/lib/navigation.js +211 -192
  49. package/addon/lib/question.js +103 -94
  50. package/addon/services/caluma-store.js +10 -6
  51. package/app/helpers/get-widget.js +4 -0
  52. package/blueprints/@projectcaluma/ember-form/index.js +1 -0
  53. package/package.json +30 -25
  54. package/addon/components/cf-navigation.js +0 -9
  55. package/addon/instance-initializers/setup-pikaday-i18n.js +0 -35
@@ -1,15 +1,15 @@
1
1
  import { getOwner } from "@ember/application";
2
2
  import { assert } from "@ember/debug";
3
- import { computed, defineProperty } from "@ember/object";
4
- import { equal, not, reads } from "@ember/object/computed";
3
+ import { associateDestroyableChild } from "@ember/destroyable";
5
4
  import { inject as service } from "@ember/service";
6
5
  import { camelize } from "@ember/string";
6
+ import { isEmpty } from "@ember/utils";
7
+ import { tracked } from "@glimmer/tracking";
7
8
  import { queryManager } from "ember-apollo-client";
8
- import { task } from "ember-concurrency";
9
+ import { restartableTask, lastValue, dropTask } from "ember-concurrency";
9
10
  import { validate } from "ember-validators";
10
- import cloneDeep from "lodash.clonedeep";
11
11
  import isEqual from "lodash.isequal";
12
- import { all, resolve } from "rsvp";
12
+ import { cached } from "tracked-toolbox";
13
13
 
14
14
  import { decodeId } from "@projectcaluma/ember-core/helpers/decode-id";
15
15
  import saveDocumentDateAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-date-answer.graphql";
@@ -19,12 +19,9 @@ import saveDocumentIntegerAnswerMutation from "@projectcaluma/ember-form/gql/mut
19
19
  import saveDocumentListAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-list-answer.graphql";
20
20
  import saveDocumentStringAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-string-answer.graphql";
21
21
  import saveDocumentTableAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-table-answer.graphql";
22
- import getDocumentUsedDynamicOptionsQuery from "@projectcaluma/ember-form/gql/queries/get-document-used-dynamic-options.graphql";
22
+ import getDocumentUsedDynamicOptionsQuery from "@projectcaluma/ember-form/gql/queries/document-used-dynamic-options.graphql";
23
23
  import Base from "@projectcaluma/ember-form/lib/base";
24
- import {
25
- nestedDependencyParents,
26
- dependencies,
27
- } from "@projectcaluma/ember-form/lib/dependencies";
24
+ import dependencies from "@projectcaluma/ember-form/lib/dependencies";
28
25
 
29
26
  export const TYPE_MAP = {
30
27
  TextQuestion: "StringAnswer",
@@ -42,10 +39,20 @@ export const TYPE_MAP = {
42
39
  DateQuestion: "DateAnswer",
43
40
  };
44
41
 
45
- const fieldIsHidden = (field) => {
42
+ const MUTATION_MAP = {
43
+ FloatAnswer: saveDocumentFloatAnswerMutation,
44
+ IntegerAnswer: saveDocumentIntegerAnswerMutation,
45
+ StringAnswer: saveDocumentStringAnswerMutation,
46
+ ListAnswer: saveDocumentListAnswerMutation,
47
+ FileAnswer: saveDocumentFileAnswerMutation,
48
+ DateAnswer: saveDocumentDateAnswerMutation,
49
+ TableAnswer: saveDocumentTableAnswerMutation,
50
+ };
51
+
52
+ const fieldIsHiddenOrEmpty = (field) => {
46
53
  return (
47
54
  field.hidden ||
48
- (field.question.__typename !== "TableQuestion" &&
55
+ (!field.question.isTable &&
49
56
  (field.answer.value === null || field.answer.value === undefined))
50
57
  );
51
58
  };
@@ -55,61 +62,44 @@ const fieldIsHidden = (field) => {
55
62
  *
56
63
  * @class Field
57
64
  */
58
- export default Base.extend({
59
- saveDocumentFloatAnswerMutation,
60
- saveDocumentIntegerAnswerMutation,
61
- saveDocumentStringAnswerMutation,
62
- saveDocumentListAnswerMutation,
63
- saveDocumentFileAnswerMutation,
64
- saveDocumentDateAnswerMutation,
65
- saveDocumentTableAnswerMutation,
66
-
67
- intl: service(),
68
- calumaStore: service(),
69
- validator: service(),
70
-
71
- apollo: queryManager(),
72
-
73
- init(...args) {
74
- assert("A document must be passed", this.document);
75
-
76
- defineProperty(this, "pk", {
77
- writable: false,
78
- value: `${this.document.pk}:Question:${this.raw.question.slug}`,
79
- });
65
+ export default class Field extends Base {
66
+ @service intl;
80
67
 
81
- this._super(...args);
68
+ @queryManager apollo;
82
69
 
83
- this._createQuestion();
84
- this._createAnswer();
70
+ constructor({ fieldset, ...args }) {
71
+ assert("`fieldset` must be passed as an argument", fieldset);
85
72
 
86
- this.set("_errors", []);
87
- },
73
+ super({ fieldset, ...args });
88
74
 
89
- willDestroy(...args) {
90
- this._super(...args);
75
+ this.fieldset = fieldset;
91
76
 
92
- if (this.answer) {
93
- this.answer.destroy();
94
- }
95
- },
77
+ this.pushIntoStore();
78
+
79
+ this._createQuestion();
80
+ this._createAnswer();
81
+ }
96
82
 
97
83
  _createQuestion() {
84
+ const owner = getOwner(this);
85
+
98
86
  const question =
99
87
  this.calumaStore.find(`Question:${this.raw.question.slug}`) ||
100
- getOwner(this)
101
- .factoryFor("caluma-model:question")
102
- .create({ raw: this.raw.question });
88
+ new (owner.factoryFor("caluma-model:question").class)({
89
+ raw: this.raw.question,
90
+ owner,
91
+ });
103
92
 
104
93
  if (question.isDynamic) {
105
94
  question.loadDynamicOptions.perform();
106
95
  }
107
96
 
108
- this.set("question", question);
109
- },
97
+ this.question = question;
98
+ }
110
99
 
111
100
  _createAnswer() {
112
- const AnswerFactory = getOwner(this).factoryFor("caluma-model:answer");
101
+ const owner = getOwner(this);
102
+ const Answer = owner.factoryFor("caluma-model:answer").class;
113
103
  let answer;
114
104
 
115
105
  // no answer passed, create an empty one
@@ -121,182 +111,188 @@ export default Base.extend({
121
111
  return;
122
112
  }
123
113
 
124
- answer = AnswerFactory.create({
114
+ answer = new Answer({
125
115
  raw: {
116
+ id: null,
126
117
  __typename: answerType,
127
118
  question: { slug: this.raw.question.slug },
128
119
  [camelize(answerType.replace(/Answer$/, "Value"))]: null,
129
120
  },
130
121
  field: this,
122
+ owner,
131
123
  });
132
124
  } else {
133
125
  answer =
134
126
  this.calumaStore.find(`Answer:${decodeId(this.raw.answer.id)}`) ||
135
- AnswerFactory.create({ raw: this.raw.answer, field: this });
127
+ new Answer({ raw: this.raw.answer, field: this, owner });
136
128
  }
137
129
 
138
- this.set("answer", answer);
139
- },
130
+ this.answer = associateDestroyableChild(this, answer);
131
+ }
140
132
 
141
133
  /**
142
134
  * The question to this field
143
135
  *
144
136
  * @property {Question} question
145
- * @accessor
146
137
  */
147
- question: null,
138
+ question = null;
148
139
 
149
140
  /**
150
141
  * The answer to this field. It is possible for this to be `null` if the
151
142
  * question is of the static question type.
152
143
  *
153
144
  * @property {Answer} answer
154
- * @accessor
155
145
  */
156
- answer: null,
146
+ answer = null;
147
+
148
+ /**
149
+ * The raw error objects which are later translated to readable messages.
150
+ *
151
+ * @property {Object[]} _errors
152
+ * @private
153
+ */
154
+ @tracked _errors = [];
155
+
156
+ /**
157
+ * Currently rendered field components that use this field. This is used in
158
+ * the document validity component to await all current save tasks before
159
+ * validating.
160
+ *
161
+ * @property {Set} _components
162
+ * @private
163
+ */
164
+ _components = new Set();
165
+
166
+ /**
167
+ * The primary key of the field. Consists of the document and question primary
168
+ * keys.
169
+ *
170
+ * @property {String} pk
171
+ */
172
+ @cached
173
+ get pk() {
174
+ return `${this.document.pk}:Question:${this.raw.question.slug}`;
175
+ }
157
176
 
158
177
  /**
159
178
  * Whether the field is valid.
160
179
  *
161
180
  * @property {Boolean} isValid
162
- * @accessor
163
181
  */
164
- isValid: equal("errors.length", 0),
182
+ get isValid() {
183
+ return this.errors.length === 0;
184
+ }
165
185
 
166
186
  /**
167
187
  * Whether the field is invalid.
168
188
  *
169
189
  * @property {Boolean} isInvalid
170
- * @accessor
171
190
  */
172
- isInvalid: not("isValid"),
191
+ get isInvalid() {
192
+ return !this.isValid;
193
+ }
173
194
 
174
195
  /**
175
196
  * Whether the field is new (never saved to the backend service or empty)
176
197
  *
177
198
  * @property {Boolean} isNew
178
- * @accessor
179
199
  */
180
- isNew: computed("answer.isNew", function () {
200
+ get isNew() {
181
201
  return !this.answer || this.answer.isNew;
182
- }),
202
+ }
183
203
 
184
204
  /**
185
205
  * Only table values, this is used for certain computed property keys.
186
206
  *
187
207
  * @property {Document[]} tableValue
188
- * @accessor
189
208
  */
190
- tableValue: computed("value", "question.isTable", function () {
209
+ get tableValue() {
191
210
  return this.question.isTable ? this.value : [];
192
- }),
211
+ }
193
212
 
194
213
  /**
195
214
  * Whether the field has the defined default answer of the question as value.
196
215
  *
197
216
  * @property {Boolean} isDefault
198
- * @accessor
199
217
  */
200
- isDefault: computed(
201
- "value",
202
- "tableValue.@each.flatAnswerMap",
203
- "question.{isTable,defaultValue}",
204
- function () {
205
- if (!this.value || !this.question.defaultValue) return false;
218
+ get isDefault() {
219
+ if (!this.value || !this.question.defaultValue) return false;
206
220
 
207
- const value = this.question.isTable
208
- ? this.tableValue.map((doc) => doc.flatAnswerMap)
209
- : this.value;
221
+ const value = this.question.isTable
222
+ ? this.tableValue.map((doc) => doc.flatAnswerMap)
223
+ : this.value;
210
224
 
211
- return isEqual(value, this.question.defaultValue);
212
- }
213
- ),
225
+ return isEqual(value, this.question.defaultValue);
226
+ }
214
227
 
215
228
  /**
216
229
  * Whether the field is dirty. This will be true, if there is a value on the
217
230
  * answer which differs from the default value of the question.
218
231
  *
219
232
  * @property {Boolean} isDirty
220
- * @accessor
221
- */
222
- isDirty: computed(
223
- "isDefault",
224
- "isNew",
225
- "question.isCalculated",
226
- "validate.lastSuccessful",
227
- function () {
228
- if (this.question.isCalculated || this.isDefault) {
229
- return false;
230
- }
231
-
232
- return Boolean(this.validate.lastSuccessful) || !this.isNew;
233
+ */
234
+ get isDirty() {
235
+ if (this.question.isCalculated || this.isDefault) {
236
+ return false;
233
237
  }
234
- ),
238
+
239
+ return Boolean(this.validate.lastSuccessful) || !this.isNew;
240
+ }
235
241
 
236
242
  /**
237
243
  * The type of the question
238
244
  *
239
245
  * @property {String} questionType
240
- * @accessor
241
246
  */
242
- questionType: reads("question.__typename"),
247
+ get questionType() {
248
+ return this.question.raw.__typename;
249
+ }
243
250
 
244
251
  /**
245
252
  * The document this field belongs to
246
253
  *
247
254
  * @property {Document} document
248
- * @accessor
249
255
  */
250
- document: reads("fieldset.document"),
256
+ get document() {
257
+ return this.fieldset.document;
258
+ }
251
259
 
252
260
  /**
253
261
  * The value of the field
254
262
  *
255
263
  * @property {*} value
256
- * @accessor
257
- */
258
- value: computed(
259
- "question.isCalculated",
260
- "calculatedValue",
261
- "answer.value",
262
- function () {
263
- if (this.question.isCalculated) {
264
- return this.calculatedValue;
265
- }
266
-
267
- return this.answer?.value;
264
+ */
265
+ get value() {
266
+ if (this.question.isCalculated) {
267
+ return this.calculatedValue;
268
268
  }
269
- ),
269
+
270
+ return this.answer?.value;
271
+ }
270
272
 
271
273
  /**
272
- * The computed value of a caluclated question
274
+ * The computed value of a calculated float question
273
275
  *
274
276
  * @property {*} calculatedValue
275
- * @accessor
276
- */
277
- calculatedValue: computed(
278
- "document.jexl",
279
- "calculatedDependencies.@each.{hidden,value}",
280
- "jexlContext",
281
- "question.{isCalculated,calcExpression}",
282
- function () {
283
- if (
284
- !this.question.isCalculated ||
285
- !this.calculatedDependencies.every((field) => !field.hidden)
286
- ) {
287
- return null;
288
- }
277
+ */
278
+ @cached
279
+ get calculatedValue() {
280
+ if (
281
+ !this.question.isCalculated ||
282
+ !this.calculatedDependencies.every((field) => !field.hidden)
283
+ ) {
284
+ return null;
285
+ }
289
286
 
290
- try {
291
- return this.document.jexl.evalSync(
292
- this.question.calcExpression,
293
- this.jexlContext
294
- );
295
- } catch (error) {
296
- return null;
297
- }
287
+ try {
288
+ return this.document.jexl.evalSync(
289
+ this.question.raw.calcExpression,
290
+ this.jexlContext
291
+ );
292
+ } catch (error) {
293
+ return null;
298
294
  }
299
- ),
295
+ }
300
296
 
301
297
  /**
302
298
  * Fetch all formerly used dynamic options for this question. This will be
@@ -306,7 +302,8 @@ export default Base.extend({
306
302
  * @return {Object[]} Formerly used dynamic options
307
303
  * @private
308
304
  */
309
- _fetchUsedDynamicOptions: task(function* () {
305
+ @dropTask
306
+ *_fetchUsedDynamicOptions() {
310
307
  if (!this.question.isDynamic) return null;
311
308
 
312
309
  const edges = yield this.apollo.query(
@@ -325,16 +322,15 @@ export default Base.extend({
325
322
  slug,
326
323
  label,
327
324
  }));
328
- }),
325
+ }
329
326
 
330
327
  /**
331
328
  * The formerly used dynamic options for this question.
332
329
  *
333
330
  * @property {Object[]} usedDynamicOptions
334
- * @accessor
335
- *
336
331
  */
337
- usedDynamicOptions: reads("_fetchUsedDynamicOptions.lastSuccessful.value"),
332
+ @lastValue("_fetchUsedDynamicOptions")
333
+ usedDynamicOptions;
338
334
 
339
335
  /**
340
336
  * The available options for choice questions. This only works for the
@@ -348,79 +344,67 @@ export default Base.extend({
348
344
  * This will also return the disabled state of the option. An option can only
349
345
  * be disabled, if it is an old value used in a dynamic question.
350
346
  *
351
- * @property {Null|Object[]} options
352
- * @accessor
353
- */
354
- options: computed(
355
- "_fetchUsedDynamicOptions.lastSuccessful",
356
- "question.{options.[],isChoice,isDynamic,isMultipleChoice}",
357
- "usedDynamicOptions.[]",
358
- "value",
359
- function () {
360
- if (!this.question.isChoice && !this.question.isMultipleChoice) {
361
- return null;
362
- }
363
-
364
- const selected =
365
- (this.question.isMultipleChoice ? this.value : [this.value]) || [];
347
+ * @property {null|Object[]} options
348
+ */
349
+ @cached
350
+ get options() {
351
+ if (!this.question.isChoice && !this.question.isMultipleChoice) {
352
+ return null;
353
+ }
366
354
 
367
- const options = this.question.options.filter(
368
- (option) => !option.disabled || selected.includes(option.slug)
369
- );
355
+ const selected =
356
+ (this.question.isMultipleChoice ? this.value : [this.value]) || [];
370
357
 
371
- const hasUnknownValue = !selected.every((slug) =>
372
- options.find((option) => option.slug === slug)
373
- );
358
+ const options = this.question.options.filter(
359
+ (option) => !option.disabled || selected.includes(option.slug)
360
+ );
374
361
 
375
- if (this.question.isDynamic && hasUnknownValue) {
376
- if (!this._fetchUsedDynamicOptions.lastSuccessful) {
377
- // Fetch used dynamic options if not done yet
378
- this._fetchUsedDynamicOptions.perform();
379
- }
362
+ const hasUnknownValue = !selected.every((slug) =>
363
+ options.find((option) => option.slug === slug)
364
+ );
380
365
 
381
- return [
382
- ...options,
383
- ...(this.usedDynamicOptions || [])
384
- .filter((used) => {
385
- return (
386
- selected.includes(used.slug) &&
387
- !options.find((option) => option.slug === used.slug)
388
- );
389
- })
390
- .map((used) => ({ ...used, disabled: true })),
391
- ];
366
+ if (this.question.isDynamic && hasUnknownValue) {
367
+ if (!this._fetchUsedDynamicOptions.lastSuccessful) {
368
+ // Fetch used dynamic options if not done yet
369
+ this._fetchUsedDynamicOptions.perform();
392
370
  }
393
371
 
394
- return options;
372
+ return [
373
+ ...options,
374
+ ...(this.usedDynamicOptions || [])
375
+ .filter((used) => {
376
+ return (
377
+ selected.includes(used.slug) &&
378
+ !options.find((option) => option.slug === used.slug)
379
+ );
380
+ })
381
+ .map((used) => ({ ...used, disabled: true })),
382
+ ];
395
383
  }
396
- ),
384
+
385
+ return options;
386
+ }
397
387
 
398
388
  /**
399
389
  * The currently selected option. This property is only used for choice
400
390
  * questions. It can either return null if no value is selected yet, an
401
391
  * object for single choices or an array of objects for multiple choices.
402
392
  *
403
- * @property {Null|Object|Object[]} selected
404
- * @accessor
393
+ * @property {null|Object|Object[]} selected
405
394
  */
406
- selected: computed(
407
- "options.@each.slug",
408
- "question.{isChoice,isMultipleChoice}",
409
- "value",
410
- function () {
411
- if (!this.question.isChoice && !this.question.isMultipleChoice) {
412
- return null;
413
- }
395
+ get selected() {
396
+ if (!this.question.isChoice && !this.question.isMultipleChoice) {
397
+ return null;
398
+ }
414
399
 
415
- const selected = this.options.filter(({ slug }) =>
416
- this.question.isMultipleChoice
417
- ? (this.value || []).includes(slug)
418
- : this.value === slug
419
- );
400
+ const selected = this.options.filter(({ slug }) =>
401
+ this.question.isMultipleChoice
402
+ ? (this.value || []).includes(slug)
403
+ : this.value === slug
404
+ );
420
405
 
421
- return this.question.isMultipleChoice ? selected : selected[0];
422
- }
423
- ),
406
+ return this.question.isMultipleChoice ? selected : selected[0];
407
+ }
424
408
 
425
409
  /**
426
410
  * The field's JEXL context.
@@ -435,33 +419,25 @@ export default Base.extend({
435
419
  * - `info.root.formMeta`: The new property for the root form meta.
436
420
  *
437
421
  * @property {Object} jexlContext
438
- * @accessor
439
- */
440
- jexlContext: computed(
441
- "document.jexlContext",
442
- "fieldset.{form.slug,form.meta,field.fieldset.form.slug,field.fieldset.form.meta}",
443
- function () {
444
- const context = cloneDeep(this.document.jexlContext);
445
-
446
- const form = this.get("fieldset.form");
447
- const parent = this.get("fieldset.field.fieldset.form");
448
-
449
- return {
450
- ...context,
451
- info: {
452
- ...context.info,
453
- form: form.slug,
454
- formMeta: form.meta,
455
- parent: parent
456
- ? {
457
- form: parent.slug,
458
- formMeta: parent.meta,
459
- }
460
- : null,
461
- },
462
- };
463
- }
464
- ),
422
+ */
423
+ get jexlContext() {
424
+ const parent = this.fieldset.field?.fieldset.form;
425
+
426
+ return {
427
+ ...this.document.jexlContext,
428
+ info: {
429
+ ...this.document.jexlContext.info,
430
+ form: this.fieldset.form.slug,
431
+ formMeta: this.fieldset.form.raw.meta,
432
+ parent: parent
433
+ ? {
434
+ form: parent.slug,
435
+ formMeta: parent.raw.meta,
436
+ }
437
+ : null,
438
+ },
439
+ };
440
+ }
465
441
 
466
442
  /**
467
443
  * Fields that are referenced in the `calcExpression` JEXL expression
@@ -470,14 +446,8 @@ export default Base.extend({
470
446
  * expression needs to be re-evaluated.
471
447
  *
472
448
  * @property {Field[]} calculatedDependencies
473
- * @accessor
474
449
  */
475
- _calculatedNestedDependencyParents: nestedDependencyParents(
476
- "question.calcExpression"
477
- ),
478
- calculatedDependencies: dependencies("question.calcExpression", {
479
- nestedParentsPath: "_calculatedNestedDependencyParents",
480
- }),
450
+ @dependencies("question.raw.calcExpression") calculatedDependencies;
481
451
 
482
452
  /**
483
453
  * Fields that are referenced in the `isHidden` JEXL expression
@@ -486,12 +456,8 @@ export default Base.extend({
486
456
  * expression needs to be re-evaluated.
487
457
  *
488
458
  * @property {Field[]} hiddenDependencies
489
- * @accessor
490
459
  */
491
- _hiddenNestedDependencyParents: nestedDependencyParents("question.isHidden"),
492
- hiddenDependencies: dependencies("question.isHidden", {
493
- nestedParentsPath: "_hiddenNestedDependencyParents",
494
- }),
460
+ @dependencies("question.raw.isHidden") hiddenDependencies;
495
461
 
496
462
  /**
497
463
  * Fields that are referenced in the `isRequired` JEXL expression
@@ -500,14 +466,8 @@ export default Base.extend({
500
466
  * expression needs to be re-evaluated.
501
467
  *
502
468
  * @property {Field[]} optionalDependencies
503
- * @accessor
504
469
  */
505
- _optionalNestedDependencyParents: nestedDependencyParents(
506
- "question.isRequired"
507
- ),
508
- optionalDependencies: dependencies("question.isRequired", {
509
- nestedParentsPath: "_optionalNestedDependencyParents",
510
- }),
470
+ @dependencies("question.raw.isRequired") optionalDependencies;
511
471
 
512
472
  /**
513
473
  * The field's hidden state
@@ -515,99 +475,85 @@ export default Base.extend({
515
475
  * A question is hidden if:
516
476
  * - The form question field of the fieldset is hidden
517
477
  * - All depending field (used in the expression) are hidden
518
- * - The evaluated `question.isHidden` expression returns `true`
478
+ * - The evaluated `question.raw.isHidden` expression returns `true`
519
479
  *
520
480
  * @property {Boolean} hidden
521
481
  */
522
- hidden: computed(
523
- "document.jexl",
524
- "fieldset.field.hidden",
525
- "hiddenDependencies.@each.{hidden,value}",
526
- "jexlContext",
527
- "question.isHidden",
528
- "pk",
529
- function () {
530
- if (
531
- this.fieldset.field?.hidden ||
532
- (this.hiddenDependencies.length &&
533
- this.hiddenDependencies.every(fieldIsHidden))
534
- ) {
535
- return true;
536
- }
482
+ @cached
483
+ get hidden() {
484
+ if (
485
+ this.fieldset.field?.hidden ||
486
+ (this.hiddenDependencies.length &&
487
+ this.hiddenDependencies.every(fieldIsHiddenOrEmpty))
488
+ ) {
489
+ return true;
490
+ }
537
491
 
538
- try {
539
- return this.document.jexl.evalSync(
540
- this.question.isHidden,
541
- this.jexlContext
542
- );
543
- } catch (error) {
544
- throw new Error(
545
- `Error while evaluating \`isHidden\` expression on field \`${this.pk}\`: ${error.message}`
546
- );
547
- }
492
+ try {
493
+ return this.document.jexl.evalSync(
494
+ this.question.raw.isHidden,
495
+ this.jexlContext
496
+ );
497
+ } catch (error) {
498
+ throw new Error(
499
+ `Error while evaluating \`isHidden\` expression on field \`${this.pk}\`: ${error.message}`
500
+ );
548
501
  }
549
- ),
502
+ }
550
503
 
551
504
  /**
552
505
  * The field's optional state
553
506
  *
554
507
  * The field is optional if:
508
+ * - The question is of the type form or calculated float
555
509
  * - The form question field of the fieldset is hidden
556
510
  * - All depending fields (used in the expression) are hidden
557
- * - The evaluated `question.isRequired` expression returns `false`
511
+ * - The evaluated `question.raw.isRequired` expression returns `false`
558
512
  * - The question type is FormQuestion or CalculatedFloatQuestion
559
513
  *
560
514
  * @property {Boolean} optional
561
515
  */
562
- optional: computed(
563
- "document.jexl",
564
- "fieldset.field.hidden",
565
- "jexlContext",
566
- "optionalDependencies.@each.{hidden,value}",
567
- "question.{__typename,isRequired}",
568
- "pk",
569
- function () {
570
- if (
571
- this.fieldset.field?.hidden ||
572
- ["FormQuestion", "CalculatedFloatQuestion"].includes(
573
- this.question.__typename
574
- ) ||
575
- (this.optionalDependencies.length &&
576
- this.optionalDependencies.every(fieldIsHidden))
577
- ) {
578
- return true;
579
- }
516
+ @cached
517
+ get optional() {
518
+ if (
519
+ ["FormQuestion", "CalculatedFloatQuestion"].includes(this.questionType) ||
520
+ this.fieldset.field?.hidden ||
521
+ (this.optionalDependencies.length &&
522
+ this.optionalDependencies.every(fieldIsHiddenOrEmpty))
523
+ ) {
524
+ return true;
525
+ }
580
526
 
581
- try {
582
- return !this.document.jexl.evalSync(
583
- this.question.isRequired,
584
- this.jexlContext
585
- );
586
- } catch (error) {
587
- throw new Error(
588
- `Error while evaluating \`isRequired\` expression on field \`${this.pk}\`: ${error.message}`
589
- );
590
- }
527
+ try {
528
+ return !this.document.jexl.evalSync(
529
+ this.question.raw.isRequired,
530
+ this.jexlContext
531
+ );
532
+ } catch (error) {
533
+ throw new Error(
534
+ `Error while evaluating \`isRequired\` expression on field \`${this.pk}\`: ${error.message}`
535
+ );
591
536
  }
592
- ),
537
+ }
593
538
 
594
539
  /**
595
540
  * Task to save a field. This uses a different mutation for every answer
596
541
  * type.
597
542
  *
598
- * @method save.perform
543
+ * @method save
599
544
  * @return {Object} The response from the server
600
545
  */
601
- save: task(function* () {
546
+ @restartableTask
547
+ *save() {
602
548
  if (this.question.isCalculated) {
603
549
  return;
604
550
  }
605
551
 
606
- const type = this.get("answer.__typename");
552
+ const type = this.answer.raw.__typename;
607
553
 
608
554
  const response = yield this.apollo.mutate(
609
555
  {
610
- mutation: this.get(`saveDocument${type}Mutation`),
556
+ mutation: MUTATION_MAP[type],
611
557
  variables: {
612
558
  input: {
613
559
  question: this.question.slug,
@@ -621,49 +567,47 @@ export default Base.extend({
621
567
  `saveDocument${type}.answer`
622
568
  );
623
569
 
624
- if (this.isNew) {
625
- // if the answer was new we need to set a pk an push the answer to the
626
- // store
627
- this.answer.set("pk", `Answer:${decodeId(response.id)}`);
628
-
629
- this.calumaStore.push(this.answer);
630
- }
570
+ const wasNew = this.isNew;
631
571
 
632
- // update the existing answer
633
- this.answer.setProperties(response);
572
+ Object.entries(response).forEach(([key, value]) => {
573
+ this.answer.raw[key] = value;
574
+ });
634
575
 
635
- this.set("raw.answer", response);
636
- this.set("answer.raw", response);
576
+ if (wasNew) {
577
+ this.answer.pushIntoStore();
578
+ }
637
579
 
638
580
  return response;
639
- }).restartable(),
581
+ }
640
582
 
641
583
  /**
642
- * The error messages on this field.
584
+ * The translated error messages
643
585
  *
644
586
  * @property {String[]} errors
645
- * @accessor
646
587
  */
647
- errors: computed("_errors.[]", function () {
588
+ @cached
589
+ get errors() {
648
590
  return this._errors.map(({ type, context, value }) => {
649
591
  return this.intl.t(
650
592
  `caluma.form.validation.${type}`,
651
593
  Object.assign({}, context, { value })
652
594
  );
653
595
  });
654
- }),
596
+ }
655
597
 
656
598
  /**
657
599
  * Validate the field. Every field goes through the required validation and
658
600
  * the validation for the given question type. This mutates the `errors` on
659
601
  * the field.
660
602
  *
661
- * @method validate.perform
603
+ * @method validate
662
604
  */
663
- validate: task(function* () {
664
- const specificValidation = this.get(`_validate${this.question.__typename}`);
605
+ @restartableTask
606
+ *validate() {
607
+ const specificValidation = this[`_validate${this.questionType}`];
608
+
665
609
  assert(
666
- `Missing validation function for ${this.question.__typename}`,
610
+ `Missing validation function for ${this.questionType}`,
667
611
  specificValidation
668
612
  );
669
613
 
@@ -672,7 +616,7 @@ export default Base.extend({
672
616
  specificValidation,
673
617
  ];
674
618
 
675
- const errors = (yield all(
619
+ const errors = (yield Promise.all(
676
620
  validationFns.map(async (fn) => {
677
621
  const res = await fn.call(this);
678
622
 
@@ -682,121 +626,144 @@ export default Base.extend({
682
626
  .reduce((arr, e) => [...arr, ...e], []) // flatten the array
683
627
  .filter((e) => typeof e === "object");
684
628
 
685
- this.set("_errors", errors);
686
- }).restartable(),
629
+ this._errors = errors;
630
+ }
631
+
632
+ /**
633
+ * Validate the value against the regexes of the given format validators.
634
+ *
635
+ * @method _validateFormatValidators
636
+ * @return {Array<Boolean|Object>} An array of error objects or `true`
637
+ * @private
638
+ */
639
+ _validateFormatValidators() {
640
+ const validators =
641
+ this.question.raw.formatValidators?.edges.map((edge) => edge.node) ?? [];
642
+ const value = this.answer.value;
643
+
644
+ if (isEmpty(value)) {
645
+ // empty values should not be validated since they are handled by the
646
+ // requiredness validation
647
+ return validators.map(() => true);
648
+ }
649
+
650
+ return validators.map((validator) => {
651
+ return (
652
+ new RegExp(validator.regex).test(value) || {
653
+ type: "format",
654
+ context: { errorMsg: validator.errorMsg },
655
+ value,
656
+ }
657
+ );
658
+ });
659
+ }
687
660
 
688
661
  /**
689
662
  * Method to validate if a question is required or not.
690
663
  *
691
664
  * @method _validateRequired
692
- * @return {RSVP.Promise} Returns an promise which resolves into an object if invalid or true if valid
693
- * @internal
665
+ * @return {Boolean|Object} Returns an object if invalid or true if valid
666
+ * @private
694
667
  */
695
- async _validateRequired() {
668
+ _validateRequired() {
696
669
  return (
697
670
  this.optional ||
698
- validate("presence", this.get("answer.value"), { presence: true })
671
+ validate("presence", this.answer.value, { presence: true })
699
672
  );
700
- },
673
+ }
701
674
 
702
675
  /**
703
676
  * Method to validate a text question. This checks if the value longer than
704
677
  * predefined by the question.
705
678
  *
706
679
  * @method _validateTextQuestion
707
- * @return {Object|Boolean} Returns an object if invalid or true if valid
708
- * @internal
680
+ * @return {Array<Boolean|Object>} An array of error objects or `true`
681
+ * @private
709
682
  */
710
- async _validateTextQuestion() {
683
+ _validateTextQuestion() {
711
684
  return [
712
- ...(await this.validator.validate(
713
- this.get("answer.value"),
714
- this.get("question.meta.formatValidators") || []
715
- )),
716
- validate("length", this.get("answer.value"), {
717
- min: this.get("question.textMinLength") || 0,
718
- max: this.get("question.textMaxLength") || Number.POSITIVE_INFINITY,
685
+ ...this._validateFormatValidators(),
686
+ validate("length", this.answer.value, {
687
+ min: this.question.raw.textMinLength || 0,
688
+ max: this.question.raw.textMaxLength || Number.POSITIVE_INFINITY,
719
689
  }),
720
690
  ];
721
- },
691
+ }
722
692
 
723
693
  /**
724
694
  * Method to validate a textarea question. This checks if the value longer
725
695
  * than predefined by the question.
726
696
  *
727
697
  * @method _validateTextareaQuestion
728
- * @return {Object|Boolean} Returns an object if invalid or true if valid
729
- * @internal
698
+ * @return {Array<Boolean|Object>} An array of error objects or `true`
699
+ * @private
730
700
  */
731
- async _validateTextareaQuestion() {
701
+ _validateTextareaQuestion() {
732
702
  return [
733
- ...(await this.validator.validate(
734
- this.get("answer.value"),
735
- this.get("question.meta.formatValidators") || []
736
- )),
737
- validate("length", this.get("answer.value"), {
738
- min: this.get("question.textareaMinLength") || 0,
739
- max: this.get("question.textareaMaxLength") || Number.POSITIVE_INFINITY,
703
+ ...this._validateFormatValidators(),
704
+ validate("length", this.answer.value, {
705
+ min: this.question.raw.textareaMinLength || 0,
706
+ max: this.question.raw.textareaMaxLength || Number.POSITIVE_INFINITY,
740
707
  }),
741
708
  ];
742
- },
709
+ }
743
710
 
744
711
  /**
745
712
  * Method to validate an integer question. This checks if the value is bigger
746
713
  * or less than the options provided by the question.
747
714
  *
748
715
  * @method _validateIntegerQuestion
749
- * @return {Object|Boolean} Returns an object if invalid or true if valid
750
- * @internal
716
+ * @return {Boolean|Object} Returns an object if invalid or true if valid
717
+ * @private
751
718
  */
752
719
  _validateIntegerQuestion() {
753
- return validate("number", this.get("answer.value"), {
720
+ return validate("number", this.answer.value, {
754
721
  integer: true,
755
- gte: this.get("question.integerMinValue") || Number.NEGATIVE_INFINITY,
756
- lte: this.get("question.integerMaxValue") || Number.POSITIVE_INFINITY,
722
+ gte: this.question.raw.integerMinValue || Number.NEGATIVE_INFINITY,
723
+ lte: this.question.raw.integerMaxValue || Number.POSITIVE_INFINITY,
757
724
  });
758
- },
725
+ }
759
726
 
760
727
  /**
761
728
  * Method to validate a float question. This checks if the value is bigger or
762
729
  * less than the options provided by the question.
763
730
  *
764
731
  * @method _validateFloatQuestion
765
- * @return {Object|Boolean} Returns an object if invalid or true if valid
766
- * @internal
732
+ * @return {Boolean|Object} Returns an object if invalid or true if valid
733
+ * @private
767
734
  */
768
735
  _validateFloatQuestion() {
769
- return validate("number", this.get("answer.value"), {
770
- gte: this.get("question.floatMinValue") || Number.NEGATIVE_INFINITY,
771
- lte: this.get("question.floatMaxValue") || Number.POSITIVE_INFINITY,
736
+ return validate("number", this.answer.value, {
737
+ gte: this.question.raw.floatMinValue || Number.NEGATIVE_INFINITY,
738
+ lte: this.question.raw.floatMaxValue || Number.POSITIVE_INFINITY,
772
739
  });
773
- },
740
+ }
774
741
 
775
742
  /**
776
743
  * Method to validate a radio question. This checks if the value is included
777
744
  * in the provided options of the question.
778
745
  *
779
746
  * @method _validateChoiceQuestion
780
- * @return {Object|Boolean} Returns an object if invalid or true if valid
781
- * @internal
747
+ * @return {Boolean|Object} Returns an object if invalid or true if valid
748
+ * @private
782
749
  */
783
750
  _validateChoiceQuestion() {
784
- return validate("inclusion", this.get("answer.value"), {
751
+ return validate("inclusion", this.answer.value, {
785
752
  allowBlank: true,
786
753
  in: (this.options || []).map(({ slug }) => slug),
787
754
  });
788
- },
755
+ }
789
756
 
790
757
  /**
791
758
  * Method to validate a checkbox question. This checks if the all of the
792
759
  * values are included in the provided options of the question.
793
760
  *
794
761
  * @method _validateMultipleChoiceQuestion
795
- * @return {Object[]|Boolean[]|Mixed[]} Returns per value an object if invalid or true if valid
796
- * @internal
762
+ * @return {Boolean|Object} Returns an object if invalid or true if valid
763
+ * @private
797
764
  */
798
765
  _validateMultipleChoiceQuestion() {
799
- const value = this.get("answer.value");
766
+ const value = this.answer.value;
800
767
  if (!value) {
801
768
  return true;
802
769
  }
@@ -805,34 +772,34 @@ export default Base.extend({
805
772
  in: (this.options || []).map(({ slug }) => slug),
806
773
  })
807
774
  );
808
- },
775
+ }
809
776
 
810
777
  /**
811
778
  * Method to validate a radio question. This checks if the value is included
812
779
  * in the provided options of the question.
813
780
  *
814
781
  * @method _validateChoiceQuestion
815
- * @return {Object|Boolean} Returns an object if invalid or true if valid
816
- * @internal
782
+ * @return {Promise<Boolean|Object>} A promise which resolves into an object if invalid or true if valid
783
+ * @private
817
784
  */
818
785
  async _validateDynamicChoiceQuestion() {
819
786
  await this.question.loadDynamicOptions.perform();
820
787
 
821
- return validate("inclusion", this.get("answer.value"), {
788
+ return validate("inclusion", this.answer.value, {
822
789
  in: (this.options || []).map(({ slug }) => slug),
823
790
  });
824
- },
791
+ }
825
792
 
826
793
  /**
827
794
  * Method to validate a checkbox question. This checks if the all of the
828
795
  * values are included in the provided options of the question.
829
796
  *
830
797
  * @method _validateMultipleChoiceQuestion
831
- * @return {Object[]|Boolean[]|Mixed[]} Returns per value an object if invalid or true if valid
832
- * @internal
798
+ * @return {Promise<Boolean[]|Object[]|Mixed[]>} A promise which resolves into an array of objects if invalid or true if valid
799
+ * @private
833
800
  */
834
801
  async _validateDynamicMultipleChoiceQuestion() {
835
- const value = this.get("answer.value");
802
+ const value = this.answer.value;
836
803
 
837
804
  if (!value) {
838
805
  return true;
@@ -845,45 +812,45 @@ export default Base.extend({
845
812
  in: (this.options || []).map(({ slug }) => slug),
846
813
  });
847
814
  });
848
- },
815
+ }
849
816
 
850
817
  /**
851
818
  * Dummy method for the validation of file uploads.
852
819
  *
853
820
  * @method _validateFileQuestion
854
- * @return {RSVP.Promise}
821
+ * @return {Boolean} Always returns true
855
822
  * @private
856
823
  */
857
824
  _validateFileQuestion() {
858
- return resolve(true);
859
- },
825
+ return true;
826
+ }
860
827
 
861
828
  /**
862
829
  * Method to validate a date question.
863
830
  *
864
831
  * @method _validateDateQuestion
865
832
  * @return {Object[]|Boolean[]|Mixed[]} Returns per value an object if invalid or true if valid
866
- * @internal
833
+ * @private
867
834
  */
868
835
  _validateDateQuestion() {
869
- return validate("date", this.get("answer.value"), {
836
+ return validate("date", this.answer.value, {
870
837
  allowBlank: true,
871
838
  });
872
- },
839
+ }
873
840
 
874
841
  /**
875
842
  * Dummy method for the validation of table fields
876
843
  *
877
844
  * @method _validateTableQuestion
878
- * @return {RSVP.Promise}
845
+ * @return {Promise<Boolean|Object>} A promise which resolves into an object if invalid or true if valid
879
846
  * @private
880
847
  */
881
848
  async _validateTableQuestion() {
882
849
  if (!this.value) return true;
883
850
 
884
- const rowValidations = await all(
851
+ const rowValidations = await Promise.all(
885
852
  this.value.map(async (row) => {
886
- const validFields = await all(
853
+ const validFields = await Promise.all(
887
854
  row.fields.map(async (field) => {
888
855
  await field.validate.perform();
889
856
 
@@ -902,49 +869,49 @@ export default Base.extend({
902
869
  value: null,
903
870
  }
904
871
  );
905
- },
872
+ }
906
873
 
907
874
  /**
908
875
  * Dummy method for the validation of static fields
909
876
  *
910
877
  * @method _validateStaticQuestion
911
- * @return {RSVP.Promise}
878
+ * @return {Boolean} Always returns true
912
879
  * @private
913
880
  */
914
881
  _validateStaticQuestion() {
915
- return resolve(true);
916
- },
882
+ return true;
883
+ }
917
884
 
918
885
  /**
919
886
  * Dummy method for the validation of form fields
920
887
  *
921
888
  * @method _validateFormQuestion
922
- * @return {RSVP.Promise}
889
+ * @return {Boolean} Always returns true
923
890
  * @private
924
891
  */
925
892
  _validateFormQuestion() {
926
- return resolve(true);
927
- },
893
+ return true;
894
+ }
928
895
 
929
896
  /**
930
897
  * Dummy method for the validation of calculated float fields
931
898
  *
932
899
  * @method _validateCalculatedFloatQuestion
933
- * @return {RSVP.Promise}
900
+ * @return {Boolean} Always returns true
934
901
  * @private
935
902
  */
936
903
  _validateCalculatedFloatQuestion() {
937
- return resolve(true);
938
- },
904
+ return true;
905
+ }
939
906
 
940
907
  /**
941
908
  * Dummy method for the validation of work item button fields
942
909
  *
943
910
  * @method _validateActionButtonQuestion
944
- * @return {RSVP.Promise}
911
+ * @return {Boolean} Always returns true
945
912
  * @private
946
913
  */
947
914
  _validateActionButtonQuestion() {
948
- return resolve(true);
949
- },
950
- });
915
+ return true;
916
+ }
917
+ }