@projectcaluma/ember-form 11.0.0-beta.8 → 11.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. package/CHANGELOG.md +162 -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 +8 -4
  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 +12 -25
  9. package/addon/components/cf-field/input/date.js +19 -11
  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 +24 -7
  21. package/addon/components/cf-field-value.js +12 -36
  22. package/addon/components/cf-field.hbs +35 -6
  23. package/addon/components/cf-field.js +25 -20
  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/addon/instance-initializers/setup-pikaday-i18n.js +0 -35
  60. 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,9 +1,26 @@
1
- {{#if this.value.fileAnswerId}}
2
- <UkButton
3
- @color="link"
4
- @label={{this.value.label}}
5
- @onClick={{fn this.download this.value.fileAnswerId}}
6
- />
1
+ {{#if (has-question-type @field.question "choice" "dynamic-choice")}}
2
+ {{@field.selected.label}}
3
+ {{else if
4
+ (has-question-type
5
+ @field.question "multiple-choice" "multiple-dynamic-choice"
6
+ )
7
+ }}
8
+ {{#each @field.selected as |opt i|}}{{if (gt i 0) ", "}}{{opt.label}}{{/each}}
9
+ {{else if (has-question-type @field.question "date")}}
10
+ {{format-date
11
+ @field.answer.value
12
+ day="2-digit"
13
+ month="2-digit"
14
+ year="numeric"
15
+ }}
16
+ {{else if (has-question-type @field.question "files")}}
17
+ {{#each @field.answer.value as |file|}}
18
+ <UkButton
19
+ @color="link"
20
+ @label={{file.name}}
21
+ @onClick={{fn this.download file.id}}
22
+ />
23
+ {{/each}}
7
24
  {{else}}
8
- {{this.value.label}}
25
+ {{@field.answer.value}}
9
26
  {{/if}}
@@ -1,50 +1,26 @@
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
- import getFileAnswerInfoQuery from "@projectcaluma/ember-form/gql/queries/fileanswer-info.graphql";
5
+ import getFilesAnswerInfoQuery from "@projectcaluma/ember-form/gql/queries/filesanswer-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
- const { downloadUrl } = await this.apollo.query(
44
- { query: getFileAnswerInfoQuery, variables: { id } },
45
- "node.fileValue"
12
+ const files = await this.apollo.query(
13
+ {
14
+ query: getFilesAnswerInfoQuery,
15
+ variables: { id: this.args.field.answer.raw.id },
16
+ fetchPolicy: "network-only",
17
+ },
18
+ "node.value"
46
19
  );
47
20
 
48
- window.open(downloadUrl, "_blank");
21
+ const { downloadUrl } = files?.find((file) => file.id === id);
22
+ if (downloadUrl) {
23
+ window.open(downloadUrl, "_blank");
24
+ }
49
25
  }
50
26
  }
@@ -1,16 +1,23 @@
1
1
  {{#if this.visible}}
2
2
  <div
3
- class="uk-margin"
3
+ class="uk-margin
4
+ {{if
5
+ (and @disabled (has-question-type @field.question 'action-button'))
6
+ 'uk-hidden'
7
+ }}"
4
8
  {{did-insert this.registerComponent}}
5
9
  {{will-destroy this.unregisterComponent}}
6
10
  >
7
11
  {{#if this.labelVisible}}
8
- <CfField::label @field={{@field}} />
12
+ <CfField::Label @field={{@field}} />
9
13
  {{/if}}
10
14
 
11
15
  <div class="uk-flex">
12
16
  <div class="uk-width-expand">
13
- {{#let (component (get-widget @field.question)) as |FieldComponent|}}
17
+ {{#let
18
+ (component (ensure-safe-component (get-widget @field.question)))
19
+ as |FieldComponent|
20
+ }}
14
21
  <FieldComponent
15
22
  @field={{@field}}
16
23
  @disabled={{@disabled}}
@@ -21,7 +28,7 @@
21
28
  </div>
22
29
 
23
30
  {{#if (and @field.question.raw.infoText this.infoTextVisible)}}
24
- <CfField::info @text={{@field.question.raw.infoText}} />
31
+ <CfField::Info @text={{@field.question.raw.infoText}} />
25
32
  {{/if}}
26
33
 
27
34
  {{#if this.saveIndicatorVisible}}
@@ -31,7 +38,25 @@
31
38
  {{#if this.save.isRunning}}
32
39
  <UkSpinner class="uk-animation-fade" />
33
40
  {{else if (or this.save.last.isError @field.isInvalid)}}
34
- <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>
35
60
  {{else if this.save.last.isSuccessful}}
36
61
  <UkIcon @icon="check" class="uk-animation-fade uk-text-success" />
37
62
  {{/if}}
@@ -39,8 +64,12 @@
39
64
  {{/if}}
40
65
  </div>
41
66
 
67
+ {{#if (and @field.question.raw.hintText this.hintTextVisible)}}
68
+ <CfField::Hint @field={{@field}} />
69
+ {{/if}}
70
+
42
71
  {{#if @field.errors.length}}
43
- <CfField::errors @field={{@field}} />
72
+ <CfField::Errors @field={{@field}} />
44
73
  {{/if}}
45
74
  </div>
46
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`.
@@ -31,11 +31,7 @@ export default class CfFieldComponent extends Component {
31
31
  get visible() {
32
32
  return (
33
33
  !this.args.field?.hidden &&
34
- !hasQuestionType(this.args.field?.question, "form") &&
35
- !(
36
- hasQuestionType(this.args.field?.question, "action-button") &&
37
- this.args.disabled
38
- )
34
+ !hasQuestionType(this.args.field?.question, "form")
39
35
  );
40
36
  }
41
37
 
@@ -50,6 +46,15 @@ export default class CfFieldComponent extends Component {
50
46
  return !hasQuestionType(this.args.field?.question, "action-button");
51
47
  }
52
48
 
49
+ get hintTextVisible() {
50
+ return !hasQuestionType(
51
+ this.args.field?.question,
52
+ "action-button",
53
+ "static",
54
+ "form"
55
+ );
56
+ }
57
+
53
58
  get saveIndicatorVisible() {
54
59
  return !hasQuestionType(this.args.field?.question, "action-button");
55
60
  }
@@ -60,16 +65,22 @@ export default class CfFieldComponent extends Component {
60
65
  * reduce the amount of saved values when changed rapidly.
61
66
  *
62
67
  * @method save
63
- * @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
64
70
  */
65
71
  @restartableTask
66
- *save(value) {
67
- const { environment } =
68
- 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
+ }
69
76
 
70
77
  /* istanbul ignore next */
71
- if (environment !== "test") {
72
- yield timeout(500);
78
+ if (macroCondition(isTesting())) {
79
+ // no timeout
80
+ } else {
81
+ if (!bypassTimeout) {
82
+ yield timeout(500);
83
+ }
73
84
  }
74
85
 
75
86
  if (this.args.field.answer) {
@@ -78,12 +89,6 @@ export default class CfFieldComponent extends Component {
78
89
 
79
90
  yield this.args.field.validate.perform();
80
91
 
81
- try {
82
- // Save the new field value unlinked so the fields save task is not
83
- // aborted when this component is destroyed
84
- return yield this.args.field.save.unlinked().perform();
85
- } catch (e) {
86
- // The component was destroyed before the fields save task was finished
87
- }
92
+ return yield this.args.field.save.unlinked().perform();
88
93
  }
89
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>