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

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 +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
  *