@projectcaluma/ember-form 11.0.0-beta.2 → 11.0.0-beta.20

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/addon/components/cf-content.hbs +37 -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 +3 -4
  10. package/addon/components/cf-field/input/date.hbs +10 -5
  11. package/addon/components/cf-field/input/date.js +40 -10
  12. package/addon/components/cf-field/input/file.hbs +2 -2
  13. package/addon/components/cf-field/input/file.js +2 -2
  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 +25 -24
  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 +8 -33
  25. package/addon/components/cf-field.hbs +39 -6
  26. package/addon/components/cf-field.js +37 -17
  27. package/addon/components/cf-form-wrapper.hbs +3 -1
  28. package/addon/components/document-validity.js +16 -1
  29. package/addon/gql/fragments/field.graphql +30 -0
  30. package/addon/helpers/format-graphql-error.js +21 -0
  31. package/addon/helpers/get-widget.js +16 -2
  32. package/addon/instance-initializers/form-widget-overrides.js +52 -0
  33. package/addon/lib/field.js +73 -46
  34. package/addon/lib/navigation.js +3 -1
  35. package/addon/lib/question.js +12 -4
  36. package/addon/modifiers/autoresize.js +16 -0
  37. package/app/components/cf-field/hint.js +1 -0
  38. package/app/helpers/format-graphql-error.js +1 -0
  39. package/app/helpers/get-widget.js +1 -4
  40. package/app/instance-initializers/form-widget-overrides.js +4 -0
  41. package/app/modifiers/autoresize.js +1 -0
  42. package/app/styles/@projectcaluma/ember-form.scss +3 -15
  43. package/package.json +37 -25
  44. package/translations/de.yaml +6 -2
  45. package/translations/en.yaml +6 -2
  46. package/translations/fr.yaml +6 -2
  47. package/addon/components/cf-field/label.js +0 -11
  48. package/addon/instance-initializers/setup-pikaday-i18n.js +0 -35
@@ -1,29 +1,27 @@
1
- {{#let (component this.componentName) as |DynamicSelect|}}
2
- <DynamicSelect
3
- @options={{@field.options}}
4
- @selected={{@field.selected}}
5
- @disabled={{@disabled}}
6
- @allowClear={{true}}
7
- @preventScroll={{true}}
8
- @searchEnabled={{this.searchEnabled}}
9
- @searchField="label"
10
- @triggerId={{@field.pk}}
11
- @renderInPlace={{true}}
12
- @placeholder={{this.placeholder}}
13
- @loadingMessage={{t "caluma.form.power-select.options-loading"}}
14
- @searchMessage={{t "caluma.form.power-select.options-empty"}}
15
- @searchPlaceholder={{t "caluma.form.power-select.search-placeholder"}}
16
- @noMatchesMessage={{t "caluma.form.power-select.search-empty"}}
17
- @onChange={{this.change}}
18
- as |option|
19
- >
20
- {{#if (and option.disabled (not @disabled))}}
21
- <del
22
- class="uk-text-muted"
23
- title={{t "caluma.form.optionNotAvailable"}}
24
- >{{option.label}}</del>
25
- {{else}}
26
- {{option.label}}
27
- {{/if}}
28
- </DynamicSelect>
29
- {{/let}}
1
+ <this.selectComponent
2
+ @options={{@field.options}}
3
+ @selected={{@field.selected}}
4
+ @disabled={{@disabled}}
5
+ @allowClear={{true}}
6
+ @preventScroll={{true}}
7
+ @searchEnabled={{this.searchEnabled}}
8
+ @searchField="label"
9
+ @triggerId={{@field.pk}}
10
+ @renderInPlace={{true}}
11
+ @placeholder={{this.placeholder}}
12
+ @loadingMessage={{t "caluma.form.power-select.options-loading"}}
13
+ @searchMessage={{t "caluma.form.power-select.options-empty"}}
14
+ @searchPlaceholder={{t "caluma.form.power-select.search-placeholder"}}
15
+ @noMatchesMessage={{t "caluma.form.power-select.search-empty"}}
16
+ @onChange={{this.change}}
17
+ as |option|
18
+ >
19
+ {{#if (and option.disabled (not @disabled))}}
20
+ <del
21
+ class="uk-text-muted"
22
+ title={{t "caluma.form.optionNotAvailable"}}
23
+ >{{option.label}}</del>
24
+ {{else}}
25
+ {{option.label}}
26
+ {{/if}}
27
+ </this.selectComponent>
@@ -1,7 +1,10 @@
1
1
  import { getOwner } from "@ember/application";
2
2
  import { action } from "@ember/object";
3
3
  import { inject as service } from "@ember/service";
4
+ import { ensureSafeComponent } from "@embroider/util";
4
5
  import Component from "@glimmer/component";
6
+ import PowerSelectComponent from "ember-power-select/components/power-select";
7
+ import PowerSelectMultipleComponent from "ember-power-select/components/power-select-multiple";
5
8
 
6
9
  /**
7
10
  * Dropdown component for the single and multiple choice question type
@@ -16,8 +19,11 @@ export default class CfFieldInputPowerselectComponent extends Component {
16
19
  return this.args.field?.question.isMultipleChoice;
17
20
  }
18
21
 
19
- get componentName() {
20
- return this.multiple ? "power-select-multiple" : "power-select";
22
+ get selectComponent() {
23
+ return ensureSafeComponent(
24
+ this.multiple ? PowerSelectMultipleComponent : PowerSelectComponent,
25
+ this
26
+ );
21
27
  }
22
28
 
23
29
  get searchEnabled() {
@@ -1,5 +1,5 @@
1
1
  {{#each @field.options as |option i|}}
2
- {{#if (gt i 0)}}<br />{{/if}}
2
+ {{#if (and (gt i 0) (not @field.raw.question.meta.vertical))}}<br />{{/if}}
3
3
  <label
4
4
  class="{{if @field.isInvalid 'uk-form-danger'}}
5
5
  {{if @field.raw.question.meta.vertical 'uk-margin-large-right'}}"
@@ -10,7 +10,7 @@
10
10
  name={{@field.pk}}
11
11
  value={{option.slug}}
12
12
  checked={{eq option.slug @field.answer.value}}
13
- disabled={{@disabled}}
13
+ disabled={{or option.disabled @disabled}}
14
14
  {{on "change" (fn @onSave option.slug)}}
15
15
  />
16
16
  {{#if (and option.disabled (not @disabled))}}
@@ -1,4 +1,4 @@
1
1
  <MarkdownToHtml
2
- @markdown={{@field.question.staticContent}}
2
+ @markdown={{@field.question.raw.staticContent}}
3
3
  @openLinksInNewWindow={{true}}
4
4
  />
@@ -1,4 +1,4 @@
1
- <table class="uk-table uk-table-divider">
1
+ <table class="uk-table uk-table-divider uk-margin-remove-vertical">
2
2
  <thead>
3
3
  <tr>
4
4
  {{#each this.columns as |column|}}
@@ -25,27 +25,27 @@
25
25
  class="uk-animation-fade uk-text-danger"
26
26
  />
27
27
  {{/if}}
28
- <button
29
- data-test-edit-row
30
- type="button"
31
- class="uk-button uk-button-link uk-flex-inline uk-margin-small-left"
28
+ <UkButton
29
+ @color="link"
30
+ @onClick={{fn this.edit document}}
32
31
  title={{t "caluma.form.edit"}}
33
- {{on "click" (fn this.edit document)}}
32
+ class="uk-flex-inline uk-margin-small-left table-controls"
33
+ data-test-edit-row
34
34
  >
35
35
  <UkIcon @icon="pencil" />
36
- <span class="uk-hidden">{{t "caluma.form.edit"}}</span>
37
- </button>
36
+ <span hidden>{{t "caluma.form.edit"}}</span>
37
+ </UkButton>
38
38
  {{#unless @disabled}}
39
- <button
40
- data-test-delete-row
41
- type="button"
42
- class="uk-button uk-button-link uk-flex-inline uk-margin-small-left"
39
+ <UkButton
40
+ @color="link"
41
+ @onClick={{fn (perform this.delete) document}}
43
42
  title={{t "caluma.form.delete"}}
44
- {{on "click" (fn (perform this.delete) document)}}
43
+ class="uk-flex-inline uk-margin-small-left table-controls"
44
+ data-test-delete-row
45
45
  >
46
46
  <UkIcon @icon="trash" />
47
- <span class="uk-hidden">{{t "caluma.form.delete"}}</span>
48
- </button>
47
+ <span hidden>{{t "caluma.form.delete"}}</span>
48
+ </UkButton>
49
49
  {{/unless}}
50
50
  </div>
51
51
  </td>
@@ -57,13 +57,13 @@
57
57
  <tr>
58
58
  <td colspan={{add this.columns.length 1}} class="uk-text-center">
59
59
  <UkButton
60
- @size="small"
61
- @color="default"
62
- @on-click={{perform this.add}}
60
+ @color="link"
61
+ @onClick={{perform this.add}}
62
+ title={{t "caluma.form.addRow"}}
63
63
  data-test-add-row
64
64
  >
65
65
  <UkIcon @icon="plus" />
66
- <span class="uk-hidden">{{t "caluma.form.addRow"}}</span>
66
+ <span hidden>{{t "caluma.form.addRow"}}</span>
67
67
  </UkButton>
68
68
  </td>
69
69
  </tr>
@@ -74,7 +74,7 @@
74
74
  {{#if this.documentToEdit}}
75
75
  <UkModal
76
76
  @visible={{this.showAddModal}}
77
- @on-hide={{perform this.closeModal}}
77
+ @onHide={{perform this.close}}
78
78
  @bgClose={{false}}
79
79
  as |modal|
80
80
  >
@@ -83,15 +83,16 @@
83
83
  @document={{this.documentToEdit}}
84
84
  @fieldset={{object-at 0 this.documentToEdit.fieldsets}}
85
85
  @disabled={{@disabled}}
86
+ @context={{@context}}
86
87
  />
87
88
  </modal.body>
88
89
 
89
- <modal.footer @class="uk-text-right">
90
+ <modal.footer class="uk-text-right">
90
91
  {{#if @disabled}}
91
92
  <UkButton
92
93
  @label={{t "caluma.form.close"}}
93
94
  @color="primary"
94
- @on-click={{perform this.close}}
95
+ @onClick={{perform this.close}}
95
96
  @disabled={{this.close.isRunning}}
96
97
  @loading={{this.close.isRunning}}
97
98
  data-test-close
@@ -99,7 +100,7 @@
99
100
  {{else}}
100
101
  <UkButton
101
102
  @label={{t "caluma.form.cancel"}}
102
- @on-click={{perform this.close}}
103
+ @onClick={{perform this.close}}
103
104
  @disabled={{this.close.isRunning}}
104
105
  @loading={{this.close.isRunning}}
105
106
  data-test-cancel
@@ -114,7 +115,7 @@
114
115
  @type="submit"
115
116
  @disabled={{or this.save.isRunning (not isValid)}}
116
117
  @loading={{this.save.isRunning}}
117
- @on-click={{fn (perform this.save) validate}}
118
+ @onClick={{fn (perform this.save) validate}}
118
119
  data-test-save
119
120
  />
120
121
  </DocumentValidity>
@@ -5,22 +5,12 @@ import Component from "@glimmer/component";
5
5
  import { tracked } from "@glimmer/tracking";
6
6
  import { queryManager } from "ember-apollo-client";
7
7
  import { dropTask } from "ember-concurrency";
8
- import UIkit from "uikit";
8
+ import { confirm } from "ember-uikit";
9
9
 
10
10
  import removeDocumentMutation from "@projectcaluma/ember-form/gql/mutations/remove-document.graphql";
11
11
  import saveDocumentMutation from "@projectcaluma/ember-form/gql/mutations/save-document.graphql";
12
12
  import { parseDocument } from "@projectcaluma/ember-form/lib/parsers";
13
13
 
14
- async function confirm(text) {
15
- try {
16
- await UIkit.modal.confirm(text);
17
-
18
- return true;
19
- } catch (error) {
20
- return false;
21
- }
22
- }
23
-
24
14
  export default class CfFieldInputTableComponent extends Component {
25
15
  @service notification;
26
16
  @service intl;
@@ -1,21 +1,8 @@
1
- {{#if this.type}}
2
- {{#let (component (concat "cf-field/input/" this.type)) as |InputComponent|}}
3
- <div
4
- class="uk-form-controls
5
- {{if
6
- (and
7
- (has-question-type @field.question 'multiple-choice' 'choice')
8
- @field.question.raw.meta.vertical
9
- )
10
- 'uk-flex'
11
- }}"
12
- >
13
- <InputComponent
14
- @field={{@field}}
15
- @disabled={{@disabled}}
16
- @onSave={{@onSave}}
17
- @context={{@context}}
18
- />
19
- </div>
20
- {{/let}}
21
- {{/if}}
1
+ <div class="uk-form-controls">
2
+ <this.inputComponent
3
+ @field={{@field}}
4
+ @disabled={{@disabled}}
5
+ @onSave={{@onSave}}
6
+ @context={{@context}}
7
+ />
8
+ </div>
@@ -1,12 +1,33 @@
1
- import { dasherize } from "@ember/string";
1
+ import { ensureSafeComponent } from "@embroider/util";
2
2
  import Component from "@glimmer/component";
3
3
 
4
- const mapping = {
5
- MultipleChoiceQuestion: "checkbox",
6
- ChoiceQuestion: "radio",
7
- DynamicMultipleChoiceQuestion: "checkbox",
8
- DynamicChoiceQuestion: "radio",
9
- CalculatedFloatQuestion: "float",
4
+ import ActionButtonComponent from "@projectcaluma/ember-form/components/cf-field/input/action-button";
5
+ import CheckboxComponent from "@projectcaluma/ember-form/components/cf-field/input/checkbox";
6
+ import DateComponent from "@projectcaluma/ember-form/components/cf-field/input/date";
7
+ import FileComponent from "@projectcaluma/ember-form/components/cf-field/input/file";
8
+ import FloatComponent from "@projectcaluma/ember-form/components/cf-field/input/float";
9
+ import IntegerComponent from "@projectcaluma/ember-form/components/cf-field/input/integer";
10
+ import RadioComponent from "@projectcaluma/ember-form/components/cf-field/input/radio";
11
+ import StaticComponent from "@projectcaluma/ember-form/components/cf-field/input/static";
12
+ import TableComponent from "@projectcaluma/ember-form/components/cf-field/input/table";
13
+ import TextComponent from "@projectcaluma/ember-form/components/cf-field/input/text";
14
+ import TextareaComponent from "@projectcaluma/ember-form/components/cf-field/input/textarea";
15
+
16
+ const COMPONENT_MAPPING = {
17
+ ActionButtonQuestion: ActionButtonComponent,
18
+ CalculatedFloatQuestion: FloatComponent,
19
+ ChoiceQuestion: RadioComponent,
20
+ DateQuestion: DateComponent,
21
+ DynamicChoiceQuestion: RadioComponent,
22
+ DynamicMultipleChoiceQuestion: CheckboxComponent,
23
+ FileQuestion: FileComponent,
24
+ FloatQuestion: FloatComponent,
25
+ IntegerQuestion: IntegerComponent,
26
+ MultipleChoiceQuestion: CheckboxComponent,
27
+ StaticQuestion: StaticComponent,
28
+ TableQuestion: TableComponent,
29
+ TextareaQuestion: TextareaComponent,
30
+ TextQuestion: TextComponent,
10
31
  };
11
32
 
12
33
  /**
@@ -16,17 +37,14 @@ const mapping = {
16
37
  */
17
38
  export default class CfFieldInputComponent extends Component {
18
39
  /**
19
- * The input component type
40
+ * The input component
20
41
  *
21
- * @property {String} type
42
+ * @property {Component} inputComponent
22
43
  * @accessor
23
44
  */
24
- get type() {
45
+ get inputComponent() {
25
46
  const typename = this.args.field?.question.raw.__typename;
26
47
 
27
- return (
28
- typename &&
29
- (mapping[typename] || dasherize(typename.replace(/Question$/, "")))
30
- );
48
+ return ensureSafeComponent(COMPONENT_MAPPING[typename], this);
31
49
  }
32
50
  }
@@ -1,11 +1,13 @@
1
- <label class="uk-form-label" for={{@field.pk}}>
1
+ <label class="uk-form-label" for={{@field.pk}} ...attributes>
2
2
  <span class="uk-text-bold">
3
3
  {{@field.question.raw.label}}
4
4
  </span>
5
5
 
6
- {{#if this.optional}}
6
+ {{#if (and (not @field.question.isCalculated) @field.optional)}}
7
7
  <span
8
8
  class="uk-margin-small-left uk-text-muted uk-text-lowercase uk-text-normal"
9
9
  >({{t "caluma.form.optional"}})</span>
10
10
  {{/if}}
11
+
12
+ {{yield}}
11
13
  </label>
@@ -1,9 +1,24 @@
1
- {{#if this.value.fileAnswerId}}
2
- <UkButton
3
- @color="link"
4
- @label={{this.value.label}}
5
- @on-click={{fn this.download this.value.fileAnswerId}}
6
- />
1
+ {{#if (has-question-type @field.question "choice" "dynamic-choice")}}
2
+ {{@field.selected.label}}
3
+ {{else if (has-question-type
4
+ @field.question "multiple-choice" "multiple-dynamic-choice"
5
+ )}}
6
+ {{#each @field.selected as |opt i|}}{{if (gt i 0) ", "}}{{opt.label}}{{/each}}
7
+ {{else if (has-question-type @field.question "date")}}
8
+ {{format-date
9
+ @field.answer.value
10
+ day="2-digit"
11
+ month="2-digit"
12
+ year="numeric"
13
+ }}
14
+ {{else if (has-question-type @field.question "file")}}
15
+ {{#if @field.answer.value}}
16
+ <UkButton
17
+ @color="link"
18
+ @label={{@field.answer.value.name}}
19
+ @onClick={{fn this.download @field.answer.raw.id}}
20
+ />
21
+ {{/if}}
7
22
  {{else}}
8
- {{this.value.label}}
23
+ {{@field.answer.value}}
9
24
  {{/if}}
@@ -1,50 +1,25 @@
1
1
  import { action } from "@ember/object";
2
2
  import Component from "@glimmer/component";
3
3
  import { queryManager } from "ember-apollo-client";
4
- import moment from "moment";
5
4
 
6
5
  import getFileAnswerInfoQuery from "@projectcaluma/ember-form/gql/queries/fileanswer-info.graphql";
7
6
 
8
7
  export default class CfFieldValueComponent extends Component {
9
8
  @queryManager apollo;
10
9
 
11
- get value() {
12
- const field = this.args.field;
13
-
14
- switch (field.questionType) {
15
- case "ChoiceQuestion":
16
- case "DynamicChoiceQuestion": {
17
- return field.selected;
18
- }
19
- case "MultipleChoiceQuestion":
20
- case "DynamicMultipleChoiceQuestion": {
21
- return { label: field.selected.map(({ label }) => label).join(", ") };
22
- }
23
- case "FileQuestion": {
24
- const answerValue = field.answer.value;
25
- return {
26
- fileAnswerId: answerValue && field.answer.raw.id,
27
- label: answerValue?.name,
28
- };
29
- }
30
- case "DateQuestion": {
31
- return {
32
- label: field.answer.value && moment(field.answer.value).format("L"),
33
- };
34
- }
35
-
36
- default:
37
- return { label: field.answer.value };
38
- }
39
- }
40
-
41
10
  @action
42
11
  async download(id) {
43
12
  const { downloadUrl } = await this.apollo.query(
44
- { query: getFileAnswerInfoQuery, variables: { id } },
13
+ {
14
+ query: getFileAnswerInfoQuery,
15
+ variables: { id },
16
+ fetchPolicy: "network-only",
17
+ },
45
18
  "node.fileValue"
46
19
  );
47
20
 
48
- window.open(downloadUrl, "_blank");
21
+ if (downloadUrl) {
22
+ window.open(downloadUrl, "_blank");
23
+ }
49
24
  }
50
25
  }
@@ -1,12 +1,23 @@
1
1
  {{#if this.visible}}
2
- <div class="uk-margin">
2
+ <div
3
+ class="uk-margin
4
+ {{if
5
+ (and @disabled (has-question-type @field.question 'action-button'))
6
+ 'uk-hidden'
7
+ }}"
8
+ {{did-insert this.registerComponent}}
9
+ {{will-destroy this.unregisterComponent}}
10
+ >
3
11
  {{#if this.labelVisible}}
4
12
  <CfField::label @field={{@field}} />
5
13
  {{/if}}
6
14
 
7
15
  <div class="uk-flex">
8
16
  <div class="uk-width-expand">
9
- {{#let (component (get-widget @field.question)) as |FieldComponent|}}
17
+ {{#let
18
+ (component (ensure-safe-component (get-widget @field.question)))
19
+ as |FieldComponent|
20
+ }}
10
21
  <FieldComponent
11
22
  @field={{@field}}
12
23
  @disabled={{@disabled}}
@@ -24,17 +35,39 @@
24
35
  <div
25
36
  class="cf-field__icon uk-padding-remove-vertical uk-flex uk-flex-middle uk-flex-center"
26
37
  >
27
- {{#if @field.save.isRunning}}
38
+ {{#if this.save.isRunning}}
28
39
  <UkSpinner class="uk-animation-fade" />
29
- {{else if (or @field.save.last.isError @field.isInvalid)}}
30
- <UkIcon @icon="warning" class="uk-animation-fade uk-text-danger" />
31
- {{else if @field.save.last.isSuccessful}}
40
+ {{else if (or this.save.last.isError @field.isInvalid)}}
41
+ <div class="uk-flex-inline">
42
+ <UkIcon
43
+ @icon="warning"
44
+ class="uk-animation-fade uk-text-danger"
45
+ />
46
+ {{#if this.save.last.error}}
47
+ <div uk-dropdown="pos: bottom-left" class="uk-padding-small">
48
+ <div class="uk-alert uk-alert-danger uk-margin-small">
49
+ {{t "caluma.form.error.intro"}}
50
+ </div>
51
+ <p class="uk-text-meta uk-margin-small">
52
+ {{t "caluma.form.error.details"}}
53
+ </p>
54
+ <pre class="uk-margin-remove">
55
+ {{~format-graphql-error this.save.last.error~}}
56
+ </pre>
57
+ </div>
58
+ {{/if}}
59
+ </div>
60
+ {{else if this.save.last.isSuccessful}}
32
61
  <UkIcon @icon="check" class="uk-animation-fade uk-text-success" />
33
62
  {{/if}}
34
63
  </div>
35
64
  {{/if}}
36
65
  </div>
37
66
 
67
+ {{#if (and @field.question.raw.hintText this.hintTextVisible)}}
68
+ <CfField::hint @field={{@field}} />
69
+ {{/if}}
70
+
38
71
  {{#if @field.errors.length}}
39
72
  <CfField::errors @field={{@field}} />
40
73
  {{/if}}
@@ -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
 
@@ -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,45 @@ 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");
54
-
72
+ *save(value, bypassTimeout = false) {
55
73
  /* istanbul ignore next */
56
- if (environment !== "test") {
57
- yield timeout(500);
74
+ if (macroCondition(isTesting())) {
75
+ // no timeout
76
+ } else {
77
+ if (!bypassTimeout) {
78
+ yield timeout(500);
79
+ }
58
80
  }
59
81
 
60
- set(this.args.field.answer, "value", value);
82
+ if (this.args.field.answer) {
83
+ this.args.field.answer.value = value;
84
+ }
61
85
 
62
86
  yield this.args.field.validate.perform();
63
87
 
64
- try {
65
- return yield this.args.field.save.unlinked().perform();
66
- } catch (e) {
67
- // that's ok
68
- }
88
+ return yield this.args.field.save.unlinked().perform();
69
89
  }
70
90
  }
@@ -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
  }}
@@ -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
  }