@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.
- 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
|
+
}
|