@projectcaluma/ember-form 11.0.0-beta.9 → 11.0.1

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/addon/components/cf-content.hbs +38 -37
  3. package/addon/components/cf-content.js +7 -3
  4. package/addon/components/cf-field/hint.hbs +5 -0
  5. package/addon/components/cf-field/input/action-button.hbs +23 -18
  6. package/addon/components/cf-field/input/action-button.js +60 -59
  7. package/addon/components/cf-field/input/checkbox.hbs +2 -3
  8. package/addon/components/cf-field/input/date.hbs +22 -25
  9. package/addon/components/cf-field/input/date.js +37 -22
  10. package/addon/components/cf-field/input/files.hbs +38 -0
  11. package/addon/components/cf-field/input/files.js +113 -0
  12. package/addon/components/cf-field/input/powerselect.hbs +27 -29
  13. package/addon/components/cf-field/input/powerselect.js +8 -2
  14. package/addon/components/cf-field/input/radio.hbs +2 -2
  15. package/addon/components/cf-field/input/table.hbs +3 -2
  16. package/addon/components/cf-field/input/table.js +1 -11
  17. package/addon/components/cf-field/input.hbs +8 -21
  18. package/addon/components/cf-field/input.js +32 -14
  19. package/addon/components/cf-field/label.hbs +4 -2
  20. package/addon/components/cf-field-value.hbs +10 -8
  21. package/addon/components/cf-field-value.js +6 -5
  22. package/addon/components/cf-field.hbs +30 -5
  23. package/addon/components/cf-field.js +24 -15
  24. package/addon/components/cf-form-wrapper.hbs +4 -1
  25. package/addon/components/cf-form.hbs +6 -1
  26. package/addon/gql/fragments/field.graphql +14 -7
  27. package/addon/gql/mutations/save-document-files-answer.graphql +9 -0
  28. package/addon/gql/queries/document-forms.graphql +1 -1
  29. package/addon/gql/queries/dynamic-options.graphql +4 -4
  30. package/addon/gql/queries/{fileanswer-info.graphql → filesanswer-info.graphql} +4 -4
  31. package/addon/helpers/format-graphql-error.js +21 -0
  32. package/addon/helpers/get-widget.js +16 -2
  33. package/addon/instance-initializers/form-widget-overrides.js +52 -0
  34. package/addon/lib/document.js +9 -1
  35. package/addon/lib/field.js +49 -46
  36. package/addon/lib/navigation.js +3 -1
  37. package/addon/lib/question.js +18 -5
  38. package/addon/modifiers/autoresize.js +14 -0
  39. package/addon/services/caluma-store.js +2 -0
  40. package/app/components/cf-field/{input/file.js → hint.js} +1 -1
  41. package/app/components/cf-field/input/files.js +1 -0
  42. package/app/helpers/format-graphql-error.js +1 -0
  43. package/app/helpers/get-widget.js +1 -4
  44. package/app/instance-initializers/form-widget-overrides.js +4 -0
  45. package/app/modifiers/autoresize.js +1 -0
  46. package/app/styles/@projectcaluma/ember-form.scss +5 -15
  47. package/app/styles/_flatpickr.scss +47 -0
  48. package/blueprints/@projectcaluma/ember-form/index.js +1 -1
  49. package/index.js +12 -0
  50. package/package.json +49 -40
  51. package/translations/de.yaml +6 -6
  52. package/translations/en.yaml +6 -6
  53. package/translations/fr.yaml +6 -6
  54. package/addon/components/cf-field/input/file.hbs +0 -32
  55. package/addon/components/cf-field/input/file.js +0 -89
  56. package/addon/components/cf-field/label.js +0 -11
  57. package/addon/gql/mutations/remove-answer.graphql +0 -7
  58. package/addon/gql/mutations/save-document-file-answer.graphql +0 -9
  59. package/config/environment.js +0 -5
@@ -0,0 +1,113 @@
1
+ import { action } from "@ember/object";
2
+ import { inject as service } from "@ember/service";
3
+ import { macroCondition, isTesting } from "@embroider/macros";
4
+ import Component from "@glimmer/component";
5
+ import { queryManager } from "ember-apollo-client";
6
+ import fetch from "fetch";
7
+
8
+ import getFilesAnswerInfoQuery from "@projectcaluma/ember-form/gql/queries/filesanswer-info.graphql";
9
+
10
+ export default class CfFieldInputFilesComponent extends Component {
11
+ @service intl;
12
+
13
+ @queryManager apollo;
14
+
15
+ get files() {
16
+ return this.args.field?.answer?.value;
17
+ }
18
+
19
+ @action
20
+ async download(fileId) {
21
+ if (!fileId) {
22
+ return;
23
+ }
24
+ const answers = await this.apollo.query(
25
+ {
26
+ query: getFilesAnswerInfoQuery,
27
+ variables: { id: this.args.field.answer.raw.id },
28
+ fetchPolicy: "network-only",
29
+ },
30
+ "node.value"
31
+ );
32
+ const { downloadUrl } =
33
+ answers.find((file) =>
34
+ // the testing graph-ql setup does a base64 encoding of `__typename: fileID`
35
+ macroCondition(isTesting())
36
+ ? file.id === fileId ||
37
+ atob(file.id).substring(file.__typename.length + 1) === fileId
38
+ : file.id === fileId
39
+ ) ?? {};
40
+ if (downloadUrl) {
41
+ window.open(downloadUrl, "_blank");
42
+ }
43
+ }
44
+
45
+ @action
46
+ async save({ target }) {
47
+ // store the old list of files
48
+ // unwrap files from FileList construct
49
+ let newFiles = Array.from(target.files).map((file) => ({
50
+ name: file.name,
51
+ value: file,
52
+ }));
53
+
54
+ const fileList = [...(this.files || []), ...newFiles];
55
+
56
+ if (newFiles.length === 0) {
57
+ return;
58
+ }
59
+
60
+ // trigger save action for file list of old and new files with
61
+ // reduces properties to match gql format
62
+ const { filesValue: savedAnswerValue } = await this.args.onSave(
63
+ fileList.map(({ name, id }) => ({ name, id }))
64
+ );
65
+
66
+ try {
67
+ // iterate over list of new files and enrich with graphql answer values
68
+ newFiles = newFiles.map((file) => ({
69
+ ...savedAnswerValue.find(
70
+ (value) =>
71
+ file.name === value.name &&
72
+ !fileList.find((file) => file.id === value.id)
73
+ ),
74
+ value: file.value,
75
+ }));
76
+
77
+ const uploadFunction = async (file) => {
78
+ const response = await fetch(file.uploadUrl, {
79
+ method: "PUT",
80
+ body: file.value,
81
+ });
82
+ if (!response.ok) {
83
+ throw new Error();
84
+ }
85
+ return response;
86
+ };
87
+
88
+ // upload the actual file to data storage
89
+ await Promise.all(newFiles.map((file) => uploadFunction(file)));
90
+
91
+ this.args.field.answer.value = savedAnswerValue;
92
+ } catch (error) {
93
+ await this.args.onSave([]);
94
+ this.args.field._errors = [{ type: "uploadFailed" }];
95
+ } finally {
96
+ // eslint-disable-next-line require-atomic-updates
97
+ target.value = "";
98
+ }
99
+ }
100
+
101
+ @action
102
+ async delete(fileId) {
103
+ const remainingFiles = this.files
104
+ .filter((file) => file.id !== fileId)
105
+ .map(({ name, id }) => ({ name, id }));
106
+
107
+ try {
108
+ await this.args.onSave(remainingFiles);
109
+ } catch (error) {
110
+ this.args.field._errors = [{ type: "deleteFailed" }];
111
+ }
112
+ }
113
+ }
@@ -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))}}
@@ -29,7 +29,7 @@
29
29
  @color="link"
30
30
  @onClick={{fn this.edit document}}
31
31
  title={{t "caluma.form.edit"}}
32
- class="uk-flex-inline uk-margin-small-left"
32
+ class="uk-flex-inline uk-margin-small-left table-controls"
33
33
  data-test-edit-row
34
34
  >
35
35
  <UkIcon @icon="pencil" />
@@ -40,7 +40,7 @@
40
40
  @color="link"
41
41
  @onClick={{fn (perform this.delete) document}}
42
42
  title={{t "caluma.form.delete"}}
43
- class="uk-flex-inline uk-margin-small-left"
43
+ class="uk-flex-inline uk-margin-small-left table-controls"
44
44
  data-test-delete-row
45
45
  >
46
46
  <UkIcon @icon="trash" />
@@ -83,6 +83,7 @@
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
 
@@ -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 FilesComponent from "@projectcaluma/ember-form/components/cf-field/input/files";
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
+ FilesQuestion: FilesComponent,
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,8 +1,10 @@
1
1
  {{#if (has-question-type @field.question "choice" "dynamic-choice")}}
2
2
  {{@field.selected.label}}
3
- {{else if (has-question-type
4
- @field.question "multiple-choice" "multiple-dynamic-choice"
5
- )}}
3
+ {{else if
4
+ (has-question-type
5
+ @field.question "multiple-choice" "multiple-dynamic-choice"
6
+ )
7
+ }}
6
8
  {{#each @field.selected as |opt i|}}{{if (gt i 0) ", "}}{{opt.label}}{{/each}}
7
9
  {{else if (has-question-type @field.question "date")}}
8
10
  {{format-date
@@ -11,14 +13,14 @@
11
13
  month="2-digit"
12
14
  year="numeric"
13
15
  }}
14
- {{else if (has-question-type @field.question "file")}}
15
- {{#if @field.answer.value}}
16
+ {{else if (has-question-type @field.question "files")}}
17
+ {{#each @field.answer.value as |file|}}
16
18
  <UkButton
17
19
  @color="link"
18
- @label={{@field.answer.value.name}}
19
- @onClick={{fn this.download @field.answer.raw.id}}
20
+ @label={{file.name}}
21
+ @onClick={{fn this.download file.id}}
20
22
  />
21
- {{/if}}
23
+ {{/each}}
22
24
  {{else}}
23
25
  {{@field.answer.value}}
24
26
  {{/if}}
@@ -2,22 +2,23 @@ import { action } from "@ember/object";
2
2
  import Component from "@glimmer/component";
3
3
  import { queryManager } from "ember-apollo-client";
4
4
 
5
- import getFileAnswerInfoQuery from "@projectcaluma/ember-form/gql/queries/fileanswer-info.graphql";
5
+ import getFilesAnswerInfoQuery from "@projectcaluma/ember-form/gql/queries/filesanswer-info.graphql";
6
6
 
7
7
  export default class CfFieldValueComponent extends Component {
8
8
  @queryManager apollo;
9
9
 
10
10
  @action
11
11
  async download(id) {
12
- const { downloadUrl } = await this.apollo.query(
12
+ const files = await this.apollo.query(
13
13
  {
14
- query: getFileAnswerInfoQuery,
15
- variables: { id },
14
+ query: getFilesAnswerInfoQuery,
15
+ variables: { id: this.args.field.answer.raw.id },
16
16
  fetchPolicy: "network-only",
17
17
  },
18
- "node.fileValue"
18
+ "node.value"
19
19
  );
20
20
 
21
+ const { downloadUrl } = files?.find((file) => file.id === id);
21
22
  if (downloadUrl) {
22
23
  window.open(downloadUrl, "_blank");
23
24
  }
@@ -9,12 +9,15 @@
9
9
  {{will-destroy this.unregisterComponent}}
10
10
  >
11
11
  {{#if this.labelVisible}}
12
- <CfField::label @field={{@field}} />
12
+ <CfField::Label @field={{@field}} />
13
13
  {{/if}}
14
14
 
15
15
  <div class="uk-flex">
16
16
  <div class="uk-width-expand">
17
- {{#let (component (get-widget @field.question)) as |FieldComponent|}}
17
+ {{#let
18
+ (component (ensure-safe-component (get-widget @field.question)))
19
+ as |FieldComponent|
20
+ }}
18
21
  <FieldComponent
19
22
  @field={{@field}}
20
23
  @disabled={{@disabled}}
@@ -25,7 +28,7 @@
25
28
  </div>
26
29
 
27
30
  {{#if (and @field.question.raw.infoText this.infoTextVisible)}}
28
- <CfField::info @text={{@field.question.raw.infoText}} />
31
+ <CfField::Info @text={{@field.question.raw.infoText}} />
29
32
  {{/if}}
30
33
 
31
34
  {{#if this.saveIndicatorVisible}}
@@ -35,7 +38,25 @@
35
38
  {{#if this.save.isRunning}}
36
39
  <UkSpinner class="uk-animation-fade" />
37
40
  {{else if (or this.save.last.isError @field.isInvalid)}}
38
- <UkIcon @icon="warning" class="uk-animation-fade uk-text-danger" />
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>
39
60
  {{else if this.save.last.isSuccessful}}
40
61
  <UkIcon @icon="check" class="uk-animation-fade uk-text-success" />
41
62
  {{/if}}
@@ -43,8 +64,12 @@
43
64
  {{/if}}
44
65
  </div>
45
66
 
67
+ {{#if (and @field.question.raw.hintText this.hintTextVisible)}}
68
+ <CfField::Hint @field={{@field}} />
69
+ {{/if}}
70
+
46
71
  {{#if @field.errors.length}}
47
- <CfField::errors @field={{@field}} />
72
+ <CfField::Errors @field={{@field}} />
48
73
  {{/if}}
49
74
  </div>
50
75
  {{/if}}
@@ -1,5 +1,5 @@
1
- import { getOwner } from "@ember/application";
2
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`.
@@ -46,6 +46,15 @@ export default class CfFieldComponent extends Component {
46
46
  return !hasQuestionType(this.args.field?.question, "action-button");
47
47
  }
48
48
 
49
+ get hintTextVisible() {
50
+ return !hasQuestionType(
51
+ this.args.field?.question,
52
+ "action-button",
53
+ "static",
54
+ "form"
55
+ );
56
+ }
57
+
49
58
  get saveIndicatorVisible() {
50
59
  return !hasQuestionType(this.args.field?.question, "action-button");
51
60
  }
@@ -56,16 +65,22 @@ export default class CfFieldComponent extends Component {
56
65
  * reduce the amount of saved values when changed rapidly.
57
66
  *
58
67
  * @method save
59
- * @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
60
70
  */
61
71
  @restartableTask
62
- *save(value) {
63
- const { environment } =
64
- 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
+ }
65
76
 
66
77
  /* istanbul ignore next */
67
- if (environment !== "test") {
68
- yield timeout(500);
78
+ if (macroCondition(isTesting())) {
79
+ // no timeout
80
+ } else {
81
+ if (!bypassTimeout) {
82
+ yield timeout(500);
83
+ }
69
84
  }
70
85
 
71
86
  if (this.args.field.answer) {
@@ -74,12 +89,6 @@ export default class CfFieldComponent extends Component {
74
89
 
75
90
  yield this.args.field.validate.perform();
76
91
 
77
- try {
78
- // Save the new field value unlinked so the fields save task is not
79
- // aborted when this component is destroyed
80
- return yield this.args.field.save.unlinked().perform();
81
- } catch (e) {
82
- // The component was destroyed before the fields save task was finished
83
- }
92
+ return yield this.args.field.save.unlinked().perform();
84
93
  }
85
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>