@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.
- package/addon/components/cf-content.hbs +36 -39
- package/addon/components/cf-content.js +47 -20
- package/addon/components/cf-field/input/action-button.hbs +1 -1
- package/addon/components/cf-field/input/action-button.js +9 -7
- package/addon/components/cf-field/input/checkbox.hbs +2 -2
- package/addon/components/cf-field/input/checkbox.js +9 -29
- package/addon/components/cf-field/input/file.js +8 -9
- package/addon/components/cf-field/input/float.hbs +4 -4
- package/addon/components/cf-field/input/integer.hbs +5 -5
- package/addon/components/cf-field/input/table.js +12 -10
- package/addon/components/cf-field/input/text.hbs +5 -5
- package/addon/components/cf-field/input/textarea.hbs +5 -5
- package/addon/components/cf-field/input.js +1 -1
- package/addon/components/cf-field/label.hbs +1 -1
- package/addon/components/cf-field-value.js +8 -13
- package/addon/components/cf-field.hbs +2 -2
- package/addon/components/cf-field.js +2 -3
- package/addon/components/cf-navigation-item.hbs +2 -2
- package/addon/components/document-validity.js +1 -1
- package/addon/gql/fragments/field.graphql +27 -0
- package/addon/gql/mutations/save-document-table-answer.graphql +1 -1
- package/addon/gql/mutations/save-document.graphql +1 -0
- package/addon/gql/queries/{get-document-answers.graphql → document-answers.graphql} +2 -1
- package/addon/gql/queries/{get-document-forms.graphql → document-forms.graphql} +2 -1
- package/addon/gql/queries/{get-document-used-dynamic-options.graphql → document-used-dynamic-options.graphql} +2 -1
- package/addon/gql/queries/{get-dynamic-options.graphql → dynamic-options.graphql} +2 -1
- package/addon/gql/queries/{get-fileanswer-info.graphql → fileanswer-info.graphql} +2 -1
- package/addon/helpers/get-widget.js +50 -0
- package/addon/lib/answer.js +108 -72
- package/addon/lib/base.js +32 -23
- package/addon/lib/dependencies.js +36 -71
- package/addon/lib/document.js +92 -96
- package/addon/lib/field.js +334 -401
- package/addon/lib/fieldset.js +46 -47
- package/addon/lib/form.js +27 -15
- package/addon/lib/navigation.js +187 -181
- package/addon/lib/question.js +103 -94
- package/addon/services/caluma-store.js +10 -6
- package/app/helpers/get-widget.js +4 -0
- package/package.json +19 -18
- 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
|
3
|
+
mutation SaveDocumentTableAnswer($input: SaveDocumentTableAnswerInput!) {
|
4
4
|
saveDocumentTableAnswer(input: $input) {
|
5
5
|
answer {
|
6
6
|
...FieldAnswer
|
@@ -1,9 +1,10 @@
|
|
1
1
|
#import FieldQuestion, FieldTableQuestion, SimpleQuestion from '../fragments/field.graphql'
|
2
2
|
|
3
|
-
query
|
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
|
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
|
}
|
@@ -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
|
+
}
|
package/addon/lib/answer.js
CHANGED
@@ -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
|
18
|
-
|
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
|
-
|
41
|
+
/Answer$/.test(raw?.__typename)
|
24
42
|
);
|
25
43
|
|
26
|
-
|
27
|
-
|
28
|
-
|
44
|
+
super({ raw, ...args });
|
45
|
+
|
46
|
+
this.field = field;
|
47
|
+
this.raw = new DedupedTrackedObject(raw);
|
29
48
|
|
30
|
-
this.
|
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
|
-
|
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
|
-
|
82
|
+
@cached
|
83
|
+
get uuid() {
|
42
84
|
return this.raw.id ? decodeId(this.raw.id) : null;
|
43
|
-
}
|
85
|
+
}
|
44
86
|
|
45
|
-
|
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
|
-
|
105
|
+
@cached
|
106
|
+
get _valueKey() {
|
59
107
|
return (
|
60
|
-
this.__typename &&
|
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
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
"
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
)
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
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 {
|
1
|
+
import { setOwner } from "@ember/application";
|
2
2
|
import { assert } from "@ember/debug";
|
3
|
-
import
|
3
|
+
import { registerDestructor } from "@ember/destroyable";
|
4
4
|
import { inject as service } from "@ember/service";
|
5
5
|
|
6
|
-
export default
|
7
|
-
calumaStore
|
6
|
+
export default class Base {
|
7
|
+
@service calumaStore;
|
8
8
|
|
9
|
-
|
10
|
-
|
9
|
+
constructor({ raw, owner }) {
|
10
|
+
assert("`owner` must be passed as an argument", owner);
|
11
11
|
|
12
|
-
assert("
|
12
|
+
assert("A primary key `pk` must be defined on the object", "pk" in this);
|
13
13
|
|
14
|
-
|
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 (
|
24
|
-
this.
|
16
|
+
if (raw) {
|
17
|
+
this.raw = raw;
|
25
18
|
}
|
26
|
-
},
|
27
19
|
|
28
|
-
|
29
|
-
|
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.
|
41
|
+
this.calumaStore.push(this);
|
33
42
|
}
|
34
|
-
}
|
35
|
-
}
|
43
|
+
}
|
44
|
+
}
|