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

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- };