@projectcaluma/ember-form 11.0.0-beta.4 → 11.0.0-beta.40
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +202 -0
- package/addon/components/cf-content.hbs +38 -37
- package/addon/components/cf-content.js +7 -3
- package/addon/components/cf-field/hint.hbs +5 -0
- package/addon/components/cf-field/input/action-button.hbs +8 -4
- package/addon/components/cf-field/input/action-button.js +60 -59
- package/addon/components/cf-field/input/checkbox.hbs +2 -3
- package/addon/components/cf-field/input/date.hbs +12 -25
- package/addon/components/cf-field/input/date.js +19 -11
- package/addon/components/cf-field/input/files.hbs +38 -0
- package/addon/components/cf-field/input/files.js +113 -0
- package/addon/components/cf-field/input/powerselect.hbs +27 -29
- package/addon/components/cf-field/input/powerselect.js +8 -2
- package/addon/components/cf-field/input/radio.hbs +2 -2
- package/addon/components/cf-field/input/static.hbs +1 -1
- package/addon/components/cf-field/input/table.hbs +20 -18
- package/addon/components/cf-field/input/table.js +1 -11
- package/addon/components/cf-field/input.hbs +8 -21
- package/addon/components/cf-field/input.js +32 -14
- package/addon/components/cf-field/label.hbs +4 -2
- package/addon/components/cf-field-value.hbs +22 -7
- package/addon/components/cf-field-value.js +12 -36
- package/addon/components/cf-field.hbs +42 -9
- package/addon/components/cf-field.js +41 -17
- package/addon/components/cf-form-wrapper.hbs +4 -1
- package/addon/components/cf-form.hbs +6 -1
- package/addon/components/document-validity.js +16 -1
- package/addon/gql/fragments/field.graphql +32 -7
- package/addon/gql/mutations/save-document-files-answer.graphql +9 -0
- package/addon/gql/queries/document-forms.graphql +1 -1
- package/addon/gql/queries/dynamic-options.graphql +4 -4
- package/addon/gql/queries/{fileanswer-info.graphql → filesanswer-info.graphql} +4 -4
- package/addon/helpers/format-graphql-error.js +21 -0
- package/addon/helpers/get-widget.js +16 -2
- package/addon/instance-initializers/form-widget-overrides.js +52 -0
- package/addon/lib/document.js +9 -1
- package/addon/lib/field.js +96 -59
- package/addon/lib/navigation.js +3 -1
- package/addon/lib/question.js +18 -5
- package/addon/modifiers/autoresize.js +14 -0
- package/addon/services/caluma-store.js +2 -0
- package/app/components/cf-field/{input/file.js → hint.js} +1 -1
- package/app/components/cf-field/input/files.js +1 -0
- package/app/helpers/format-graphql-error.js +1 -0
- package/app/helpers/get-widget.js +1 -4
- package/app/instance-initializers/form-widget-overrides.js +4 -0
- package/app/modifiers/autoresize.js +1 -0
- package/app/styles/@projectcaluma/ember-form.scss +5 -15
- package/app/styles/_flatpickr.scss +47 -0
- package/blueprints/@projectcaluma/ember-form/index.js +1 -1
- package/index.js +12 -0
- package/package.json +47 -37
- package/translations/de.yaml +6 -6
- package/translations/en.yaml +6 -6
- package/translations/fr.yaml +6 -6
- package/addon/components/cf-field/input/file.hbs +0 -32
- package/addon/components/cf-field/input/file.js +0 -89
- package/addon/components/cf-field/label.js +0 -11
- package/addon/gql/mutations/remove-answer.graphql +0 -7
- package/addon/gql/mutations/save-document-file-answer.graphql +0 -9
- package/addon/instance-initializers/setup-pikaday-i18n.js +0 -35
- package/config/environment.js +0 -5
@@ -1,5 +1,5 @@
|
|
1
|
-
import {
|
2
|
-
import {
|
1
|
+
import { action } from "@ember/object";
|
2
|
+
import { macroCondition, isTesting } from "@embroider/macros";
|
3
3
|
import Component from "@glimmer/component";
|
4
4
|
import { timeout, restartableTask } from "ember-concurrency";
|
5
5
|
|
@@ -9,7 +9,7 @@ import { hasQuestionType } from "@projectcaluma/ember-core/helpers/has-question-
|
|
9
9
|
* Component to display a label and input for a certain field of a document.
|
10
10
|
*
|
11
11
|
* ```hbs
|
12
|
-
*
|
12
|
+
* <CfField @field={{this.someField}} />
|
13
13
|
* ```
|
14
14
|
*
|
15
15
|
* You can disable the field by passing `disabled=true`.
|
@@ -18,6 +18,16 @@ import { hasQuestionType } from "@projectcaluma/ember-core/helpers/has-question-
|
|
18
18
|
* @argument {Field} field The field data model to render
|
19
19
|
*/
|
20
20
|
export default class CfFieldComponent extends Component {
|
21
|
+
@action
|
22
|
+
registerComponent() {
|
23
|
+
this.args.field._components.add(this);
|
24
|
+
}
|
25
|
+
|
26
|
+
@action
|
27
|
+
unregisterComponent() {
|
28
|
+
this.args.field._components.delete(this);
|
29
|
+
}
|
30
|
+
|
21
31
|
get visible() {
|
22
32
|
return (
|
23
33
|
!this.args.field?.hidden &&
|
@@ -36,35 +46,49 @@ export default class CfFieldComponent extends Component {
|
|
36
46
|
return !hasQuestionType(this.args.field?.question, "action-button");
|
37
47
|
}
|
38
48
|
|
49
|
+
get hintTextVisible() {
|
50
|
+
return !hasQuestionType(
|
51
|
+
this.args.field?.question,
|
52
|
+
"action-button",
|
53
|
+
"static",
|
54
|
+
"form"
|
55
|
+
);
|
56
|
+
}
|
57
|
+
|
39
58
|
get saveIndicatorVisible() {
|
40
59
|
return !hasQuestionType(this.args.field?.question, "action-button");
|
41
60
|
}
|
42
61
|
|
43
62
|
/**
|
44
|
-
* Task to save a field. This will set the passed value to the answer and
|
45
|
-
*
|
63
|
+
* Task to save a field. This will set the passed value to the answer and save
|
64
|
+
* the field to the API after a timeout of 500 milliseconds which intends to
|
65
|
+
* reduce the amount of saved values when changed rapidly.
|
46
66
|
*
|
47
67
|
* @method save
|
48
|
-
* @param {String|Number|String[]} value
|
68
|
+
* @param {String|Number|String[]} value The new value to save to the field
|
69
|
+
* @param {Boolean} bypassTimeout Whether to bypass the timeout
|
49
70
|
*/
|
50
71
|
@restartableTask
|
51
|
-
*save(value) {
|
52
|
-
|
53
|
-
|
72
|
+
*save(value, bypassTimeout = false) {
|
73
|
+
if (typeof this.args.onSave === "function") {
|
74
|
+
return yield this.args.onSave(this.args.field, value);
|
75
|
+
}
|
54
76
|
|
55
77
|
/* istanbul ignore next */
|
56
|
-
if (
|
57
|
-
|
78
|
+
if (macroCondition(isTesting())) {
|
79
|
+
// no timeout
|
80
|
+
} else {
|
81
|
+
if (!bypassTimeout) {
|
82
|
+
yield timeout(500);
|
83
|
+
}
|
58
84
|
}
|
59
85
|
|
60
|
-
|
86
|
+
if (this.args.field.answer) {
|
87
|
+
this.args.field.answer.value = value;
|
88
|
+
}
|
61
89
|
|
62
90
|
yield this.args.field.validate.perform();
|
63
91
|
|
64
|
-
|
65
|
-
return yield this.args.field.save.unlinked().perform();
|
66
|
-
} catch (e) {
|
67
|
-
// that's ok
|
68
|
-
}
|
92
|
+
return yield this.args.field.save.unlinked().perform();
|
69
93
|
}
|
70
94
|
}
|
@@ -1,6 +1,8 @@
|
|
1
1
|
{{#let
|
2
2
|
(component
|
3
|
-
(
|
3
|
+
(ensure-safe-component
|
4
|
+
(get-widget @fieldset.field.question @fieldset.form default="cf-form")
|
5
|
+
)
|
4
6
|
)
|
5
7
|
as |FormComponent|
|
6
8
|
}}
|
@@ -9,5 +11,6 @@
|
|
9
11
|
@fieldset={{@fieldset}}
|
10
12
|
@context={{@context}}
|
11
13
|
@disabled={{@disabled}}
|
14
|
+
@onSave={{@onSave}}
|
12
15
|
/>
|
13
16
|
{{/let}}
|
@@ -3,7 +3,12 @@
|
|
3
3
|
{{#if (has-block)}}
|
4
4
|
{{yield field}}
|
5
5
|
{{else}}
|
6
|
-
<CfField
|
6
|
+
<CfField
|
7
|
+
@field={{field}}
|
8
|
+
@disabled={{@disabled}}
|
9
|
+
@context={{@context}}
|
10
|
+
@onSave={{@onSave}}
|
11
|
+
/>
|
7
12
|
{{/if}}
|
8
13
|
{{/each}}
|
9
14
|
</form>
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import { action } from "@ember/object";
|
2
2
|
import Component from "@glimmer/component";
|
3
3
|
import { restartableTask } from "ember-concurrency";
|
4
|
+
import { cached } from "tracked-toolbox";
|
4
5
|
|
5
6
|
/**
|
6
7
|
* Component to check the validity of a document
|
@@ -31,12 +32,26 @@ export default class DocumentValidity extends Component {
|
|
31
32
|
* @argument {Boolean} validateOnEnter
|
32
33
|
*/
|
33
34
|
|
35
|
+
@cached
|
34
36
|
get isValid() {
|
35
|
-
return this.args.document.fields
|
37
|
+
return this.args.document.fields
|
38
|
+
.filter((f) => !f.hidden)
|
39
|
+
.every((f) => f.isValid);
|
36
40
|
}
|
37
41
|
|
38
42
|
@restartableTask
|
39
43
|
*_validate() {
|
44
|
+
const saveTasks = this.args.document.fields
|
45
|
+
.flatMap((field) => [
|
46
|
+
...[...(field._components ?? [])].map((c) => c.save.last),
|
47
|
+
field.save?.last,
|
48
|
+
])
|
49
|
+
.filter(Boolean);
|
50
|
+
|
51
|
+
// Wait until all currently running save tasks in the UI and in the field
|
52
|
+
// itself are finished
|
53
|
+
yield Promise.all(saveTasks);
|
54
|
+
|
40
55
|
for (const field of this.args.document.fields) {
|
41
56
|
yield field.validate.linked().perform();
|
42
57
|
}
|
@@ -1,8 +1,3 @@
|
|
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
|
-
|
6
1
|
fragment SimpleQuestion on Question {
|
7
2
|
id
|
8
3
|
slug
|
@@ -19,6 +14,16 @@ fragment SimpleQuestion on Question {
|
|
19
14
|
value
|
20
15
|
}
|
21
16
|
placeholder
|
17
|
+
formatValidators {
|
18
|
+
edges {
|
19
|
+
node {
|
20
|
+
slug
|
21
|
+
regex
|
22
|
+
errorMsg
|
23
|
+
}
|
24
|
+
}
|
25
|
+
}
|
26
|
+
hintText
|
22
27
|
}
|
23
28
|
... on TextareaQuestion {
|
24
29
|
textareaMinLength: minLength
|
@@ -28,6 +33,16 @@ fragment SimpleQuestion on Question {
|
|
28
33
|
value
|
29
34
|
}
|
30
35
|
placeholder
|
36
|
+
formatValidators {
|
37
|
+
edges {
|
38
|
+
node {
|
39
|
+
slug
|
40
|
+
regex
|
41
|
+
errorMsg
|
42
|
+
}
|
43
|
+
}
|
44
|
+
}
|
45
|
+
hintText
|
31
46
|
}
|
32
47
|
... on IntegerQuestion {
|
33
48
|
integerMinValue: minValue
|
@@ -37,6 +52,7 @@ fragment SimpleQuestion on Question {
|
|
37
52
|
value
|
38
53
|
}
|
39
54
|
placeholder
|
55
|
+
hintText
|
40
56
|
}
|
41
57
|
... on FloatQuestion {
|
42
58
|
floatMinValue: minValue
|
@@ -46,6 +62,7 @@ fragment SimpleQuestion on Question {
|
|
46
62
|
value
|
47
63
|
}
|
48
64
|
placeholder
|
65
|
+
hintText
|
49
66
|
}
|
50
67
|
... on ChoiceQuestion {
|
51
68
|
choiceOptions: options {
|
@@ -62,6 +79,7 @@ fragment SimpleQuestion on Question {
|
|
62
79
|
id
|
63
80
|
value
|
64
81
|
}
|
82
|
+
hintText
|
65
83
|
}
|
66
84
|
... on MultipleChoiceQuestion {
|
67
85
|
multipleChoiceOptions: options {
|
@@ -78,18 +96,24 @@ fragment SimpleQuestion on Question {
|
|
78
96
|
id
|
79
97
|
value
|
80
98
|
}
|
99
|
+
hintText
|
81
100
|
}
|
82
101
|
... on DateQuestion {
|
83
102
|
dateDefaultAnswer: defaultAnswer {
|
84
103
|
id
|
85
104
|
value
|
86
105
|
}
|
106
|
+
hintText
|
87
107
|
}
|
88
108
|
... on StaticQuestion {
|
89
109
|
staticContent
|
90
110
|
}
|
91
111
|
... on CalculatedFloatQuestion {
|
92
112
|
calcExpression
|
113
|
+
hintText
|
114
|
+
}
|
115
|
+
... on FilesQuestion {
|
116
|
+
hintText
|
93
117
|
}
|
94
118
|
... on ActionButtonQuestion {
|
95
119
|
action
|
@@ -112,6 +136,7 @@ fragment FieldTableQuestion on Question {
|
|
112
136
|
}
|
113
137
|
}
|
114
138
|
}
|
139
|
+
hintText
|
115
140
|
tableDefaultAnswer: defaultAnswer {
|
116
141
|
id
|
117
142
|
value {
|
@@ -204,8 +229,8 @@ fragment SimpleAnswer on Answer {
|
|
204
229
|
... on ListAnswer {
|
205
230
|
listValue: value
|
206
231
|
}
|
207
|
-
... on
|
208
|
-
|
232
|
+
... on FilesAnswer {
|
233
|
+
filesValue: value {
|
209
234
|
id
|
210
235
|
uploadUrl
|
211
236
|
downloadUrl
|
@@ -1,11 +1,11 @@
|
|
1
|
-
query DynamicOptions($question: String
|
2
|
-
allQuestions(filter: [{
|
1
|
+
query DynamicOptions($question: String!, $context: JSONString) {
|
2
|
+
allQuestions(filter: [{ slugs: [$question] }], first: 1) {
|
3
3
|
edges {
|
4
4
|
node {
|
5
5
|
id
|
6
6
|
slug
|
7
7
|
... on DynamicChoiceQuestion {
|
8
|
-
dynamicChoiceOptions: options {
|
8
|
+
dynamicChoiceOptions: options(context: $context) {
|
9
9
|
edges {
|
10
10
|
node {
|
11
11
|
slug
|
@@ -15,7 +15,7 @@ query DynamicOptions($question: String!) {
|
|
15
15
|
}
|
16
16
|
}
|
17
17
|
... on DynamicMultipleChoiceQuestion {
|
18
|
-
dynamicMultipleChoiceOptions: options {
|
18
|
+
dynamicMultipleChoiceOptions: options(context: $context) {
|
19
19
|
edges {
|
20
20
|
node {
|
21
21
|
slug
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import { helper } from "@ember/component/helper";
|
2
|
+
|
3
|
+
export function formatGraphqlErrorObject(error) {
|
4
|
+
try {
|
5
|
+
const path = error.path.join(".");
|
6
|
+
const { line, column } = error.locations[error.locations.length - 1];
|
7
|
+
|
8
|
+
return `${path}:${line}:${column}: ${error.message}`;
|
9
|
+
} catch (e) {
|
10
|
+
return null;
|
11
|
+
}
|
12
|
+
}
|
13
|
+
|
14
|
+
export function formatGraphqlError(error) {
|
15
|
+
return (
|
16
|
+
error?.errors?.map(formatGraphqlErrorObject).filter(Boolean).join("\n") ??
|
17
|
+
""
|
18
|
+
);
|
19
|
+
}
|
20
|
+
|
21
|
+
export default helper(([error]) => formatGraphqlError(error));
|
@@ -1,6 +1,15 @@
|
|
1
1
|
import Helper from "@ember/component/helper";
|
2
2
|
import { warn } from "@ember/debug";
|
3
3
|
import { inject as service } from "@ember/service";
|
4
|
+
import { ensureSafeComponent } from "@embroider/util";
|
5
|
+
|
6
|
+
import InputComponent from "@projectcaluma/ember-form/components/cf-field/input";
|
7
|
+
import FormComponent from "@projectcaluma/ember-form/components/cf-form";
|
8
|
+
|
9
|
+
const DEFAULT_WIDGETS = {
|
10
|
+
"cf-field/input": InputComponent,
|
11
|
+
"cf-form": FormComponent,
|
12
|
+
};
|
4
13
|
|
5
14
|
/**
|
6
15
|
* Helper for getting the right widget.
|
@@ -42,9 +51,14 @@ export default class GetWidgetHelper extends Helper {
|
|
42
51
|
{ id: "ember-caluma.unregistered-override" }
|
43
52
|
);
|
44
53
|
|
45
|
-
if (override)
|
54
|
+
if (override) {
|
55
|
+
return ensureSafeComponent(
|
56
|
+
override.componentClass ?? override.component,
|
57
|
+
this
|
58
|
+
);
|
59
|
+
}
|
46
60
|
}
|
47
61
|
|
48
|
-
return defaultWidget;
|
62
|
+
return ensureSafeComponent(DEFAULT_WIDGETS[defaultWidget], this);
|
49
63
|
}
|
50
64
|
}
|
@@ -0,0 +1,52 @@
|
|
1
|
+
import { setOwner } from "@ember/application";
|
2
|
+
import { inject as service } from "@ember/service";
|
3
|
+
|
4
|
+
import HiddenComponent from "@projectcaluma/ember-form/components/cf-field/input/hidden";
|
5
|
+
import PowerSelectComponent from "@projectcaluma/ember-form/components/cf-field/input/powerselect";
|
6
|
+
|
7
|
+
class HiddenOverride {
|
8
|
+
@service intl;
|
9
|
+
|
10
|
+
get label() {
|
11
|
+
return this.intl.t("caluma.form-builder.question.widgetOverrides.hidden");
|
12
|
+
}
|
13
|
+
|
14
|
+
component = "cf-field/input/hidden";
|
15
|
+
componentClass = HiddenComponent;
|
16
|
+
}
|
17
|
+
|
18
|
+
class PowerSelectOverride {
|
19
|
+
@service intl;
|
20
|
+
|
21
|
+
get label() {
|
22
|
+
return this.intl.t(
|
23
|
+
"caluma.form-builder.question.widgetOverrides.powerselect"
|
24
|
+
);
|
25
|
+
}
|
26
|
+
|
27
|
+
component = "cf-field/input/powerselect";
|
28
|
+
componentClass = PowerSelectComponent;
|
29
|
+
types = [
|
30
|
+
"ChoiceQuestion",
|
31
|
+
"MultipleChoiceQuestion",
|
32
|
+
"DynamicChoiceQuestion",
|
33
|
+
"DynamicMultipleChoiceQuestion",
|
34
|
+
];
|
35
|
+
}
|
36
|
+
|
37
|
+
export function initialize(appInstance) {
|
38
|
+
const options = appInstance.lookup("service:caluma-options");
|
39
|
+
|
40
|
+
const hiddenOverride = new HiddenOverride();
|
41
|
+
const powerSelectOverride = new PowerSelectOverride();
|
42
|
+
|
43
|
+
setOwner(hiddenOverride, appInstance);
|
44
|
+
setOwner(powerSelectOverride, appInstance);
|
45
|
+
|
46
|
+
options.registerComponentOverride(hiddenOverride);
|
47
|
+
options.registerComponentOverride(powerSelectOverride);
|
48
|
+
}
|
49
|
+
|
50
|
+
export default {
|
51
|
+
initialize,
|
52
|
+
};
|
package/addon/lib/document.js
CHANGED
@@ -18,7 +18,7 @@ const sum = (nums) => nums.reduce((num, base) => base + num, 0);
|
|
18
18
|
* @class Document
|
19
19
|
*/
|
20
20
|
export default class Document extends Base {
|
21
|
-
constructor({ raw, parentDocument, ...args }) {
|
21
|
+
constructor({ raw, parentDocument, dataSourceContext, ...args }) {
|
22
22
|
assert(
|
23
23
|
"A graphql document `raw` must be passed",
|
24
24
|
raw?.__typename === "Document"
|
@@ -27,6 +27,7 @@ export default class Document extends Base {
|
|
27
27
|
super({ raw, ...args });
|
28
28
|
|
29
29
|
this.parentDocument = parentDocument;
|
30
|
+
this.dataSourceContext = dataSourceContext;
|
30
31
|
|
31
32
|
this.pushIntoStore();
|
32
33
|
|
@@ -83,6 +84,13 @@ export default class Document extends Base {
|
|
83
84
|
*/
|
84
85
|
fieldsets = [];
|
85
86
|
|
87
|
+
/**
|
88
|
+
* Context object for data sources
|
89
|
+
*
|
90
|
+
* @property {Object} dataSourceContext
|
91
|
+
*/
|
92
|
+
dataSourceContext = null;
|
93
|
+
|
86
94
|
/**
|
87
95
|
* The primary key of the document.
|
88
96
|
*
|