@projectcaluma/ember-form 10.0.2 → 11.0.0-beta.11
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +1194 -0
- package/addon/components/cf-content.hbs +38 -41
- package/addon/components/cf-content.js +48 -29
- package/addon/components/cf-field/info.hbs +2 -2
- package/addon/components/cf-field/info.js +0 -15
- package/addon/components/cf-field/input/action-button.hbs +17 -19
- package/addon/components/cf-field/input/action-button.js +6 -10
- package/addon/components/cf-field/input/checkbox.hbs +6 -2
- package/addon/components/cf-field/input/checkbox.js +9 -29
- package/addon/components/cf-field/input/date.hbs +9 -5
- package/addon/components/cf-field/input/date.js +31 -10
- package/addon/components/cf-field/input/file.hbs +2 -2
- package/addon/components/cf-field/input/file.js +10 -11
- 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/radio.hbs +4 -1
- package/addon/components/cf-field/input/static.hbs +1 -1
- package/addon/components/cf-field/input/table.hbs +24 -24
- 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 +6 -5
- package/addon/components/cf-field/input.hbs +10 -1
- package/addon/components/cf-field/input.js +1 -1
- package/addon/components/cf-field/label.hbs +1 -1
- package/addon/components/cf-field-value.hbs +22 -7
- package/addon/components/cf-field-value.js +8 -38
- package/addon/components/cf-field.hbs +14 -6
- package/addon/components/cf-field.js +22 -8
- package/addon/components/cf-navigation-item.hbs +2 -2
- package/addon/components/cf-navigation.hbs +4 -1
- package/addon/components/document-validity.js +17 -2
- package/addon/gql/fragments/field.graphql +40 -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 +374 -407
- package/addon/lib/fieldset.js +46 -47
- package/addon/lib/form.js +27 -15
- package/addon/lib/navigation.js +211 -192
- package/addon/lib/question.js +103 -94
- package/addon/services/caluma-store.js +10 -6
- package/app/helpers/get-widget.js +4 -0
- package/blueprints/@projectcaluma/ember-form/index.js +1 -0
- package/package.json +30 -25
- package/addon/components/cf-navigation.js +0 -9
- package/addon/instance-initializers/setup-pikaday-i18n.js +0 -35
@@ -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
|
+
}
|
@@ -1,4 +1,5 @@
|
|
1
|
-
import {
|
1
|
+
import { get } from "@ember/object";
|
2
|
+
import { cached } from "tracked-toolbox";
|
2
3
|
|
3
4
|
import { getAST, getTransforms } from "@projectcaluma/ember-core/utils/jexl";
|
4
5
|
|
@@ -47,79 +48,43 @@ export function getDependenciesFromJexl(jexl, expression) {
|
|
47
48
|
}
|
48
49
|
|
49
50
|
/**
|
50
|
-
*
|
51
|
-
* nested dependency parent would be a table field that is used with a mapby
|
52
|
-
* transform in the JEXL expression.
|
53
|
-
*
|
54
|
-
* E.g: 'foo'|answer in 'bar'|answer|mapby('column') where 'bar' would be a
|
55
|
-
* nested dependency parent.
|
56
|
-
*
|
57
|
-
* Those need to be extracted seperately since the overall dependencies need to
|
58
|
-
* depend on the values of the nested dependency parents to recompute
|
59
|
-
* correctly.
|
51
|
+
* Getter to extract all fields used in an expression.
|
60
52
|
*
|
61
53
|
* @param {String} expressionPath The path of the expression
|
62
|
-
* @return {Field[]}
|
54
|
+
* @return {Field[]} An array of all dependency fields
|
63
55
|
*/
|
64
|
-
export function
|
65
|
-
return
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
const slugs = getDependenciesFromJexl(this.document.jexl, expression);
|
97
|
-
|
98
|
-
return slugs
|
99
|
-
.flatMap((slug) => {
|
100
|
-
const [fieldSlug, nestedSlug = null] = slug.split(".");
|
101
|
-
|
102
|
-
if (onlyNestedParents && !nestedSlug) {
|
103
|
-
return null;
|
104
|
-
}
|
105
|
-
|
106
|
-
const field = this.document.findField(fieldSlug);
|
107
|
-
|
108
|
-
if (!onlyNestedParents && nestedSlug && field?.value) {
|
109
|
-
// Get the nested fields from the parents value (rows)
|
110
|
-
const childFields =
|
111
|
-
nestedSlug === "__all__"
|
112
|
-
? field.value.flatMap((row) => row.fields)
|
113
|
-
: field.value.map((row) => row.findField(nestedSlug));
|
114
|
-
|
115
|
-
return [field, ...childFields];
|
116
|
-
}
|
117
|
-
|
118
|
-
return [field];
|
119
|
-
})
|
120
|
-
.filter(Boolean);
|
121
|
-
}
|
122
|
-
);
|
56
|
+
export function dependencies(expressionPath) {
|
57
|
+
return function (target, key) {
|
58
|
+
return cached(target, key, {
|
59
|
+
get() {
|
60
|
+
const expression = get(this, expressionPath);
|
61
|
+
|
62
|
+
if (!expression) return [];
|
63
|
+
|
64
|
+
const slugs = getDependenciesFromJexl(this.document.jexl, expression);
|
65
|
+
|
66
|
+
return slugs
|
67
|
+
.flatMap((slug) => {
|
68
|
+
const [fieldSlug, nestedSlug = null] = slug.split(".");
|
69
|
+
|
70
|
+
const field = this.document.findField(fieldSlug);
|
71
|
+
|
72
|
+
if (nestedSlug && field?.value) {
|
73
|
+
// Get the nested fields from the parents value (rows)
|
74
|
+
const childFields =
|
75
|
+
nestedSlug === "__all__"
|
76
|
+
? field.value.flatMap((row) => row.fields)
|
77
|
+
: field.value.map((row) => row.findField(nestedSlug));
|
78
|
+
|
79
|
+
return [field, ...childFields];
|
80
|
+
}
|
81
|
+
|
82
|
+
return [field];
|
83
|
+
})
|
84
|
+
.filter(Boolean);
|
85
|
+
},
|
86
|
+
});
|
87
|
+
};
|
123
88
|
}
|
124
89
|
|
125
90
|
export default dependencies;
|