@projectcaluma/ember-form 10.0.1 → 11.0.0-beta.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. package/CHANGELOG.md +1181 -0
  2. package/addon/components/cf-content.hbs +36 -41
  3. package/addon/components/cf-content.js +48 -29
  4. package/addon/components/cf-field/info.hbs +2 -2
  5. package/addon/components/cf-field/info.js +0 -15
  6. package/addon/components/cf-field/input/action-button.hbs +18 -19
  7. package/addon/components/cf-field/input/action-button.js +9 -7
  8. package/addon/components/cf-field/input/checkbox.hbs +6 -2
  9. package/addon/components/cf-field/input/checkbox.js +9 -29
  10. package/addon/components/cf-field/input/date.hbs +8 -5
  11. package/addon/components/cf-field/input/date.js +28 -10
  12. package/addon/components/cf-field/input/file.hbs +2 -2
  13. package/addon/components/cf-field/input/file.js +10 -11
  14. package/addon/components/cf-field/input/float.hbs +4 -4
  15. package/addon/components/cf-field/input/integer.hbs +5 -5
  16. package/addon/components/cf-field/input/radio.hbs +4 -1
  17. package/addon/components/cf-field/input/static.hbs +1 -1
  18. package/addon/components/cf-field/input/table.hbs +24 -24
  19. package/addon/components/cf-field/input/table.js +12 -10
  20. package/addon/components/cf-field/input/text.hbs +5 -5
  21. package/addon/components/cf-field/input/textarea.hbs +6 -5
  22. package/addon/components/cf-field/input.hbs +10 -1
  23. package/addon/components/cf-field/input.js +1 -1
  24. package/addon/components/cf-field/label.hbs +1 -1
  25. package/addon/components/cf-field-value.hbs +22 -7
  26. package/addon/components/cf-field-value.js +8 -38
  27. package/addon/components/cf-field.hbs +14 -6
  28. package/addon/components/cf-field.js +22 -8
  29. package/addon/components/cf-navigation-item.hbs +2 -2
  30. package/addon/components/cf-navigation.hbs +4 -1
  31. package/addon/components/document-validity.js +17 -2
  32. package/addon/gql/fragments/field.graphql +45 -0
  33. package/addon/gql/mutations/save-document-table-answer.graphql +1 -1
  34. package/addon/gql/mutations/save-document.graphql +1 -0
  35. package/addon/gql/queries/{get-document-answers.graphql → document-answers.graphql} +2 -1
  36. package/addon/gql/queries/{get-document-forms.graphql → document-forms.graphql} +2 -1
  37. package/addon/gql/queries/{get-document-used-dynamic-options.graphql → document-used-dynamic-options.graphql} +2 -1
  38. package/addon/gql/queries/{get-dynamic-options.graphql → dynamic-options.graphql} +2 -1
  39. package/addon/gql/queries/{get-fileanswer-info.graphql → fileanswer-info.graphql} +2 -1
  40. package/addon/helpers/get-widget.js +50 -0
  41. package/addon/lib/answer.js +108 -72
  42. package/addon/lib/base.js +32 -23
  43. package/addon/lib/dependencies.js +36 -71
  44. package/addon/lib/document.js +92 -96
  45. package/addon/lib/field.js +374 -407
  46. package/addon/lib/fieldset.js +46 -47
  47. package/addon/lib/form.js +27 -15
  48. package/addon/lib/navigation.js +211 -192
  49. package/addon/lib/question.js +103 -94
  50. package/addon/services/caluma-store.js +10 -6
  51. package/app/helpers/get-widget.js +4 -0
  52. package/blueprints/@projectcaluma/ember-form/index.js +1 -0
  53. package/package.json +30 -25
  54. package/addon/components/cf-navigation.js +0 -9
  55. 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 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
+ }
@@ -1,4 +1,5 @@
1
- import { computed, get } from "@ember/object";
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
- * Computed property to get all nested dependency parents of an expression. A
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[]} Returns an array of nested dependency parent fields
54
+ * @return {Field[]} An array of all dependency fields
63
55
  */
64
- export function nestedDependencyParents(expressionPath) {
65
- return dependencies(expressionPath, { onlyNestedParents: true });
66
- }
67
-
68
- /**
69
- * Computed property to get all dependencies of an expression.
70
- *
71
- * @param {String} expressionPath The path of the expression
72
- * @param {Object} options
73
- * @param {Boolean} options.onlyNestedParents Only include nested parent fields
74
- * @param {String} options.nestedParentsPath Path of the nested parent fields to trigger recomputation
75
- * @return {Field[]} Returns an array of all dependency fields
76
- */
77
- export function dependencies(
78
- expressionPath,
79
- { onlyNestedParents = false, nestedParentsPath = null } = {}
80
- ) {
81
- // If there are nested parents we need to recompute the property if their
82
- // values change
83
- const nestedTriggers = nestedParentsPath
84
- ? [`${nestedParentsPath}.@each.value`]
85
- : [];
86
-
87
- return computed(
88
- "document.{jexl,fields.[]}",
89
- expressionPath,
90
- ...nestedTriggers,
91
- function () {
92
- const expression = get(this, expressionPath);
93
-
94
- if (!expression) return [];
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;