@projectcaluma/ember-form 11.0.0-beta.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.
@@ -1,6 +1,7 @@
1
1
  import { action } from "@ember/object";
2
2
  import Component from "@glimmer/component";
3
3
  import { restartableTask } from "ember-concurrency";
4
+ import { cached } from "tracked-toolbox";
4
5
 
5
6
  /**
6
7
  * Component to check the validity of a document
@@ -31,12 +32,26 @@ export default class DocumentValidity extends Component {
31
32
  * @argument {Boolean} validateOnEnter
32
33
  */
33
34
 
35
+ @cached
34
36
  get isValid() {
35
- return this.args.document.fields.every((f) => f.isValid);
37
+ return this.args.document.fields
38
+ .filter((f) => !f.hidden)
39
+ .every((f) => f.isValid);
36
40
  }
37
41
 
38
42
  @restartableTask
39
43
  *_validate() {
44
+ const saveTasks = this.args.document.fields
45
+ .flatMap((field) => [
46
+ ...[...(field._components ?? [])].map((c) => c.save.last),
47
+ field.save?.last,
48
+ ])
49
+ .filter(Boolean);
50
+
51
+ // Wait until all currently running save tasks in the UI and in the field
52
+ // itself are finished
53
+ yield Promise.all(saveTasks);
54
+
40
55
  for (const field of this.args.document.fields) {
41
56
  yield field.validate.linked().perform();
42
57
  }
@@ -19,6 +19,15 @@ fragment SimpleQuestion on Question {
19
19
  value
20
20
  }
21
21
  placeholder
22
+ formatValidators {
23
+ edges {
24
+ node {
25
+ slug
26
+ regex
27
+ errorMsg
28
+ }
29
+ }
30
+ }
22
31
  }
23
32
  ... on TextareaQuestion {
24
33
  textareaMinLength: minLength
@@ -28,6 +37,15 @@ fragment SimpleQuestion on Question {
28
37
  value
29
38
  }
30
39
  placeholder
40
+ formatValidators {
41
+ edges {
42
+ node {
43
+ slug
44
+ regex
45
+ errorMsg
46
+ }
47
+ }
48
+ }
31
49
  }
32
50
  ... on IntegerQuestion {
33
51
  integerMinValue: minValue
@@ -3,6 +3,7 @@ import { assert } from "@ember/debug";
3
3
  import { associateDestroyableChild } from "@ember/destroyable";
4
4
  import { inject as service } from "@ember/service";
5
5
  import { camelize } from "@ember/string";
6
+ import { isEmpty } from "@ember/utils";
6
7
  import { tracked } from "@glimmer/tracking";
7
8
  import { queryManager } from "ember-apollo-client";
8
9
  import { restartableTask, lastValue, dropTask } from "ember-concurrency";
@@ -63,7 +64,6 @@ const fieldIsHiddenOrEmpty = (field) => {
63
64
  */
64
65
  export default class Field extends Base {
65
66
  @service intl;
66
- @service validator;
67
67
 
68
68
  @queryManager apollo;
69
69
 
@@ -113,6 +113,7 @@ export default class Field extends Base {
113
113
 
114
114
  answer = new Answer({
115
115
  raw: {
116
+ id: null,
116
117
  __typename: answerType,
117
118
  question: { slug: this.raw.question.slug },
118
119
  [camelize(answerType.replace(/Answer$/, "Value"))]: null,
@@ -152,6 +153,16 @@ export default class Field extends Base {
152
153
  */
153
154
  @tracked _errors = [];
154
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
+
155
166
  /**
156
167
  * The primary key of the field. Consists of the document and question primary
157
168
  * keys.
@@ -618,6 +629,35 @@ export default class Field extends Base {
618
629
  this._errors = errors;
619
630
  }
620
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
+ }
660
+
621
661
  /**
622
662
  * Method to validate if a question is required or not.
623
663
  *
@@ -637,15 +677,12 @@ export default class Field extends Base {
637
677
  * predefined by the question.
638
678
  *
639
679
  * @method _validateTextQuestion
640
- * @return {Promise<Boolean|Object>} A promise which resolves into an object if invalid or true if valid
680
+ * @return {Array<Boolean|Object>} An array of error objects or `true`
641
681
  * @private
642
682
  */
643
- async _validateTextQuestion() {
683
+ _validateTextQuestion() {
644
684
  return [
645
- ...(await this.validator.validate(
646
- this.answer.value,
647
- this.question.raw.meta.formatValidators ?? []
648
- )),
685
+ ...this._validateFormatValidators(),
649
686
  validate("length", this.answer.value, {
650
687
  min: this.question.raw.textMinLength || 0,
651
688
  max: this.question.raw.textMaxLength || Number.POSITIVE_INFINITY,
@@ -658,15 +695,12 @@ export default class Field extends Base {
658
695
  * than predefined by the question.
659
696
  *
660
697
  * @method _validateTextareaQuestion
661
- * @return {Promise<Boolean|Object>} A promise which resolves into an object if invalid or true if valid
698
+ * @return {Array<Boolean|Object>} An array of error objects or `true`
662
699
  * @private
663
700
  */
664
- async _validateTextareaQuestion() {
701
+ _validateTextareaQuestion() {
665
702
  return [
666
- ...(await this.validator.validate(
667
- this.answer.value,
668
- this.question.raw.meta.formatValidators ?? []
669
- )),
703
+ ...this._validateFormatValidators(),
670
704
  validate("length", this.answer.value, {
671
705
  min: this.question.raw.textareaMinLength || 0,
672
706
  max: this.question.raw.textareaMaxLength || Number.POSITIVE_INFINITY,
@@ -1,7 +1,11 @@
1
1
  import { getOwner } from "@ember/application";
2
2
  import { assert } from "@ember/debug";
3
- import { associateDestroyableChild } from "@ember/destroyable";
4
- 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";
5
9
  import { inject as service } from "@ember/service";
6
10
  import { cached } from "tracked-toolbox";
7
11
 
@@ -300,7 +304,18 @@ export class Navigation extends Base {
300
304
 
301
305
  this._createItems();
302
306
 
303
- this.router.on("routeDidChange", this, "goToNextItemIfNonNavigable");
307
+ const transitionHandler = () => {
308
+ this._timer = next(this, "goToNextItemIfNonNavigable");
309
+ };
310
+
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
+ });
304
319
  }
305
320
 
306
321
  /**
@@ -399,31 +414,29 @@ export class Navigation extends Base {
399
414
  }
400
415
 
401
416
  /**
402
- * Observer which transitions to the next navigable item if the current item
403
- * 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.
404
420
  *
405
421
  * @method goToNextItemIfNonNavigable
406
422
  */
423
+ @action
407
424
  goToNextItemIfNonNavigable() {
408
- if (!this.nextItem?.slug || this.currentItem?.navigable) {
425
+ if (this.currentItem?.navigable) {
409
426
  return;
410
427
  }
411
428
 
412
- later(this, () => once(this, "goToNextItem"));
429
+ once(this, "_transitionToNextItem");
413
430
  }
414
431
 
415
432
  /**
416
- * Replace the current item with the next navigable item
433
+ * Transition to next item or start (empty displayed form).
417
434
  *
418
- * @method goToNextItem
435
+ * @method _transitionToNextItem
419
436
  */
420
- goToNextItem() {
421
- if (!this.nextItem?.slug || this.currentItem?.navigable) {
422
- return;
423
- }
424
-
437
+ _transitionToNextItem() {
425
438
  this.router.replaceWith({
426
- queryParams: { displayedForm: this.nextItem.slug },
439
+ queryParams: { displayedForm: this.nextItem?.slug ?? "" },
427
440
  });
428
441
  }
429
442
  }
@@ -12,6 +12,7 @@ module.exports = {
12
12
  { name: "ember-math-helpers" },
13
13
  { name: "ember-pikaday" },
14
14
  { name: "ember-power-select" },
15
+ { name: "ember-autoresize-modifier" },
15
16
  ],
16
17
  });
17
18
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projectcaluma/ember-form",
3
- "version": "11.0.0-beta.1",
3
+ "version": "11.0.0-beta.10",
4
4
  "description": "Ember addon for rendering Caluma forms.",
5
5
  "keywords": [
6
6
  "ember-addon"
@@ -16,39 +16,43 @@
16
16
  "dependencies": {
17
17
  "@glimmer/component": "^1.0.4",
18
18
  "@glimmer/tracking": "^1.0.4",
19
- "@projectcaluma/ember-core": "^11.0.0-beta.1",
19
+ "@projectcaluma/ember-core": "^11.0.0-beta.4",
20
20
  "ember-apollo-client": "^3.2.0",
21
- "ember-auto-import": "^2.2.3",
21
+ "ember-auto-import": "^2.4.0",
22
+ "ember-autoresize-modifier": "^0.5.0",
22
23
  "ember-cli-babel": "^7.26.11",
23
24
  "ember-cli-htmlbars": "^6.0.1",
24
25
  "ember-cli-showdown": "^6.0.1",
25
26
  "ember-composable-helpers": "^5.0.0",
26
- "ember-fetch": "^8.0.4",
27
+ "ember-concurrency": "^2.2.1",
28
+ "ember-fetch": "^8.1.1",
27
29
  "ember-in-viewport": "^4.0.0",
28
30
  "ember-intl": "^5.7.2",
29
- "ember-math-helpers": "^2.18.0",
30
- "ember-pikaday": "^3.0.0",
31
+ "ember-math-helpers": "^2.18.1",
32
+ "ember-pikaday": "^4.0.0",
31
33
  "ember-power-select": "^5.0.3",
32
- "ember-resources": "^4.1.3",
33
- "ember-uikit": "^4.0.0",
34
+ "ember-resources": "^4.3.1",
35
+ "ember-uikit": "^5.0.0",
36
+ "ember-validators": "^4.1.2",
34
37
  "graphql": "^15.8.0",
35
38
  "jexl": "^2.3.0",
36
39
  "lodash.isequal": "^4.5.0",
37
- "moment": "^2.29.1",
40
+ "luxon": "^2.3.0",
38
41
  "tracked-toolbox": "^1.2.3"
39
42
  },
40
43
  "devDependencies": {
41
44
  "@ember/optional-features": "2.0.0",
42
45
  "@ember/test-helpers": "2.6.0",
43
- "@embroider/test-setup": "0.49.0",
44
- "@projectcaluma/ember-testing": "10.1.0",
45
- "@projectcaluma/ember-workflow": "10.0.3",
46
+ "@embroider/test-setup": "1.1.0",
47
+ "@faker-js/faker": "6.0.0-alpha.5",
48
+ "@projectcaluma/ember-testing": "11.0.0-beta.2",
49
+ "@projectcaluma/ember-workflow": "11.0.0-beta.4",
46
50
  "broccoli-asset-rev": "3.0.0",
47
51
  "ember-cli": "3.28.5",
48
52
  "ember-cli-code-coverage": "1.0.3",
49
53
  "ember-cli-dependency-checker": "3.2.0",
50
54
  "ember-cli-inject-live-reload": "2.1.0",
51
- "ember-cli-mirage": "2.2.0",
55
+ "ember-cli-mirage": "2.4.0",
52
56
  "ember-cli-sri": "2.1.1",
53
57
  "ember-cli-terser": "4.0.2",
54
58
  "ember-disable-prototype-extensions": "1.1.3",
@@ -60,13 +64,13 @@
60
64
  "ember-source": "3.28.8",
61
65
  "ember-source-channel-url": "3.0.0",
62
66
  "ember-try": "2.0.0",
63
- "faker": "5.5.3",
64
67
  "loader.js": "4.7.0",
68
+ "miragejs": "0.1.43",
65
69
  "npm-run-all": "4.1.5",
66
70
  "qunit": "2.17.2",
67
71
  "qunit-dom": "2.0.0",
68
72
  "uuid": "8.3.2",
69
- "webpack": "5.65.0"
73
+ "webpack": "5.68.0"
70
74
  },
71
75
  "engines": {
72
76
  "node": "12.* || 14.* || >= 16"
@@ -1,9 +0,0 @@
1
- import { action } from "@ember/object";
2
- import Component from "@glimmer/component";
3
-
4
- export default class CfNavigationComponent extends Component {
5
- @action
6
- goToNextItem() {
7
- this.args.navigation.goToNextItem();
8
- }
9
- }
@@ -1,35 +0,0 @@
1
- import EmberObject from "@ember/object";
2
- import { inject as service } from "@ember/service";
3
- import moment from "moment";
4
-
5
- class Translations extends EmberObject {
6
- @service intl;
7
-
8
- get previousMonth() {
9
- return this.intl.t("caluma.form.pikaday.month-previous");
10
- }
11
-
12
- get nextMonth() {
13
- return this.intl.t("caluma.form.pikaday.month-next");
14
- }
15
-
16
- months = moment.localeData().months();
17
- weekdays = moment.localeData().weekdays();
18
- weekdaysShort = moment.localeData().weekdaysShort();
19
- }
20
-
21
- export function initialize(applicationInstance) {
22
- applicationInstance.register("pikaday-i18n:main", Translations, {
23
- singleton: true,
24
- });
25
- applicationInstance.inject(
26
- "component:pikaday-input",
27
- "i18n",
28
- "pikaday-i18n:main"
29
- );
30
- }
31
-
32
- export default {
33
- name: "setup-pikaday-i18n",
34
- initialize,
35
- };