@projectcaluma/ember-form 10.0.0 → 11.0.0-beta.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 (41) hide show
  1. package/addon/components/cf-content.hbs +36 -39
  2. package/addon/components/cf-content.js +47 -20
  3. package/addon/components/cf-field/input/action-button.hbs +1 -1
  4. package/addon/components/cf-field/input/action-button.js +9 -7
  5. package/addon/components/cf-field/input/checkbox.hbs +2 -2
  6. package/addon/components/cf-field/input/checkbox.js +9 -29
  7. package/addon/components/cf-field/input/file.js +8 -9
  8. package/addon/components/cf-field/input/float.hbs +4 -4
  9. package/addon/components/cf-field/input/integer.hbs +5 -5
  10. package/addon/components/cf-field/input/table.js +12 -10
  11. package/addon/components/cf-field/input/text.hbs +5 -5
  12. package/addon/components/cf-field/input/textarea.hbs +5 -5
  13. package/addon/components/cf-field/input.js +1 -1
  14. package/addon/components/cf-field/label.hbs +1 -1
  15. package/addon/components/cf-field-value.js +8 -13
  16. package/addon/components/cf-field.hbs +2 -2
  17. package/addon/components/cf-field.js +2 -3
  18. package/addon/components/cf-navigation-item.hbs +2 -2
  19. package/addon/components/document-validity.js +1 -1
  20. package/addon/gql/fragments/field.graphql +27 -0
  21. package/addon/gql/mutations/save-document-table-answer.graphql +1 -1
  22. package/addon/gql/mutations/save-document.graphql +1 -0
  23. package/addon/gql/queries/{get-document-answers.graphql → document-answers.graphql} +2 -1
  24. package/addon/gql/queries/{get-document-forms.graphql → document-forms.graphql} +2 -1
  25. package/addon/gql/queries/{get-document-used-dynamic-options.graphql → document-used-dynamic-options.graphql} +2 -1
  26. package/addon/gql/queries/{get-dynamic-options.graphql → dynamic-options.graphql} +2 -1
  27. package/addon/gql/queries/{get-fileanswer-info.graphql → fileanswer-info.graphql} +2 -1
  28. package/addon/helpers/get-widget.js +50 -0
  29. package/addon/lib/answer.js +108 -72
  30. package/addon/lib/base.js +32 -23
  31. package/addon/lib/dependencies.js +36 -71
  32. package/addon/lib/document.js +92 -96
  33. package/addon/lib/field.js +334 -401
  34. package/addon/lib/fieldset.js +46 -47
  35. package/addon/lib/form.js +27 -15
  36. package/addon/lib/navigation.js +187 -181
  37. package/addon/lib/question.js +103 -94
  38. package/addon/services/caluma-store.js +10 -6
  39. package/app/helpers/get-widget.js +4 -0
  40. package/package.json +19 -18
  41. package/CHANGELOG.md +0 -21
@@ -1,4 +1,10 @@
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
+
1
6
  fragment SimpleQuestion on Question {
7
+ id
2
8
  slug
3
9
  label
4
10
  isRequired
@@ -9,6 +15,7 @@ fragment SimpleQuestion on Question {
9
15
  textMinLength: minLength
10
16
  textMaxLength: maxLength
11
17
  textDefaultAnswer: defaultAnswer {
18
+ id
12
19
  value
13
20
  }
14
21
  placeholder
@@ -17,6 +24,7 @@ fragment SimpleQuestion on Question {
17
24
  textareaMinLength: minLength
18
25
  textareaMaxLength: maxLength
19
26
  textareaDefaultAnswer: defaultAnswer {
27
+ id
20
28
  value
21
29
  }
22
30
  placeholder
@@ -25,6 +33,7 @@ fragment SimpleQuestion on Question {
25
33
  integerMinValue: minValue
26
34
  integerMaxValue: maxValue
27
35
  integerDefaultAnswer: defaultAnswer {
36
+ id
28
37
  value
29
38
  }
30
39
  placeholder
@@ -33,6 +42,7 @@ fragment SimpleQuestion on Question {
33
42
  floatMinValue: minValue
34
43
  floatMaxValue: maxValue
35
44
  floatDefaultAnswer: defaultAnswer {
45
+ id
36
46
  value
37
47
  }
38
48
  placeholder
@@ -41,6 +51,7 @@ fragment SimpleQuestion on Question {
41
51
  choiceOptions: options {
42
52
  edges {
43
53
  node {
54
+ id
44
55
  slug
45
56
  label
46
57
  isArchived
@@ -48,6 +59,7 @@ fragment SimpleQuestion on Question {
48
59
  }
49
60
  }
50
61
  choiceDefaultAnswer: defaultAnswer {
62
+ id
51
63
  value
52
64
  }
53
65
  }
@@ -55,6 +67,7 @@ fragment SimpleQuestion on Question {
55
67
  multipleChoiceOptions: options {
56
68
  edges {
57
69
  node {
70
+ id
58
71
  slug
59
72
  label
60
73
  isArchived
@@ -62,11 +75,13 @@ fragment SimpleQuestion on Question {
62
75
  }
63
76
  }
64
77
  multipleChoiceDefaultAnswer: defaultAnswer {
78
+ id
65
79
  value
66
80
  }
67
81
  }
68
82
  ... on DateQuestion {
69
83
  dateDefaultAnswer: defaultAnswer {
84
+ id
70
85
  value
71
86
  }
72
87
  }
@@ -84,8 +99,10 @@ fragment SimpleQuestion on Question {
84
99
  }
85
100
 
86
101
  fragment FieldTableQuestion on Question {
102
+ id
87
103
  ... on TableQuestion {
88
104
  rowForm {
105
+ id
89
106
  slug
90
107
  questions {
91
108
  edges {
@@ -96,6 +113,7 @@ fragment FieldTableQuestion on Question {
96
113
  }
97
114
  }
98
115
  tableDefaultAnswer: defaultAnswer {
116
+ id
99
117
  value {
100
118
  id
101
119
  answers {
@@ -103,6 +121,7 @@ fragment FieldTableQuestion on Question {
103
121
  node {
104
122
  id
105
123
  question {
124
+ id
106
125
  slug
107
126
  }
108
127
  ... on StringAnswer {
@@ -129,10 +148,12 @@ fragment FieldTableQuestion on Question {
129
148
  }
130
149
 
131
150
  fragment FieldQuestion on Question {
151
+ id
132
152
  ...SimpleQuestion
133
153
  ...FieldTableQuestion
134
154
  ... on FormQuestion {
135
155
  subForm {
156
+ id
136
157
  slug
137
158
  name
138
159
  questions {
@@ -140,10 +161,12 @@ fragment FieldQuestion on Question {
140
161
  node {
141
162
  # This part here limits our query to 2 level deep nested forms. This
142
163
  # has to be solved in another way!
164
+ id
143
165
  ...SimpleQuestion
144
166
  ...FieldTableQuestion
145
167
  ... on FormQuestion {
146
168
  subForm {
169
+ id
147
170
  slug
148
171
  name
149
172
  questions {
@@ -166,6 +189,7 @@ fragment FieldQuestion on Question {
166
189
  fragment SimpleAnswer on Answer {
167
190
  id
168
191
  question {
192
+ id
169
193
  slug
170
194
  }
171
195
  ... on StringAnswer {
@@ -182,6 +206,7 @@ fragment SimpleAnswer on Answer {
182
206
  }
183
207
  ... on FileAnswer {
184
208
  fileValue: value {
209
+ id
185
210
  uploadUrl
186
211
  downloadUrl
187
212
  metadata
@@ -194,11 +219,13 @@ fragment SimpleAnswer on Answer {
194
219
  }
195
220
 
196
221
  fragment FieldAnswer on Answer {
222
+ id
197
223
  ...SimpleAnswer
198
224
  ... on TableAnswer {
199
225
  tableValue: value {
200
226
  id
201
227
  form {
228
+ id
202
229
  slug
203
230
  questions {
204
231
  edges {
@@ -1,6 +1,6 @@
1
1
  #import * from '../fragments/field.graphql'
2
2
 
3
- mutation saveDocumentTableAnswer($input: SaveDocumentTableAnswerInput!) {
3
+ mutation SaveDocumentTableAnswer($input: SaveDocumentTableAnswerInput!) {
4
4
  saveDocumentTableAnswer(input: $input) {
5
5
  answer {
6
6
  ...FieldAnswer
@@ -12,6 +12,7 @@ mutation SaveDocument($input: SaveDocumentInput!) {
12
12
  }
13
13
  }
14
14
  form {
15
+ id
15
16
  slug
16
17
  questions {
17
18
  edges {
@@ -1,11 +1,12 @@
1
1
  #import * from '../fragments/field.graphql'
2
2
 
3
- query GetDocumentAnswers($id: ID!) {
3
+ query DocumentAnswers($id: ID!) {
4
4
  allDocuments(filter: [{ id: $id }]) {
5
5
  edges {
6
6
  node {
7
7
  id
8
8
  form {
9
+ id
9
10
  slug
10
11
  }
11
12
  workItem {
@@ -1,9 +1,10 @@
1
1
  #import FieldQuestion, FieldTableQuestion, SimpleQuestion from '../fragments/field.graphql'
2
2
 
3
- query GetDocumentForms($slug: String!) {
3
+ query DocumentForms($slug: String!) {
4
4
  allForms(filter: [{ slug: $slug }]) {
5
5
  edges {
6
6
  node {
7
+ id
7
8
  slug
8
9
  name
9
10
  meta
@@ -1,9 +1,10 @@
1
- query GetDocumentUsedDynamicOptions($document: ID!, $question: ID!) {
1
+ query DocumentUsedDynamicOptions($document: ID!, $question: ID!) {
2
2
  allUsedDynamicOptions(
3
3
  filter: [{ document: $document }, { question: $question }]
4
4
  ) {
5
5
  edges {
6
6
  node {
7
+ id
7
8
  slug
8
9
  label
9
10
  }
@@ -1,7 +1,8 @@
1
- query GetDynamicOptions($question: String!) {
1
+ query DynamicOptions($question: String!) {
2
2
  allQuestions(filter: [{ slug: $question }], first: 1) {
3
3
  edges {
4
4
  node {
5
+ id
5
6
  slug
6
7
  ... on DynamicChoiceQuestion {
7
8
  dynamicChoiceOptions: options {
@@ -1,8 +1,9 @@
1
- query GetFileAnswerInfo($id: ID!) {
1
+ query FileAnswerInfo($id: ID!) {
2
2
  node(id: $id) {
3
3
  id
4
4
  ... on FileAnswer {
5
5
  fileValue: value {
6
+ id
6
7
  uploadUrl
7
8
  downloadUrl
8
9
  metadata
@@ -0,0 +1,50 @@
1
+ import Helper from "@ember/component/helper";
2
+ import { warn } from "@ember/debug";
3
+ import { inject as service } from "@ember/service";
4
+
5
+ /**
6
+ * Helper for getting the right widget.
7
+ *
8
+ * This helper expects n objects as positional parameters. It checks if the
9
+ * object has a widget override in it's metadata. If one exists it checks if
10
+ * said widget was registered in the caluma options service and then returns
11
+ * the widget name. If it doesn't have a valid widget, the next object will be
12
+ * checked. If no object returns a valid widget, the passed default widget will
13
+ * be used.
14
+ *
15
+ * ```hbs
16
+ * {{component (get-widget field.question someobject default="cf-form") foo=bar}}
17
+ * ```
18
+ *
19
+ * @function getWidget
20
+ * @param {Array} params
21
+ * @param {Object} [options]
22
+ * @param {String} [options.default]
23
+ */
24
+ export default class GetWidgetHelper extends Helper {
25
+ @service calumaOptions;
26
+
27
+ compute(params, { default: defaultWidget = "cf-field/input" }) {
28
+ for (const obj of params) {
29
+ const widget = obj?.raw?.meta?.widgetOverride;
30
+ if (!widget) {
31
+ continue;
32
+ }
33
+ const override =
34
+ widget &&
35
+ this.calumaOptions
36
+ .getComponentOverrides()
37
+ .find(({ component }) => component === widget);
38
+
39
+ warn(
40
+ `Widget override "${widget}" is not registered. Please register it by calling \`calumaOptions.registerComponentOverride\``,
41
+ override,
42
+ { id: "ember-caluma.unregistered-override" }
43
+ );
44
+
45
+ if (override) return widget;
46
+ }
47
+
48
+ return defaultWidget;
49
+ }
50
+ }
@@ -1,130 +1,166 @@
1
1
  import { getOwner } from "@ember/application";
2
2
  import { assert } from "@ember/debug";
3
- import { computed } from "@ember/object";
4
- import { inject as service } from "@ember/service";
5
3
  import { camelize } from "@ember/string";
6
4
  import { isEmpty } from "@ember/utils";
5
+ import { dedupeTracked, cached } from "tracked-toolbox";
7
6
 
8
7
  import { decodeId } from "@projectcaluma/ember-core/helpers/decode-id";
9
8
  import Base from "@projectcaluma/ember-form/lib/base";
10
9
  import { parseDocument } from "@projectcaluma/ember-form/lib/parsers";
11
10
 
11
+ /**
12
+ * Class that automatically defines all keys of the passed object as deduped
13
+ * tracked property and assigns the initial value.
14
+ *
15
+ * @class DedupedTrackedObject
16
+ * @private
17
+ */
18
+ class DedupedTrackedObject {
19
+ constructor(obj) {
20
+ Object.entries(obj).forEach(([key, value]) => {
21
+ Object.defineProperty(
22
+ this,
23
+ key,
24
+ dedupeTracked(this, key, { initializer: () => value })
25
+ );
26
+ });
27
+ }
28
+ }
29
+
12
30
  /**
13
31
  * Object which represents an answer in context of a field
14
32
  *
15
33
  * @class Answer
16
34
  */
17
- export default Base.extend({
18
- calumaStore: service(),
35
+ export default class Answer extends Base {
36
+ constructor({ raw, field, ...args }) {
37
+ assert("`field` must be passed as an argument", field);
19
38
 
20
- init(...args) {
21
39
  assert(
22
40
  "A graphql answer `raw` must be passed",
23
- this.raw && /Answer$/.test(this.raw.__typename)
41
+ /Answer$/.test(raw?.__typename)
24
42
  );
25
43
 
26
- if (this.raw.id) {
27
- this.set("pk", `Answer:${decodeId(this.raw.id)}`);
28
- }
44
+ super({ raw, ...args });
45
+
46
+ this.field = field;
47
+ this.raw = new DedupedTrackedObject(raw);
29
48
 
30
- this._super(...args);
49
+ this.pushIntoStore();
50
+ }
51
+
52
+ /**
53
+ * The field this answer originates from
54
+ *
55
+ * @property {Field} field
56
+ */
57
+ field = null;
58
+
59
+ /**
60
+ * The raw data of the answer. This is the only `raw` property that is tracked
61
+ * since only the answers properties are changed while rendering the form.
62
+ *
63
+ * @property {DedupedTrackedObject} raw
64
+ */
65
+ raw = {};
31
66
 
32
- this.setProperties(this.raw);
33
- },
67
+ /**
68
+ * The primary key of the answer.
69
+ *
70
+ * @property {String} pk
71
+ */
72
+ @cached
73
+ get pk() {
74
+ return this.uuid && `Answer:${this.uuid}`;
75
+ }
34
76
 
35
77
  /**
36
78
  * The uuid of the answer
37
79
  *
38
80
  * @property {String} uuid
39
- * @accessor
40
81
  */
41
- uuid: computed("raw.id", function () {
82
+ @cached
83
+ get uuid() {
42
84
  return this.raw.id ? decodeId(this.raw.id) : null;
43
- }),
85
+ }
44
86
 
45
- isNew: computed("uuid", "value", function () {
87
+ /**
88
+ * Whether the answer is new. This is true when there is no object from the
89
+ * backend or the value is empty.
90
+ *
91
+ * @property {Boolean} isNew
92
+ */
93
+ @cached
94
+ get isNew() {
46
95
  return !this.uuid || isEmpty(this.value);
47
- }),
96
+ }
48
97
 
49
98
  /**
50
99
  * The name of the property in which the value is stored. This depends on the
51
100
  * type of the answer.
52
101
  *
53
- *
54
102
  * @property {String} _valueKey
55
- * @accessor
56
103
  * @private
57
104
  */
58
- _valueKey: computed("__typename", function () {
105
+ @cached
106
+ get _valueKey() {
59
107
  return (
60
- this.__typename && camelize(this.__typename.replace(/Answer$/, "Value"))
108
+ this.raw.__typename &&
109
+ camelize(this.raw.__typename.replace(/Answer$/, "Value"))
61
110
  );
62
- }),
111
+ }
63
112
 
64
113
  /**
65
114
  * The value of the answer, the type of this value depends on the type of the
66
115
  * answer. For table answers this returns an array of documents.
67
116
  *
68
117
  * @property {String|Number|String[]|Document[]} value
69
- * @computed
70
118
  */
71
- value: computed(
72
- "field.document",
73
- "_valueKey",
74
- "dateValue",
75
- "fileValue",
76
- "floatValue",
77
- "integerValue",
78
- "listValue.[]",
79
- "stringValue",
80
- "tableValue.[]",
81
- {
82
- get() {
83
- const value = this.get(this._valueKey);
84
-
85
- if (this._valueKey === "tableValue" && value) {
86
- return value.map((document) => {
87
- const existing = this.calumaStore.find(
88
- `Document:${decodeId(document.id)}`
89
- );
90
-
91
- return (
92
- existing ||
93
- getOwner(this)
94
- .factoryFor("caluma-model:document")
95
- .create({
96
- raw: parseDocument(document),
97
- parentDocument: this.field.document,
98
- })
99
- );
100
- });
101
- }
102
-
103
- return value;
104
- },
105
- set(_, value) {
106
- value = [undefined, ""].includes(value) ? null : value;
107
-
108
- if (this._valueKey) {
109
- this.set(this._valueKey, value);
110
- }
111
-
112
- return value;
113
- },
119
+ @cached
120
+ get value() {
121
+ const value = this.raw[this._valueKey];
122
+
123
+ if (this._valueKey === "tableValue" && value) {
124
+ const owner = getOwner(this);
125
+ const Document = owner.factoryFor("caluma-model:document").class;
126
+
127
+ return value.map((document) => {
128
+ if (document instanceof Document) return document;
129
+
130
+ const existing = this.calumaStore.find(
131
+ `Document:${decodeId(document.id)}`
132
+ );
133
+
134
+ return (
135
+ existing ||
136
+ new Document({
137
+ raw: parseDocument(document),
138
+ parentDocument: this.field.document,
139
+ owner,
140
+ })
141
+ );
142
+ });
143
+ }
144
+
145
+ return value;
146
+ }
147
+
148
+ set value(value) {
149
+ if (this._valueKey) {
150
+ this.raw[this._valueKey] = [undefined, ""].includes(value) ? null : value;
114
151
  }
115
- ),
152
+ }
116
153
 
117
154
  /**
118
155
  * The value serialized for a backend request.
119
156
  *
120
157
  * @property {String|Number|String[]} serializedValue
121
- * @accessor
122
158
  */
123
- serializedValue: computed("__typename", "value", function () {
124
- if (this.__typename === "TableAnswer") {
159
+ get serializedValue() {
160
+ if (this.raw.__typename === "TableAnswer") {
125
161
  return (this.value || []).map(({ uuid }) => uuid);
126
162
  }
127
163
 
128
164
  return this.value;
129
- }),
130
- });
165
+ }
166
+ }
package/addon/lib/base.js CHANGED
@@ -1,35 +1,44 @@
1
- import { getOwner } from "@ember/application";
1
+ import { setOwner } from "@ember/application";
2
2
  import { assert } from "@ember/debug";
3
- import EmberObject from "@ember/object";
3
+ import { registerDestructor } from "@ember/destroyable";
4
4
  import { inject as service } from "@ember/service";
5
5
 
6
- export default EmberObject.extend({
7
- calumaStore: service(),
6
+ export default class Base {
7
+ @service calumaStore;
8
8
 
9
- init(...args) {
10
- this._super(...args);
9
+ constructor({ raw, owner }) {
10
+ assert("`owner` must be passed as an argument", owner);
11
11
 
12
- assert("Owner must be injected", getOwner(this));
12
+ assert("A primary key `pk` must be defined on the object", "pk" in this);
13
13
 
14
- // answers don't need a pk if they are new
15
- if (!/Answer$/.test(this.get("raw.__typename"))) {
16
- assert("A primary key `pk` must be passed", this.pk);
17
- assert(
18
- "The primary key `pk` must be readonly",
19
- !Object.getOwnPropertyDescriptor(this, "pk").writable
20
- );
21
- }
14
+ setOwner(this, owner);
22
15
 
23
- if (this.pk) {
24
- this.calumaStore.push(this);
16
+ if (raw) {
17
+ this.raw = raw;
25
18
  }
26
- },
27
19
 
28
- willDestroy(...args) {
29
- this._super(...args);
20
+ registerDestructor(this, () => {
21
+ if (this.pk) {
22
+ this.calumaStore.delete(this.pk);
23
+ }
24
+ });
25
+ }
30
26
 
27
+ /**
28
+ * The raw data of the object
29
+ *
30
+ * @property {Object} raw
31
+ */
32
+ raw = {};
33
+
34
+ /**
35
+ * Push the object into the caluma store
36
+ *
37
+ * @method pushIntoStore
38
+ */
39
+ pushIntoStore() {
31
40
  if (this.pk) {
32
- this.calumaStore.delete(this.pk);
41
+ this.calumaStore.push(this);
33
42
  }
34
- },
35
- });
43
+ }
44
+ }