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

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