@projectcaluma/ember-form 11.0.0-beta.4 → 11.0.0-beta.40

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 (62) hide show
  1. package/CHANGELOG.md +202 -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/static.hbs +1 -1
  16. package/addon/components/cf-field/input/table.hbs +20 -18
  17. package/addon/components/cf-field/input/table.js +1 -11
  18. package/addon/components/cf-field/input.hbs +8 -21
  19. package/addon/components/cf-field/input.js +32 -14
  20. package/addon/components/cf-field/label.hbs +4 -2
  21. package/addon/components/cf-field-value.hbs +22 -7
  22. package/addon/components/cf-field-value.js +12 -36
  23. package/addon/components/cf-field.hbs +42 -9
  24. package/addon/components/cf-field.js +41 -17
  25. package/addon/components/cf-form-wrapper.hbs +4 -1
  26. package/addon/components/cf-form.hbs +6 -1
  27. package/addon/components/document-validity.js +16 -1
  28. package/addon/gql/fragments/field.graphql +32 -7
  29. package/addon/gql/mutations/save-document-files-answer.graphql +9 -0
  30. package/addon/gql/queries/document-forms.graphql +1 -1
  31. package/addon/gql/queries/dynamic-options.graphql +4 -4
  32. package/addon/gql/queries/{fileanswer-info.graphql → filesanswer-info.graphql} +4 -4
  33. package/addon/helpers/format-graphql-error.js +21 -0
  34. package/addon/helpers/get-widget.js +16 -2
  35. package/addon/instance-initializers/form-widget-overrides.js +52 -0
  36. package/addon/lib/document.js +9 -1
  37. package/addon/lib/field.js +96 -59
  38. package/addon/lib/navigation.js +3 -1
  39. package/addon/lib/question.js +18 -5
  40. package/addon/modifiers/autoresize.js +14 -0
  41. package/addon/services/caluma-store.js +2 -0
  42. package/app/components/cf-field/{input/file.js → hint.js} +1 -1
  43. package/app/components/cf-field/input/files.js +1 -0
  44. package/app/helpers/format-graphql-error.js +1 -0
  45. package/app/helpers/get-widget.js +1 -4
  46. package/app/instance-initializers/form-widget-overrides.js +4 -0
  47. package/app/modifiers/autoresize.js +1 -0
  48. package/app/styles/@projectcaluma/ember-form.scss +5 -15
  49. package/app/styles/_flatpickr.scss +47 -0
  50. package/blueprints/@projectcaluma/ember-form/index.js +1 -1
  51. package/index.js +12 -0
  52. package/package.json +47 -37
  53. package/translations/de.yaml +6 -6
  54. package/translations/en.yaml +6 -6
  55. package/translations/fr.yaml +6 -6
  56. package/addon/components/cf-field/input/file.hbs +0 -32
  57. package/addon/components/cf-field/input/file.js +0 -89
  58. package/addon/components/cf-field/label.js +0 -11
  59. package/addon/gql/mutations/remove-answer.graphql +0 -7
  60. package/addon/gql/mutations/save-document-file-answer.graphql +0 -9
  61. package/addon/instance-initializers/setup-pikaday-i18n.js +0 -35
  62. package/config/environment.js +0 -5
@@ -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
  }
@@ -1,8 +1,3 @@
1
- # We can not symlink this file so an exact copy exists in another package:
2
- # packages/form-builder/addon/gql/fragments/field.graphql
3
- #
4
- # When changing this file the other must also receive the same changes.
5
-
6
1
  fragment SimpleQuestion on Question {
7
2
  id
8
3
  slug
@@ -19,6 +14,16 @@ fragment SimpleQuestion on Question {
19
14
  value
20
15
  }
21
16
  placeholder
17
+ formatValidators {
18
+ edges {
19
+ node {
20
+ slug
21
+ regex
22
+ errorMsg
23
+ }
24
+ }
25
+ }
26
+ hintText
22
27
  }
23
28
  ... on TextareaQuestion {
24
29
  textareaMinLength: minLength
@@ -28,6 +33,16 @@ fragment SimpleQuestion on Question {
28
33
  value
29
34
  }
30
35
  placeholder
36
+ formatValidators {
37
+ edges {
38
+ node {
39
+ slug
40
+ regex
41
+ errorMsg
42
+ }
43
+ }
44
+ }
45
+ hintText
31
46
  }
32
47
  ... on IntegerQuestion {
33
48
  integerMinValue: minValue
@@ -37,6 +52,7 @@ fragment SimpleQuestion on Question {
37
52
  value
38
53
  }
39
54
  placeholder
55
+ hintText
40
56
  }
41
57
  ... on FloatQuestion {
42
58
  floatMinValue: minValue
@@ -46,6 +62,7 @@ fragment SimpleQuestion on Question {
46
62
  value
47
63
  }
48
64
  placeholder
65
+ hintText
49
66
  }
50
67
  ... on ChoiceQuestion {
51
68
  choiceOptions: options {
@@ -62,6 +79,7 @@ fragment SimpleQuestion on Question {
62
79
  id
63
80
  value
64
81
  }
82
+ hintText
65
83
  }
66
84
  ... on MultipleChoiceQuestion {
67
85
  multipleChoiceOptions: options {
@@ -78,18 +96,24 @@ fragment SimpleQuestion on Question {
78
96
  id
79
97
  value
80
98
  }
99
+ hintText
81
100
  }
82
101
  ... on DateQuestion {
83
102
  dateDefaultAnswer: defaultAnswer {
84
103
  id
85
104
  value
86
105
  }
106
+ hintText
87
107
  }
88
108
  ... on StaticQuestion {
89
109
  staticContent
90
110
  }
91
111
  ... on CalculatedFloatQuestion {
92
112
  calcExpression
113
+ hintText
114
+ }
115
+ ... on FilesQuestion {
116
+ hintText
93
117
  }
94
118
  ... on ActionButtonQuestion {
95
119
  action
@@ -112,6 +136,7 @@ fragment FieldTableQuestion on Question {
112
136
  }
113
137
  }
114
138
  }
139
+ hintText
115
140
  tableDefaultAnswer: defaultAnswer {
116
141
  id
117
142
  value {
@@ -204,8 +229,8 @@ fragment SimpleAnswer on Answer {
204
229
  ... on ListAnswer {
205
230
  listValue: value
206
231
  }
207
- ... on FileAnswer {
208
- fileValue: value {
232
+ ... on FilesAnswer {
233
+ filesValue: value {
209
234
  id
210
235
  uploadUrl
211
236
  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,11 +1,11 @@
1
- query DynamicOptions($question: String!) {
2
- allQuestions(filter: [{ slug: $question }], first: 1) {
1
+ query DynamicOptions($question: String!, $context: JSONString) {
2
+ allQuestions(filter: [{ slugs: [$question] }], first: 1) {
3
3
  edges {
4
4
  node {
5
5
  id
6
6
  slug
7
7
  ... on DynamicChoiceQuestion {
8
- dynamicChoiceOptions: options {
8
+ dynamicChoiceOptions: options(context: $context) {
9
9
  edges {
10
10
  node {
11
11
  slug
@@ -15,7 +15,7 @@ query DynamicOptions($question: String!) {
15
15
  }
16
16
  }
17
17
  ... on DynamicMultipleChoiceQuestion {
18
- dynamicMultipleChoiceOptions: options {
18
+ dynamicMultipleChoiceOptions: options(context: $context) {
19
19
  edges {
20
20
  node {
21
21
  slug
@@ -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
+ };
@@ -18,7 +18,7 @@ const sum = (nums) => nums.reduce((num, base) => base + num, 0);
18
18
  * @class Document
19
19
  */
20
20
  export default class Document extends Base {
21
- constructor({ raw, parentDocument, ...args }) {
21
+ constructor({ raw, parentDocument, dataSourceContext, ...args }) {
22
22
  assert(
23
23
  "A graphql document `raw` must be passed",
24
24
  raw?.__typename === "Document"
@@ -27,6 +27,7 @@ export default class Document extends Base {
27
27
  super({ raw, ...args });
28
28
 
29
29
  this.parentDocument = parentDocument;
30
+ this.dataSourceContext = dataSourceContext;
30
31
 
31
32
  this.pushIntoStore();
32
33
 
@@ -83,6 +84,13 @@ export default class Document extends Base {
83
84
  */
84
85
  fieldsets = [];
85
86
 
87
+ /**
88
+ * Context object for data sources
89
+ *
90
+ * @property {Object} dataSourceContext
91
+ */
92
+ dataSourceContext = null;
93
+
86
94
  /**
87
95
  * The primary key of the document.
88
96
  *