@projectcaluma/ember-form 10.0.0 → 11.0.0-beta.1

Sign up to get free protection for your applications and to get access to all the features.
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
+ }