@projectcaluma/ember-form 11.0.0-beta.3 → 11.0.0-beta.31

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/addon/components/cf-content.hbs +38 -37
  3. package/addon/components/cf-content.js +2 -2
  4. package/addon/components/cf-field/hint.hbs +5 -0
  5. package/addon/components/cf-field/info.hbs +2 -2
  6. package/addon/components/cf-field/info.js +0 -15
  7. package/addon/components/cf-field/input/action-button.hbs +8 -4
  8. package/addon/components/cf-field/input/action-button.js +60 -59
  9. package/addon/components/cf-field/input/checkbox.hbs +2 -3
  10. package/addon/components/cf-field/input/date.hbs +12 -25
  11. package/addon/components/cf-field/input/date.js +19 -11
  12. package/addon/components/cf-field/input/files.hbs +38 -0
  13. package/addon/components/cf-field/input/files.js +113 -0
  14. package/addon/components/cf-field/input/powerselect.hbs +27 -29
  15. package/addon/components/cf-field/input/powerselect.js +8 -2
  16. package/addon/components/cf-field/input/radio.hbs +2 -2
  17. package/addon/components/cf-field/input/static.hbs +1 -1
  18. package/addon/components/cf-field/input/table.hbs +20 -19
  19. package/addon/components/cf-field/input/table.js +1 -11
  20. package/addon/components/cf-field/input.hbs +8 -21
  21. package/addon/components/cf-field/input.js +32 -14
  22. package/addon/components/cf-field/label.hbs +4 -2
  23. package/addon/components/cf-field-value.hbs +22 -7
  24. package/addon/components/cf-field-value.js +12 -36
  25. package/addon/components/cf-field.hbs +42 -9
  26. package/addon/components/cf-field.js +41 -17
  27. package/addon/components/cf-form-wrapper.hbs +4 -1
  28. package/addon/components/cf-form.hbs +6 -1
  29. package/addon/components/document-validity.js +16 -1
  30. package/addon/gql/fragments/field.graphql +32 -2
  31. package/addon/gql/mutations/save-document-files-answer.graphql +9 -0
  32. package/addon/gql/queries/document-forms.graphql +1 -1
  33. package/addon/gql/queries/dynamic-options.graphql +1 -1
  34. package/addon/gql/queries/{fileanswer-info.graphql → filesanswer-info.graphql} +4 -4
  35. package/addon/helpers/format-graphql-error.js +21 -0
  36. package/addon/helpers/get-widget.js +16 -2
  37. package/addon/instance-initializers/form-widget-overrides.js +52 -0
  38. package/addon/lib/field.js +80 -51
  39. package/addon/lib/navigation.js +3 -1
  40. package/addon/lib/question.js +12 -4
  41. package/addon/modifiers/autoresize.js +14 -0
  42. package/addon/services/caluma-store.js +2 -0
  43. package/app/components/cf-field/{input/file.js → hint.js} +1 -1
  44. package/app/components/cf-field/input/files.js +1 -0
  45. package/app/helpers/format-graphql-error.js +1 -0
  46. package/app/helpers/get-widget.js +1 -4
  47. package/app/instance-initializers/form-widget-overrides.js +4 -0
  48. package/app/modifiers/autoresize.js +1 -0
  49. package/app/styles/@projectcaluma/ember-form.scss +5 -15
  50. package/app/styles/_flatpickr.scss +47 -0
  51. package/blueprints/@projectcaluma/ember-form/index.js +1 -1
  52. package/index.js +12 -0
  53. package/package.json +45 -35
  54. package/translations/de.yaml +6 -6
  55. package/translations/en.yaml +6 -6
  56. package/translations/fr.yaml +6 -6
  57. package/addon/components/cf-field/input/file.hbs +0 -32
  58. package/addon/components/cf-field/input/file.js +0 -89
  59. package/addon/components/cf-field/label.js +0 -11
  60. package/addon/gql/mutations/remove-answer.graphql +0 -7
  61. package/addon/gql/mutations/save-document-file-answer.graphql +0 -9
  62. package/addon/instance-initializers/setup-pikaday-i18n.js +0 -35
@@ -1,5 +1,5 @@
1
- import { getOwner } from "@ember/application";
2
- import { set } from "@ember/object";
1
+ import { action } from "@ember/object";
2
+ import { macroCondition, isTesting } from "@embroider/macros";
3
3
  import Component from "@glimmer/component";
4
4
  import { timeout, restartableTask } from "ember-concurrency";
5
5
 
@@ -9,7 +9,7 @@ import { hasQuestionType } from "@projectcaluma/ember-core/helpers/has-question-
9
9
  * Component to display a label and input for a certain field of a document.
10
10
  *
11
11
  * ```hbs
12
- * {{cf-field field=someField}}
12
+ * <CfField @field={{this.someField}} />
13
13
  * ```
14
14
  *
15
15
  * You can disable the field by passing `disabled=true`.
@@ -18,6 +18,16 @@ import { hasQuestionType } from "@projectcaluma/ember-core/helpers/has-question-
18
18
  * @argument {Field} field The field data model to render
19
19
  */
20
20
  export default class CfFieldComponent extends Component {
21
+ @action
22
+ registerComponent() {
23
+ this.args.field._components.add(this);
24
+ }
25
+
26
+ @action
27
+ unregisterComponent() {
28
+ this.args.field._components.delete(this);
29
+ }
30
+
21
31
  get visible() {
22
32
  return (
23
33
  !this.args.field?.hidden &&
@@ -36,35 +46,49 @@ export default class CfFieldComponent extends Component {
36
46
  return !hasQuestionType(this.args.field?.question, "action-button");
37
47
  }
38
48
 
49
+ get hintTextVisible() {
50
+ return !hasQuestionType(
51
+ this.args.field?.question,
52
+ "action-button",
53
+ "static",
54
+ "form"
55
+ );
56
+ }
57
+
39
58
  get saveIndicatorVisible() {
40
59
  return !hasQuestionType(this.args.field?.question, "action-button");
41
60
  }
42
61
 
43
62
  /**
44
- * Task to save a field. This will set the passed value to the answer and
45
- * save the field to the API after a timeout off 500 milliseconds.
63
+ * Task to save a field. This will set the passed value to the answer and save
64
+ * the field to the API after a timeout of 500 milliseconds which intends to
65
+ * reduce the amount of saved values when changed rapidly.
46
66
  *
47
67
  * @method save
48
- * @param {String|Number|String[]} value
68
+ * @param {String|Number|String[]} value The new value to save to the field
69
+ * @param {Boolean} bypassTimeout Whether to bypass the timeout
49
70
  */
50
71
  @restartableTask
51
- *save(value) {
52
- const { environment } =
53
- getOwner(this).resolveRegistration("config:environment");
72
+ *save(value, bypassTimeout = false) {
73
+ if (typeof this.args.onSave === "function") {
74
+ return yield this.args.onSave(this.args.field, value);
75
+ }
54
76
 
55
77
  /* istanbul ignore next */
56
- if (environment !== "test") {
57
- yield timeout(500);
78
+ if (macroCondition(isTesting())) {
79
+ // no timeout
80
+ } else {
81
+ if (!bypassTimeout) {
82
+ yield timeout(500);
83
+ }
58
84
  }
59
85
 
60
- set(this.args.field.answer, "value", value);
86
+ if (this.args.field.answer) {
87
+ this.args.field.answer.value = value;
88
+ }
61
89
 
62
90
  yield this.args.field.validate.perform();
63
91
 
64
- try {
65
- return yield this.args.field.save.unlinked().perform();
66
- } catch (e) {
67
- // that's ok
68
- }
92
+ return yield this.args.field.save.unlinked().perform();
69
93
  }
70
94
  }
@@ -1,6 +1,8 @@
1
1
  {{#let
2
2
  (component
3
- (get-widget @fieldset.field.question @fieldset.form default="cf-form")
3
+ (ensure-safe-component
4
+ (get-widget @fieldset.field.question @fieldset.form default="cf-form")
5
+ )
4
6
  )
5
7
  as |FormComponent|
6
8
  }}
@@ -9,5 +11,6 @@
9
11
  @fieldset={{@fieldset}}
10
12
  @context={{@context}}
11
13
  @disabled={{@disabled}}
14
+ @onSave={{@onSave}}
12
15
  />
13
16
  {{/let}}
@@ -3,7 +3,12 @@
3
3
  {{#if (has-block)}}
4
4
  {{yield field}}
5
5
  {{else}}
6
- <CfField @field={{field}} @disabled={{@disabled}} @context={{@context}} />
6
+ <CfField
7
+ @field={{field}}
8
+ @disabled={{@disabled}}
9
+ @context={{@context}}
10
+ @onSave={{@onSave}}
11
+ />
7
12
  {{/if}}
8
13
  {{/each}}
9
14
  </form>
@@ -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,16 @@ 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
+ }
31
+ hintText
22
32
  }
23
33
  ... on TextareaQuestion {
24
34
  textareaMinLength: minLength
@@ -28,6 +38,16 @@ fragment SimpleQuestion on Question {
28
38
  value
29
39
  }
30
40
  placeholder
41
+ formatValidators {
42
+ edges {
43
+ node {
44
+ slug
45
+ regex
46
+ errorMsg
47
+ }
48
+ }
49
+ }
50
+ hintText
31
51
  }
32
52
  ... on IntegerQuestion {
33
53
  integerMinValue: minValue
@@ -37,6 +57,7 @@ fragment SimpleQuestion on Question {
37
57
  value
38
58
  }
39
59
  placeholder
60
+ hintText
40
61
  }
41
62
  ... on FloatQuestion {
42
63
  floatMinValue: minValue
@@ -46,6 +67,7 @@ fragment SimpleQuestion on Question {
46
67
  value
47
68
  }
48
69
  placeholder
70
+ hintText
49
71
  }
50
72
  ... on ChoiceQuestion {
51
73
  choiceOptions: options {
@@ -62,6 +84,7 @@ fragment SimpleQuestion on Question {
62
84
  id
63
85
  value
64
86
  }
87
+ hintText
65
88
  }
66
89
  ... on MultipleChoiceQuestion {
67
90
  multipleChoiceOptions: options {
@@ -78,18 +101,24 @@ fragment SimpleQuestion on Question {
78
101
  id
79
102
  value
80
103
  }
104
+ hintText
81
105
  }
82
106
  ... on DateQuestion {
83
107
  dateDefaultAnswer: defaultAnswer {
84
108
  id
85
109
  value
86
110
  }
111
+ hintText
87
112
  }
88
113
  ... on StaticQuestion {
89
114
  staticContent
90
115
  }
91
116
  ... on CalculatedFloatQuestion {
92
117
  calcExpression
118
+ hintText
119
+ }
120
+ ... on FilesQuestion {
121
+ hintText
93
122
  }
94
123
  ... on ActionButtonQuestion {
95
124
  action
@@ -112,6 +141,7 @@ fragment FieldTableQuestion on Question {
112
141
  }
113
142
  }
114
143
  }
144
+ hintText
115
145
  tableDefaultAnswer: defaultAnswer {
116
146
  id
117
147
  value {
@@ -204,8 +234,8 @@ fragment SimpleAnswer on Answer {
204
234
  ... on ListAnswer {
205
235
  listValue: value
206
236
  }
207
- ... on FileAnswer {
208
- fileValue: value {
237
+ ... on FilesAnswer {
238
+ filesValue: value {
209
239
  id
210
240
  uploadUrl
211
241
  downloadUrl
@@ -0,0 +1,9 @@
1
+ #import * from '../fragments/field.graphql'
2
+
3
+ mutation SaveDocumentFilesAnswer($input: SaveDocumentFilesAnswerInput!) {
4
+ saveDocumentFilesAnswer(input: $input) {
5
+ answer {
6
+ ...FieldAnswer
7
+ }
8
+ }
9
+ }
@@ -1,7 +1,7 @@
1
1
  #import FieldQuestion, FieldTableQuestion, SimpleQuestion from '../fragments/field.graphql'
2
2
 
3
3
  query DocumentForms($slug: String!) {
4
- allForms(filter: [{ slug: $slug }]) {
4
+ allForms(filter: [{ slugs: [$slug] }]) {
5
5
  edges {
6
6
  node {
7
7
  id
@@ -1,5 +1,5 @@
1
1
  query DynamicOptions($question: String!) {
2
- allQuestions(filter: [{ slug: $question }], first: 1) {
2
+ allQuestions(filter: [{ slugs: [$question] }], first: 1) {
3
3
  edges {
4
4
  node {
5
5
  id
@@ -1,8 +1,8 @@
1
- query FileAnswerInfo($id: ID!) {
1
+ query FilesAnswerInfo($id: ID!) {
2
2
  node(id: $id) {
3
- id
4
- ... on FileAnswer {
5
- fileValue: value {
3
+ ... on FilesAnswer {
4
+ id
5
+ value {
6
6
  id
7
7
  uploadUrl
8
8
  downloadUrl
@@ -0,0 +1,21 @@
1
+ import { helper } from "@ember/component/helper";
2
+
3
+ export function formatGraphqlErrorObject(error) {
4
+ try {
5
+ const path = error.path.join(".");
6
+ const { line, column } = error.locations[error.locations.length - 1];
7
+
8
+ return `${path}:${line}:${column}: ${error.message}`;
9
+ } catch (e) {
10
+ return null;
11
+ }
12
+ }
13
+
14
+ export function formatGraphqlError(error) {
15
+ return (
16
+ error?.errors?.map(formatGraphqlErrorObject).filter(Boolean).join("\n") ??
17
+ ""
18
+ );
19
+ }
20
+
21
+ export default helper(([error]) => formatGraphqlError(error));
@@ -1,6 +1,15 @@
1
1
  import Helper from "@ember/component/helper";
2
2
  import { warn } from "@ember/debug";
3
3
  import { inject as service } from "@ember/service";
4
+ import { ensureSafeComponent } from "@embroider/util";
5
+
6
+ import InputComponent from "@projectcaluma/ember-form/components/cf-field/input";
7
+ import FormComponent from "@projectcaluma/ember-form/components/cf-form";
8
+
9
+ const DEFAULT_WIDGETS = {
10
+ "cf-field/input": InputComponent,
11
+ "cf-form": FormComponent,
12
+ };
4
13
 
5
14
  /**
6
15
  * Helper for getting the right widget.
@@ -42,9 +51,14 @@ export default class GetWidgetHelper extends Helper {
42
51
  { id: "ember-caluma.unregistered-override" }
43
52
  );
44
53
 
45
- if (override) return widget;
54
+ if (override) {
55
+ return ensureSafeComponent(
56
+ override.componentClass ?? override.component,
57
+ this
58
+ );
59
+ }
46
60
  }
47
61
 
48
- return defaultWidget;
62
+ return ensureSafeComponent(DEFAULT_WIDGETS[defaultWidget], this);
49
63
  }
50
64
  }
@@ -0,0 +1,52 @@
1
+ import { setOwner } from "@ember/application";
2
+ import { inject as service } from "@ember/service";
3
+
4
+ import HiddenComponent from "@projectcaluma/ember-form/components/cf-field/input/hidden";
5
+ import PowerSelectComponent from "@projectcaluma/ember-form/components/cf-field/input/powerselect";
6
+
7
+ class HiddenOverride {
8
+ @service intl;
9
+
10
+ get label() {
11
+ return this.intl.t("caluma.form-builder.question.widgetOverrides.hidden");
12
+ }
13
+
14
+ component = "cf-field/input/hidden";
15
+ componentClass = HiddenComponent;
16
+ }
17
+
18
+ class PowerSelectOverride {
19
+ @service intl;
20
+
21
+ get label() {
22
+ return this.intl.t(
23
+ "caluma.form-builder.question.widgetOverrides.powerselect"
24
+ );
25
+ }
26
+
27
+ component = "cf-field/input/powerselect";
28
+ componentClass = PowerSelectComponent;
29
+ types = [
30
+ "ChoiceQuestion",
31
+ "MultipleChoiceQuestion",
32
+ "DynamicChoiceQuestion",
33
+ "DynamicMultipleChoiceQuestion",
34
+ ];
35
+ }
36
+
37
+ export function initialize(appInstance) {
38
+ const options = appInstance.lookup("service:caluma-options");
39
+
40
+ const hiddenOverride = new HiddenOverride();
41
+ const powerSelectOverride = new PowerSelectOverride();
42
+
43
+ setOwner(hiddenOverride, appInstance);
44
+ setOwner(powerSelectOverride, appInstance);
45
+
46
+ options.registerComponentOverride(hiddenOverride);
47
+ options.registerComponentOverride(powerSelectOverride);
48
+ }
49
+
50
+ export default {
51
+ initialize,
52
+ };
@@ -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";
@@ -12,7 +13,7 @@ import { cached } from "tracked-toolbox";
12
13
 
13
14
  import { decodeId } from "@projectcaluma/ember-core/helpers/decode-id";
14
15
  import saveDocumentDateAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-date-answer.graphql";
15
- import saveDocumentFileAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-file-answer.graphql";
16
+ import saveDocumentFilesAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-files-answer.graphql";
16
17
  import saveDocumentFloatAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-float-answer.graphql";
17
18
  import saveDocumentIntegerAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-integer-answer.graphql";
18
19
  import saveDocumentListAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-list-answer.graphql";
@@ -33,7 +34,7 @@ export const TYPE_MAP = {
33
34
  DynamicChoiceQuestion: "StringAnswer",
34
35
  TableQuestion: "TableAnswer",
35
36
  FormQuestion: null,
36
- FileQuestion: "FileAnswer",
37
+ FilesQuestion: "FilesAnswer",
37
38
  StaticQuestion: null,
38
39
  DateQuestion: "DateAnswer",
39
40
  };
@@ -43,7 +44,7 @@ const MUTATION_MAP = {
43
44
  IntegerAnswer: saveDocumentIntegerAnswerMutation,
44
45
  StringAnswer: saveDocumentStringAnswerMutation,
45
46
  ListAnswer: saveDocumentListAnswerMutation,
46
- FileAnswer: saveDocumentFileAnswerMutation,
47
+ FilesAnswer: saveDocumentFilesAnswerMutation,
47
48
  DateAnswer: saveDocumentDateAnswerMutation,
48
49
  TableAnswer: saveDocumentTableAnswerMutation,
49
50
  };
@@ -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
 
@@ -83,18 +83,12 @@ export default class Field extends Base {
83
83
  _createQuestion() {
84
84
  const owner = getOwner(this);
85
85
 
86
- const question =
86
+ this.question =
87
87
  this.calumaStore.find(`Question:${this.raw.question.slug}`) ||
88
88
  new (owner.factoryFor("caluma-model:question").class)({
89
89
  raw: this.raw.question,
90
90
  owner,
91
91
  });
92
-
93
- if (question.isDynamic) {
94
- question.loadDynamicOptions.perform();
95
- }
96
-
97
- this.question = question;
98
92
  }
99
93
 
100
94
  _createAnswer() {
@@ -113,6 +107,7 @@ export default class Field extends Base {
113
107
 
114
108
  answer = new Answer({
115
109
  raw: {
110
+ id: null,
116
111
  __typename: answerType,
117
112
  question: { slug: this.raw.question.slug },
118
113
  [camelize(answerType.replace(/Answer$/, "Value"))]: null,
@@ -152,6 +147,16 @@ export default class Field extends Base {
152
147
  */
153
148
  @tracked _errors = [];
154
149
 
150
+ /**
151
+ * Currently rendered field components that use this field. This is used in
152
+ * the document validity component to await all current save tasks before
153
+ * validating.
154
+ *
155
+ * @property {Set} _components
156
+ * @private
157
+ */
158
+ _components = new Set();
159
+
155
160
  /**
156
161
  * The primary key of the field. Consists of the document and question primary
157
162
  * keys.
@@ -577,10 +582,10 @@ export default class Field extends Base {
577
582
  @cached
578
583
  get errors() {
579
584
  return this._errors.map(({ type, context, value }) => {
580
- return this.intl.t(
581
- `caluma.form.validation.${type}`,
582
- Object.assign({}, context, { value })
583
- );
585
+ return this.intl.t(`caluma.form.validation.${type}`, {
586
+ ...context,
587
+ value,
588
+ });
584
589
  });
585
590
  }
586
591
 
@@ -618,6 +623,35 @@ export default class Field extends Base {
618
623
  this._errors = errors;
619
624
  }
620
625
 
626
+ /**
627
+ * Validate the value against the regexes of the given format validators.
628
+ *
629
+ * @method _validateFormatValidators
630
+ * @return {Array<Boolean|Object>} An array of error objects or `true`
631
+ * @private
632
+ */
633
+ _validateFormatValidators() {
634
+ const validators =
635
+ this.question.raw.formatValidators?.edges.map((edge) => edge.node) ?? [];
636
+ const value = this.answer.value;
637
+
638
+ if (isEmpty(value)) {
639
+ // empty values should not be validated since they are handled by the
640
+ // requiredness validation
641
+ return validators.map(() => true);
642
+ }
643
+
644
+ return validators.map((validator) => {
645
+ return (
646
+ new RegExp(validator.regex).test(value) || {
647
+ type: "format",
648
+ context: { errorMsg: validator.errorMsg },
649
+ value,
650
+ }
651
+ );
652
+ });
653
+ }
654
+
621
655
  /**
622
656
  * Method to validate if a question is required or not.
623
657
  *
@@ -637,15 +671,12 @@ export default class Field extends Base {
637
671
  * predefined by the question.
638
672
  *
639
673
  * @method _validateTextQuestion
640
- * @return {Promise<Boolean|Object>} A promise which resolves into an object if invalid or true if valid
674
+ * @return {Array<Boolean|Object>} An array of error objects or `true`
641
675
  * @private
642
676
  */
643
- async _validateTextQuestion() {
677
+ _validateTextQuestion() {
644
678
  return [
645
- ...(await this.validator.validate(
646
- this.answer.value,
647
- this.question.raw.meta.formatValidators ?? []
648
- )),
679
+ ...this._validateFormatValidators(),
649
680
  validate("length", this.answer.value, {
650
681
  min: this.question.raw.textMinLength || 0,
651
682
  max: this.question.raw.textMaxLength || Number.POSITIVE_INFINITY,
@@ -658,15 +689,12 @@ export default class Field extends Base {
658
689
  * than predefined by the question.
659
690
  *
660
691
  * @method _validateTextareaQuestion
661
- * @return {Promise<Boolean|Object>} A promise which resolves into an object if invalid or true if valid
692
+ * @return {Array<Boolean|Object>} An array of error objects or `true`
662
693
  * @private
663
694
  */
664
- async _validateTextareaQuestion() {
695
+ _validateTextareaQuestion() {
665
696
  return [
666
- ...(await this.validator.validate(
667
- this.answer.value,
668
- this.question.raw.meta.formatValidators ?? []
669
- )),
697
+ ...this._validateFormatValidators(),
670
698
  validate("length", this.answer.value, {
671
699
  min: this.question.raw.textareaMinLength || 0,
672
700
  max: this.question.raw.textareaMaxLength || Number.POSITIVE_INFINITY,
@@ -714,10 +742,7 @@ export default class Field extends Base {
714
742
  * @private
715
743
  */
716
744
  _validateChoiceQuestion() {
717
- return validate("inclusion", this.answer.value, {
718
- allowBlank: true,
719
- in: (this.options || []).map(({ slug }) => slug),
720
- });
745
+ return this._validateOption(this.answer.value, true);
721
746
  }
722
747
 
723
748
  /**
@@ -729,15 +754,9 @@ export default class Field extends Base {
729
754
  * @private
730
755
  */
731
756
  _validateMultipleChoiceQuestion() {
732
- const value = this.answer.value;
733
- if (!value) {
734
- return true;
735
- }
736
- return value.map((value) =>
737
- validate("inclusion", value, {
738
- in: (this.options || []).map(({ slug }) => slug),
739
- })
740
- );
757
+ return this.answer.value
758
+ ? this.answer.value.map((value) => this._validateOption(value))
759
+ : true;
741
760
  }
742
761
 
743
762
  /**
@@ -749,11 +768,9 @@ export default class Field extends Base {
749
768
  * @private
750
769
  */
751
770
  async _validateDynamicChoiceQuestion() {
752
- await this.question.loadDynamicOptions.perform();
771
+ await this.question.dynamicOptions;
753
772
 
754
- return validate("inclusion", this.answer.value, {
755
- in: (this.options || []).map(({ slug }) => slug),
756
- });
773
+ return this._validateOption(this.answer.value, true);
757
774
  }
758
775
 
759
776
  /**
@@ -771,23 +788,35 @@ export default class Field extends Base {
771
788
  return true;
772
789
  }
773
790
 
774
- await this.question.loadDynamicOptions.perform();
791
+ await this.question.dynamicOptions;
775
792
 
776
- return value.map((value) => {
777
- return validate("inclusion", value, {
778
- in: (this.options || []).map(({ slug }) => slug),
779
- });
793
+ return this.answer.value
794
+ ? value.map((value) => this._validateOption(value))
795
+ : true;
796
+ }
797
+
798
+ _validateOption(value, allowBlank = false) {
799
+ const label = Array.isArray(this.selected)
800
+ ? this.selected.find((selected) => selected.slug === value)?.label
801
+ : this.selected?.label;
802
+
803
+ return validate("inclusion", value, {
804
+ in: (this.options || [])
805
+ .filter((option) => !option.disabled)
806
+ .map(({ slug }) => slug),
807
+ allowBlank,
808
+ label: label ?? value,
780
809
  });
781
810
  }
782
811
 
783
812
  /**
784
813
  * Dummy method for the validation of file uploads.
785
814
  *
786
- * @method _validateFileQuestion
815
+ * @method _validateFilesQuestion
787
816
  * @return {Boolean} Always returns true
788
817
  * @private
789
818
  */
790
- _validateFileQuestion() {
819
+ _validateFilesQuestion() {
791
820
  return true;
792
821
  }
793
822
 
@@ -232,7 +232,9 @@ export class NavigationItem extends Base {
232
232
  @cached
233
233
  get visibleFields() {
234
234
  return this.fieldset.fields.filter(
235
- (f) => f.questionType !== "FormQuestion" && !f.hidden
235
+ (f) =>
236
+ !["FormQuestion", "StaticQuestion"].includes(f.questionType) &&
237
+ !f.hidden
236
238
  );
237
239
  }
238
240