@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,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;