@projectcaluma/ember-form 10.0.1 → 11.0.0-beta.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.
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,9 +1,13 @@
1
1
  import { getOwner } from "@ember/application";
2
2
  import { assert } from "@ember/debug";
3
- import { computed, observer, defineProperty } from "@ember/object";
4
- import { reads } from "@ember/object/computed";
5
- import { later, once } from "@ember/runloop";
3
+ import {
4
+ associateDestroyableChild,
5
+ registerDestructor,
6
+ } from "@ember/destroyable";
7
+ import { action } from "@ember/object";
8
+ import { next, cancel, once } from "@ember/runloop";
6
9
  import { inject as service } from "@ember/service";
10
+ import { cached } from "tracked-toolbox";
7
11
 
8
12
  import Base from "@projectcaluma/ember-form/lib/base";
9
13
 
@@ -19,87 +23,112 @@ const STATES = {
19
23
  *
20
24
  * @class NavigationItem
21
25
  */
22
- export const NavigationItem = Base.extend({
23
- router: service(),
26
+ export class NavigationItem extends Base {
27
+ @service router;
24
28
 
25
- init(...args) {
26
- assert("A fieldset `fieldset` must be passed", this.fieldset);
27
- assert("A navigation `navigation` must be passed", this.navigation);
29
+ constructor({ fieldset, navigation, ...args }) {
30
+ assert("`fieldset` must be passed as an argument", fieldset);
31
+ assert("`navigation` must be passed as an argument", navigation);
28
32
 
29
- defineProperty(this, "pk", {
30
- writable: false,
31
- value: `NavigationItem:${this.fieldset.pk}`,
32
- });
33
+ super({ ...args });
34
+
35
+ this.fieldset = fieldset;
36
+ this.navigation = navigation;
37
+
38
+ this.pushIntoStore();
39
+ }
40
+
41
+ /**
42
+ * The fieldset to build the navigation item for
43
+ *
44
+ * @property {Fieldset} fieldset
45
+ */
46
+ fieldset = null;
33
47
 
34
- this._super(...args);
35
- },
48
+ /**
49
+ * The navigation object this item originates from. This is used to determine
50
+ * the items child and parent items.
51
+ *
52
+ * @property {Navigation} navigation
53
+ */
54
+ navigation = null;
55
+
56
+ /**
57
+ * The primary key of the navigation item.
58
+ *
59
+ * @property {String} pk
60
+ */
61
+ @cached
62
+ get pk() {
63
+ return `NavigationItem:${this.fieldset.pk}`;
64
+ }
36
65
 
37
66
  /**
38
67
  * The parent navigation item
39
68
  *
40
69
  * @property {NavigationItem} parent
41
- * @accessor
42
70
  */
43
- parent: computed("_parentSlug", "navigation.items.@each.slug", function () {
71
+ @cached
72
+ get parent() {
44
73
  return this.navigation.items.find((item) => item.slug === this._parentSlug);
45
- }),
74
+ }
46
75
 
47
76
  /**
48
77
  * The children of this navigation item
49
78
  *
50
79
  * @property {NavigationItem[]} children
51
- * @accessor
52
80
  */
53
- children: computed("slug", "navigation.items.@each._parentSlug", function () {
81
+ @cached
82
+ get children() {
54
83
  return this.navigation.items.filter(
55
84
  (item) => item._parentSlug === this.slug
56
85
  );
57
- }),
86
+ }
58
87
 
59
88
  /**
60
89
  * The visible children of this navigation item
61
90
  *
62
91
  * @property {NavigationItem[]} visibleChildren
63
- * @accessor
64
92
  */
65
- visibleChildren: computed("children.@each.visible", function () {
93
+ @cached
94
+ get visibleChildren() {
66
95
  return this.children.filter((child) => child.visible);
67
- }),
68
-
69
- /**
70
- * The fieldset to build the navigation item for
71
- *
72
- * @property {Fieldset} fieldset
73
- */
74
- fieldset: null,
96
+ }
75
97
 
76
98
  /**
77
99
  * The label displayed in the navigation
78
100
  *
79
101
  * @property {String} label
80
- * @accessor
81
102
  */
82
- label: computed.or("fieldset.field.question.label", "fieldset.form.name"),
103
+ @cached
104
+ get label() {
105
+ return (
106
+ this.fieldset.field?.question.raw.label ?? this.fieldset.form.raw.name
107
+ );
108
+ }
83
109
 
84
110
  /**
85
111
  * The slug of the items form
86
112
  *
87
113
  * @property {String} slug
88
- * @accessor
89
114
  */
90
- slug: computed.or(
91
- "fieldset.field.question.subForm.slug",
92
- "fieldset.form.slug"
93
- ),
115
+ @cached
116
+ get slug() {
117
+ return (
118
+ this.fieldset.field?.question.raw.subForm.slug ?? this.fieldset.form.slug
119
+ );
120
+ }
94
121
 
95
122
  /**
96
123
  * The slug of the parent items form
97
124
  *
98
125
  * @property {String} _parentSlug
99
- * @accessor
100
126
  * @private
101
127
  */
102
- _parentSlug: reads("fieldset.field.fieldset.field.question.subForm.slug"),
128
+ @cached
129
+ get _parentSlug() {
130
+ return this.fieldset.field?.fieldset.field?.question.raw.subForm.slug;
131
+ }
103
132
 
104
133
  /**
105
134
  * The item is active if the query param `displayedForm` is equal to the
@@ -107,24 +136,20 @@ export const NavigationItem = Base.extend({
107
136
  *
108
137
  * @property {Boolean} active
109
138
  */
110
- active: computed(
111
- "router.currentRoute.queryParams.displayedForm",
112
- "slug",
113
- function () {
114
- return (
115
- this.slug === this.get("router.currentRoute.queryParams.displayedForm")
116
- );
117
- }
118
- ),
139
+ @cached
140
+ get active() {
141
+ return this.slug === this.router.currentRoute?.queryParams.displayedForm;
142
+ }
119
143
 
120
144
  /**
121
145
  * Whether the item has active children
122
146
  *
123
147
  * @property {Boolean} childrenActive
124
148
  */
125
- childrenActive: computed("children.@each.active", function () {
149
+ @cached
150
+ get childrenActive() {
126
151
  return this.children.some((child) => child.active);
127
- }),
152
+ }
128
153
 
129
154
  /**
130
155
  * The item is navigable if it is not hidden and its fieldset contains at
@@ -132,27 +157,25 @@ export const NavigationItem = Base.extend({
132
157
  *
133
158
  * @property {Boolean} navigable
134
159
  */
135
- navigable: computed(
136
- "fieldset.field.hidden",
137
- "fieldset.fields.@each.{hidden,questionType}",
138
- function () {
139
- return (
140
- (this.fieldset.field === undefined || !this.fieldset.field.hidden) &&
141
- this.fieldset.fields.some(
142
- (field) => field.questionType !== "FormQuestion" && !field.hidden
143
- )
144
- );
145
- }
146
- ),
160
+ @cached
161
+ get navigable() {
162
+ return (
163
+ (this.fieldset.field === undefined || !this.fieldset.field.hidden) &&
164
+ this.fieldset.fields.some(
165
+ (field) => field.questionType !== "FormQuestion" && !field.hidden
166
+ )
167
+ );
168
+ }
147
169
 
148
170
  /**
149
171
  * The item is visible if it is navigable or has at least one child item.
150
172
  *
151
173
  * @property {Boolean} visible
152
174
  */
153
- visible: computed("navigable", "visibleChildren.length", function () {
175
+ @cached
176
+ get visible() {
154
177
  return this.navigable || Boolean(this.visibleChildren.length);
155
- }),
178
+ }
156
179
 
157
180
  /**
158
181
  * The current state consisting of the items and the childrens fieldset
@@ -165,64 +188,53 @@ export const NavigationItem = Base.extend({
165
188
  * - `valid` if every fieldset is `valid`
166
189
  *
167
190
  * @property {String} state
168
- * @accessor
169
191
  */
170
- state: computed(
171
- "fieldsetState",
172
- "visibleChildren.@each.fieldsetState",
173
- function () {
174
- const states = [
175
- this.fieldsetState,
176
- ...this.visibleChildren.map((child) => child.fieldsetState),
177
- ].filter(Boolean);
178
-
179
- if (states.every((state) => state === STATES.EMPTY)) {
180
- return STATES.EMPTY;
181
- }
182
-
183
- if (states.some((state) => state === STATES.INVALID)) {
184
- return STATES.INVALID;
185
- }
186
-
187
- return states.every((state) => state === STATES.VALID)
188
- ? STATES.VALID
189
- : STATES.IN_PROGRESS;
192
+ @cached
193
+ get state() {
194
+ const states = [
195
+ this.fieldsetState,
196
+ ...this.visibleChildren.map((child) => child.fieldsetState),
197
+ ].filter(Boolean);
198
+
199
+ if (states.every((state) => state === STATES.EMPTY)) {
200
+ return STATES.EMPTY;
201
+ }
202
+
203
+ if (states.some((state) => state === STATES.INVALID)) {
204
+ return STATES.INVALID;
190
205
  }
191
- ),
206
+
207
+ return states.every((state) => state === STATES.VALID)
208
+ ? STATES.VALID
209
+ : STATES.IN_PROGRESS;
210
+ }
192
211
 
193
212
  /**
194
213
  * The dirty state of the navigation item. This will be true if at least one
195
214
  * of the children or the navigation fieldset itself is dirty.
196
215
  *
197
216
  * @property {Boolean} fieldsetDirty
198
- * @accessor
199
217
  */
200
- dirty: computed(
201
- "fieldsetDirty",
202
- "visibleChildren.@each.fieldsetDirty",
203
- function () {
204
- return [
205
- this.fieldsetDirty,
206
- ...this.visibleChildren.map((child) => child.fieldsetDirty),
207
- ].some(Boolean);
208
- }
209
- ),
218
+ @cached
219
+ get dirty() {
220
+ return [
221
+ this.fieldsetDirty,
222
+ ...this.visibleChildren.map((child) => child.fieldsetDirty),
223
+ ].some(Boolean);
224
+ }
210
225
 
211
226
  /**
212
227
  * All visible fields (excluding form question fields) of the fieldset that
213
228
  * are visible.
214
229
  *
215
230
  * @property {Field[]} visibleFields
216
- * @accessor
217
231
  */
218
- visibleFields: computed(
219
- "fieldset.fields.@each.{questionType,hidden}",
220
- function () {
221
- return this.fieldset.fields.filter(
222
- (f) => f.questionType !== "FormQuestion" && !f.hidden
223
- );
224
- }
225
- ),
232
+ @cached
233
+ get visibleFields() {
234
+ return this.fieldset.fields.filter(
235
+ (f) => f.questionType !== "FormQuestion" && !f.hidden
236
+ );
237
+ }
226
238
 
227
239
  /**
228
240
  * The current state of the item's fieldset. This does not consider the state
@@ -235,137 +247,147 @@ export const NavigationItem = Base.extend({
235
247
  * - `valid` if every field is valid
236
248
  *
237
249
  * @property {String} fieldsetState
238
- * @accessor
239
250
  */
240
- fieldsetState: computed(
241
- "visibleFields.@each.{isNew,isValid,optional}",
242
- function () {
243
- if (!this.visibleFields.length) {
244
- return null;
245
- }
246
-
247
- if (this.visibleFields.some((f) => !f.isValid && f.isDirty)) {
248
- return STATES.INVALID;
249
- }
250
-
251
- if (this.visibleFields.every((f) => f.isNew)) {
252
- return STATES.EMPTY;
253
- }
254
-
255
- if (
256
- this.visibleFields
257
- .filter((f) => !f.optional)
258
- .every((f) => f.isValid && !f.isNew)
259
- ) {
260
- return STATES.VALID;
261
- }
262
-
263
- return STATES.IN_PROGRESS;
251
+ @cached
252
+ get fieldsetState() {
253
+ if (!this.visibleFields.length) {
254
+ return null;
255
+ }
256
+
257
+ if (this.visibleFields.some((f) => !f.isValid && f.isDirty)) {
258
+ return STATES.INVALID;
264
259
  }
265
- ),
260
+
261
+ if (this.visibleFields.every((f) => f.isNew)) {
262
+ return STATES.EMPTY;
263
+ }
264
+
265
+ if (
266
+ this.visibleFields
267
+ .filter((f) => !f.optional)
268
+ .every((f) => f.isValid && !f.isNew)
269
+ ) {
270
+ return STATES.VALID;
271
+ }
272
+
273
+ return STATES.IN_PROGRESS;
274
+ }
266
275
 
267
276
  /**
268
277
  * The dirty state of the current fieldset. This will be true if at least one
269
278
  * field in the fieldset is dirty.
270
279
  *
271
280
  * @property {Boolean} fieldsetDirty
272
- * @accessor
273
281
  */
274
- fieldsetDirty: computed("visibleFields.@each.isDirty", function () {
282
+ @cached
283
+ get fieldsetDirty() {
275
284
  return this.visibleFields.some((f) => f.isDirty);
276
- }),
277
- });
285
+ }
286
+ }
278
287
 
279
288
  /**
280
289
  * Object to represent a navigation state for a certain document.
281
290
  *
282
291
  * @class Navigation
283
292
  */
284
- export const Navigation = Base.extend({
285
- router: service(),
286
- calumaStore: service(),
293
+ export class Navigation extends Base {
294
+ @service router;
287
295
 
288
- init(...args) {
289
- assert("A document `document` must be passed", this.document);
296
+ constructor({ document, ...args }) {
297
+ assert("`document` must be passed as an argument", document);
290
298
 
291
- defineProperty(this, "pk", {
292
- writable: false,
293
- value: `Navigation:${this.document.pk}`,
294
- });
299
+ super({ ...args });
295
300
 
296
- this._super(...args);
301
+ this.document = document;
297
302
 
298
- this.set("items", []);
303
+ this.pushIntoStore();
299
304
 
300
305
  this._createItems();
301
- },
302
306
 
303
- willDestroy(...args) {
304
- this._super(...args);
307
+ const transitionHandler = () => {
308
+ this._timer = next(this, "goToNextItemIfNonNavigable");
309
+ };
305
310
 
306
- const items = this.items;
307
- this.set("items", []);
308
- items.forEach((item) => item.destroy());
309
- },
311
+ // go to next item in next run loop, this is necessary when the user clicks
312
+ // on a non navigable item in the navigation
313
+ this.router.on("routeDidChange", this, transitionHandler);
314
+
315
+ registerDestructor(this, () => {
316
+ cancel(this._timer);
317
+ this.router.off("routeDidChange", this, transitionHandler);
318
+ });
319
+ }
320
+
321
+ /**
322
+ * The primary key of the navigation
323
+ *
324
+ * @property {String} pk
325
+ */
326
+ @cached
327
+ get pk() {
328
+ return `Navigation:${this.document.pk}`;
329
+ }
310
330
 
311
331
  _createItems() {
332
+ const owner = getOwner(this);
333
+
312
334
  const items = this.document.fieldsets.map((fieldset) => {
313
335
  const pk = `NavigationItem:${fieldset.pk}`;
314
336
 
315
- return (
337
+ return associateDestroyableChild(
338
+ this,
316
339
  this.calumaStore.find(pk) ||
317
- getOwner(this)
318
- .factoryFor("caluma-model:navigation-item")
319
- .create({ fieldset, navigation: this })
340
+ new (owner.factoryFor("caluma-model:navigation-item").class)({
341
+ fieldset,
342
+ navigation: this,
343
+ owner,
344
+ })
320
345
  );
321
346
  });
322
347
 
323
- this.set("items", items);
324
- },
348
+ this.items = items;
349
+ }
325
350
 
326
351
  /**
327
352
  * The document to build the navigation for
328
353
  *
329
354
  * @property {Document} document
330
355
  */
331
- document: null,
356
+ document = null;
332
357
 
333
358
  /**
334
359
  * The navigation items for the given document
335
360
  *
336
361
  * @property {NavigationItem[]} items
337
- * @accessor
338
362
  */
339
- items: null,
363
+ items = [];
340
364
 
341
365
  /**
342
366
  * The top level navigation items. Those are items without a parent item that
343
367
  * are visible.
344
368
  *
345
369
  * @property {NavigationItem[]} rootItems
346
- * @accessor
347
370
  */
348
- rootItems: computed("items.@each.{parent,visible}", function () {
371
+ @cached
372
+ get rootItems() {
349
373
  return this.items.filter((i) => !i.parent && i.visible);
350
- }),
374
+ }
351
375
 
352
376
  /**
353
377
  * The currently active navigation item
354
378
  *
355
379
  * @property {NavigationItem} currentItem
356
- * @accessor
357
380
  */
358
- currentItem: computed("items.@each.active", function () {
381
+ get currentItem() {
359
382
  return this.items.find((item) => item.active);
360
- }),
383
+ }
361
384
 
362
385
  /**
363
386
  * The next navigable item in the navigation tree
364
387
  *
365
388
  * @property {NavigationItem} nextItem
366
- * @accessor
367
389
  */
368
- nextItem: computed("currentItem", "items.@each.navigable", function () {
390
+ get nextItem() {
369
391
  if (!this.currentItem)
370
392
  return this.items.filter((item) => item.navigable)[0];
371
393
 
@@ -374,15 +396,14 @@ export const Navigation = Base.extend({
374
396
  .filter((item) => item.navigable);
375
397
 
376
398
  return items.length ? items[0] : null;
377
- }),
399
+ }
378
400
 
379
401
  /**
380
402
  * The previous navigable item in the navigation tree
381
403
  *
382
404
  * @property {NavigationItem} previousItem
383
- * @accessor
384
405
  */
385
- previousItem: computed("currentItem", "items.@each.navigable", function () {
406
+ get previousItem() {
386
407
  if (!this.currentItem) return null;
387
408
 
388
409
  const items = this.items
@@ -390,36 +411,34 @@ export const Navigation = Base.extend({
390
411
  .filter((item) => item.navigable);
391
412
 
392
413
  return items.length ? items[items.length - 1] : null;
393
- }),
414
+ }
394
415
 
395
416
  /**
396
- * Observer which transitions to the next navigable item if the current item
397
- * is not navigable.
417
+ * Replace the current item with the next navigable item if the current item
418
+ * is not navigable. This makes sure that only one transition per runloop
419
+ * happens.
398
420
  *
399
- * @method preventNonNavigableItem
421
+ * @method goToNextItemIfNonNavigable
400
422
  */
401
- preventNonNavigableItem: observer("currentItem", function () {
402
- if (!this.get("nextItem.slug") || this.get("currentItem.navigable")) {
423
+ @action
424
+ goToNextItemIfNonNavigable() {
425
+ if (this.currentItem?.navigable) {
403
426
  return;
404
427
  }
405
428
 
406
- later(this, () => once(this, "goToNextItem"));
407
- }),
429
+ once(this, "_transitionToNextItem");
430
+ }
408
431
 
409
432
  /**
410
- * Replace the current item with the next navigable item
433
+ * Transition to next item or start (empty displayed form).
411
434
  *
412
- * @method goToNextItem
435
+ * @method _transitionToNextItem
413
436
  */
414
- goToNextItem() {
415
- if (!this.get("nextItem.slug") || this.get("currentItem.navigable")) {
416
- return;
417
- }
418
-
437
+ _transitionToNextItem() {
419
438
  this.router.replaceWith({
420
- queryParams: { displayedForm: this.nextItem.slug },
439
+ queryParams: { displayedForm: this.nextItem?.slug ?? "" },
421
440
  });
422
- },
423
- });
441
+ }
442
+ }
424
443
 
425
444
  export default Navigation;