@projectcaluma/ember-form 14.12.0 → 14.14.0

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.
@@ -160,18 +160,17 @@ export default class CfContentComponent extends Component {
160
160
 
161
161
  let answerDocument = null;
162
162
  let historicalDocument = null;
163
+ let jexlContext = null;
163
164
 
164
165
  if (!this.args.compare) {
165
- [answerDocument] = (
166
- await this.apollo.query(
167
- {
168
- query: getDocumentAnswersQuery,
169
- fetchPolicy: "network-only",
170
- variables: { id: this.args.documentId },
171
- },
172
- "allDocuments.edges",
173
- )
174
- ).map(({ node }) => node);
166
+ const data = await this.apollo.query({
167
+ query: getDocumentAnswersQuery,
168
+ fetchPolicy: "network-only",
169
+ variables: { id: this.args.documentId },
170
+ });
171
+
172
+ answerDocument = data.allDocuments.edges[0].node;
173
+ jexlContext = data.jexlContext;
175
174
  } else {
176
175
  const { from, to } = this.args.compare;
177
176
 
@@ -186,6 +185,7 @@ export default class CfContentComponent extends Component {
186
185
  });
187
186
  historicalDocument = data.fromRevision;
188
187
  answerDocument = data.toRevision;
188
+ jexlContext = data.jexlContext;
189
189
  }
190
190
 
191
191
  const [form] = (
@@ -199,14 +199,14 @@ export default class CfContentComponent extends Component {
199
199
  )
200
200
  ).map(({ node }) => node);
201
201
 
202
- const raw = parseDocument({ ...answerDocument, form });
202
+ const raw = parseDocument({ ...answerDocument, form, jexlContext });
203
203
 
204
204
  const document = new Document({
205
205
  raw,
206
206
  owner,
207
207
  dataSourceContext: this.args.context,
208
208
  historicalDocument: historicalDocument
209
- ? parseDocument({ ...historicalDocument, form })
209
+ ? parseDocument({ ...historicalDocument, form, jexlContext })
210
210
  : null,
211
211
  compare: this.args.compare,
212
212
  });
@@ -1,4 +1,5 @@
1
1
  <this.selectComponent
2
+ @multiple={{this.multiple}}
2
3
  @options={{@field.options}}
3
4
  @selected={{this.selected}}
4
5
  @disabled={{@disabled}}
@@ -3,7 +3,6 @@ import { inject as service } from "@ember/service";
3
3
  import { ensureSafeComponent } from "@embroider/util";
4
4
  import Component from "@glimmer/component";
5
5
  import PowerSelectComponent from "ember-power-select/components/power-select";
6
- import PowerSelectMultipleComponent from "ember-power-select/components/power-select-multiple";
7
6
 
8
7
  import getConfig from "@projectcaluma/ember-core/utils/get-config";
9
8
 
@@ -33,10 +32,7 @@ export default class CfFieldInputPowerselectComponent extends Component {
33
32
  }
34
33
 
35
34
  get selectComponent() {
36
- return ensureSafeComponent(
37
- this.multiple ? PowerSelectMultipleComponent : PowerSelectComponent,
38
- this,
39
- );
35
+ return ensureSafeComponent(PowerSelectComponent, this);
40
36
  }
41
37
 
42
38
  get searchEnabled() {
@@ -70,6 +70,7 @@
70
70
  <UkButton
71
71
  @color="link"
72
72
  @onClick={{fn this.edit document}}
73
+ @disabled={{or this.add.isRunning this.delete.isRunning}}
73
74
  title={{t "caluma.form.edit"}}
74
75
  class="uk-flex-inline uk-margin-small-left table-controls"
75
76
  data-test-edit-row
@@ -79,7 +80,8 @@
79
80
  </UkButton>
80
81
  <UkButton
81
82
  @color="link"
82
- @onClick={{fn (perform this.delete) document}}
83
+ @onClick={{fn this.delete.perform document}}
84
+ @disabled={{or this.add.isRunning this.delete.isRunning}}
83
85
  title={{t "caluma.form.delete"}}
84
86
  class="uk-flex-inline uk-margin-small-left table-controls"
85
87
  data-test-delete-row
@@ -99,7 +101,9 @@
99
101
  <td colspan={{add this.columns.length 1}} class="uk-text-center">
100
102
  <UkButton
101
103
  @color="link"
102
- @onClick={{perform this.add}}
104
+ @onClick={{this.add.perform}}
105
+ @disabled={{this.add.isRunning}}
106
+ @loading={{this.add.isRunning}}
103
107
  title={{t "caluma.form.addRow"}}
104
108
  data-test-add-row
105
109
  >
@@ -115,7 +119,7 @@
115
119
  {{#if this.documentToEdit}}
116
120
  <UkModal
117
121
  @visible={{this.showAddModal}}
118
- @onHide={{perform this.close}}
122
+ @onHide={{this.close.perform}}
119
123
  @bgClose={{false}}
120
124
  as |modal|
121
125
  >
@@ -123,7 +127,7 @@
123
127
  <CfFormWrapper
124
128
  @document={{this.documentToEdit}}
125
129
  @fieldset={{object-at 0 this.documentToEdit.fieldsets}}
126
- @disabled={{@disabled}}
130
+ @disabled={{or @disabled this.add.isRunning}}
127
131
  @context={{@context}}
128
132
  @compare={{@compare}}
129
133
  />
@@ -134,7 +138,7 @@
134
138
  <UkButton
135
139
  @label={{t "caluma.form.close"}}
136
140
  @color="primary"
137
- @onClick={{perform this.close}}
141
+ @onClick={{this.close.perform}}
138
142
  @disabled={{this.close.isRunning}}
139
143
  @loading={{this.close.isRunning}}
140
144
  data-test-close
@@ -142,8 +146,8 @@
142
146
  {{else}}
143
147
  <UkButton
144
148
  @label={{t "caluma.form.cancel"}}
145
- @onClick={{perform this.close}}
146
- @disabled={{this.close.isRunning}}
149
+ @onClick={{this.close.perform}}
150
+ @disabled={{or this.close.isRunning this.add.isRunning}}
147
151
  @loading={{this.close.isRunning}}
148
152
  data-test-cancel
149
153
  />
@@ -158,7 +162,7 @@
158
162
  @type="submit"
159
163
  @disabled={{or this.save.isRunning (not isValid)}}
160
164
  @loading={{this.save.isRunning}}
161
- @onClick={{fn (perform this.save) validate}}
165
+ @onClick={{fn this.save.perform validate}}
162
166
  data-test-save
163
167
  />
164
168
  </DocumentValidity>
@@ -74,22 +74,68 @@ export default class CfFieldInputTableComponent extends Component {
74
74
  owner,
75
75
  });
76
76
 
77
+ // Open the modal immediately - attaching the row to the table (below) is
78
+ // debounced and would otherwise noticeably delay opening the modal. While
79
+ // this task runs, the row's fields and the cancel button are disabled in
80
+ // the modal, so no answer can be saved to (and validated by) the backend
81
+ // and the row cannot be removed again before it is attached to the table.
77
82
  this.documentToEditIsNew = true;
78
83
  this.documentToEdit = newDocument;
79
84
  this.showAddModal = true;
80
- });
81
85
 
82
- delete = task({ drop: true }, async (document) => {
83
- if (!(await confirm(this.intl.t("caluma.form.deleteRow")))) {
84
- return;
86
+ // Attach the row document to the table answer, so the backend has the
87
+ // full context to validate and evaluate the row's fields while editing.
88
+ // `documentToEditIsNew` marks the row as preliminary, so it will be
89
+ // removed again if the edit dialog is cancelled.
90
+ try {
91
+ const rows = this.args.field.answer.value ?? [];
92
+ await this.args.onSave([...rows, newDocument]);
93
+ } catch {
94
+ this.notification.danger(
95
+ this.intl.t("caluma.form.notification.table.add.error"),
96
+ );
97
+
98
+ // Attaching failed - remove the preliminary row and close the modal.
99
+ // `close` cannot be performed here, as it waits for this task to finish.
100
+ await this.deleteRow(this.documentToEdit);
101
+ this.documentToEditIsNew = false;
102
+ this.showAddModal = false;
103
+ this.documentToEdit = null;
85
104
  }
105
+ });
86
106
 
87
- const remainingDocuments = this.args.field.answer.value.filter(
107
+ /**
108
+ * Delete row without asking. Remove from the table, then remove row document
109
+ *
110
+ * @async
111
+ * @method deleteRow
112
+ * @param {Document} document The row document to delete
113
+ * @return {Promise<Void>}
114
+ */
115
+ async deleteRow(document) {
116
+ const remainingDocuments = (this.args.field.answer.value ?? []).filter(
88
117
  (doc) => doc.pk !== document.pk,
89
118
  );
90
119
 
120
+ // remove row from table
91
121
  await this.args.onSave(remainingDocuments);
92
- await this.removeOrphan(document);
122
+
123
+ // delete row document
124
+ await this.apollo.mutate({
125
+ mutation: removeDocumentMutation,
126
+ variables: { input: { document: document.uuid } },
127
+ });
128
+
129
+ // Remove orphaned document from Caluma store.
130
+ this.calumaStore.delete(document.pk);
131
+ }
132
+
133
+ delete = task({ drop: true }, async (document) => {
134
+ if (!(await confirm(this.intl.t("caluma.form.deleteRow")))) {
135
+ return;
136
+ }
137
+
138
+ await this.deleteRow(document);
93
139
  });
94
140
 
95
141
  save = task({ drop: true }, async (validate) => {
@@ -106,19 +152,13 @@ export default class CfFieldInputTableComponent extends Component {
106
152
  return;
107
153
  }
108
154
 
109
- const rows = this.args.field.answer.value ?? [];
110
-
111
- if (!rows.find((doc) => doc.pk === newDocument.pk)) {
112
- // add document to table
113
- await this.args.onSave([...rows, newDocument]);
114
-
155
+ if (this.documentToEditIsNew) {
115
156
  this.notification.success(
116
157
  this.intl.t("caluma.form.notification.table.add.success"),
117
158
  );
159
+ this.documentToEditIsNew = false;
118
160
  }
119
161
 
120
- this.documentToEditIsNew = false;
121
-
122
162
  await this.close.perform();
123
163
  } catch {
124
164
  this.notification.danger(
@@ -128,10 +168,20 @@ export default class CfFieldInputTableComponent extends Component {
128
168
  });
129
169
 
130
170
  close = task({ drop: true }, async () => {
131
- if (this.documentToEditIsNew) {
132
- await this.removeOrphan(this.documentToEdit);
171
+ // Wait for a pending "add" to finish attaching the new row to the table
172
+ // before closing - otherwise removing or validating the row would race
173
+ // with the debounced save of the table answer.
174
+ if (this.add.isRunning) {
175
+ await this.add.last;
176
+ }
133
177
 
178
+ if (this.documentToEditIsNew) {
179
+ await this.deleteRow(this.documentToEdit);
134
180
  this.documentToEditIsNew = false;
181
+ this.showAddModal = false;
182
+ this.documentToEdit = null;
183
+ // No validation for new documents that are deleted
184
+ return;
135
185
  }
136
186
 
137
187
  if (!this.args.disabled) {
@@ -142,17 +192,6 @@ export default class CfFieldInputTableComponent extends Component {
142
192
  this.documentToEdit = null;
143
193
  });
144
194
 
145
- async removeOrphan(calumaDocument) {
146
- // Remove orphaned document from database.
147
- await this.apollo.mutate({
148
- mutation: removeDocumentMutation,
149
- variables: { input: { document: calumaDocument.uuid } },
150
- });
151
-
152
- // Remove orphaned document from Caluma store.
153
- this.calumaStore.delete(calumaDocument.pk);
154
- }
155
-
156
195
  @action
157
196
  edit(document) {
158
197
  this.documentToEdit = document;
@@ -4,7 +4,7 @@
4
4
  field=@field
5
5
  disabled=true
6
6
  context=@context
7
- onSave=(perform this.save)
7
+ onSave=this.save.perform
8
8
  )
9
9
  as |FieldComponent|
10
10
  }}
@@ -34,7 +34,7 @@
34
34
  @field={{@field}}
35
35
  @disabled={{or @disabled @field.refreshAnswer.isRunning}}
36
36
  @context={{@context}}
37
- @onSave={{perform this.save}}
37
+ @onSave={{this.save.perform}}
38
38
  />
39
39
  {{/let}}
40
40
  {{/if}}
@@ -108,6 +108,8 @@ query (
108
108
  $to: DateTime!
109
109
  $context: JSONString
110
110
  ) {
111
+ jexlContext: documentGlobalJexlContext(id: $documentId)
112
+
111
113
  fromRevision: documentAsOf(id: $documentId, asOf: $from) {
112
114
  ...RevisionDocument
113
115
  answers: historicalAnswers(asOf: $from, excludeDeleted: true) {
@@ -1,31 +1,46 @@
1
1
  #import * from '../fragments/field.graphql'
2
- #import * from '../fragments/case-form-and-workflow.graphql'
3
2
 
4
3
  query DocumentAnswers($id: ID!) {
4
+ # Global JEXL context (case & work item) for JEXL evaluations. This will be
5
+ # enriched with the document depending data in `lib/document.js` and
6
+ # `lib/field.js` before being passed into the JEXL evaluation
7
+ jexlContext: documentGlobalJexlContext(id: $id)
8
+
5
9
  allDocuments(filter: [{ id: $id }]) {
6
10
  edges {
7
11
  node {
8
12
  id
13
+
14
+ # Basic form information
9
15
  form {
10
16
  id
11
17
  slug
12
18
  }
19
+
20
+ # All answers of the document, up to three levels deep (top form -> sub
21
+ # form -> table form)
22
+ answers {
23
+ edges {
24
+ node {
25
+ ...FieldAnswer
26
+ }
27
+ }
28
+ }
29
+
30
+ # Work item and case work items to determine the actionable work item
31
+ # for action buttons. This is either the direct work item below or a
32
+ # case work item of the type complete workflow form.
33
+ # See `workItemUuid` in `lib/document.js`
13
34
  workItem {
14
35
  id
15
- meta
16
- task {
17
- slug
18
- }
19
- case {
20
- ...CaseFormAndWorkflow
21
- }
22
36
  }
23
37
  case {
24
- ...CaseFormAndWorkflow
25
38
  workItems {
26
39
  edges {
27
40
  node {
28
41
  id
42
+ # `__typename` is what identifies the complete workflow form
43
+ # work item, the `task` selection is only there to request it
29
44
  task {
30
45
  id
31
46
  }
@@ -33,13 +48,6 @@ query DocumentAnswers($id: ID!) {
33
48
  }
34
49
  }
35
50
  }
36
- answers {
37
- edges {
38
- node {
39
- ...FieldAnswer
40
- }
41
- }
42
- }
43
51
  }
44
52
  }
45
53
  }
@@ -232,38 +232,30 @@ export default class Document extends Base {
232
232
  * @property {Object} jexlContext
233
233
  */
234
234
  get jexlContext() {
235
- const workItem = this.raw.workItem;
236
- const _case = workItem?.case ?? this.raw.case;
235
+ const globalContext = this.raw.jexlContext ?? {
236
+ info: {
237
+ // Expressions rely on these being `null` rather than absent when the
238
+ // document is not attached to a case or work item.
239
+ case: null,
240
+ workItem: null,
241
+ },
242
+ };
237
243
 
238
244
  return (
239
245
  this.parentDocument?.jexlContext ?? {
246
+ ...globalContext,
240
247
  // JEXL interprets null in an expression as variable instead of a
241
248
  // primitive. This resolves that issue.
242
249
  null: null,
250
+ // The information about the root form needs to be constructed on the
251
+ // first document as it doesn't depend on the field.
243
252
  form: this.rootForm.slug,
244
253
  info: {
254
+ ...globalContext.info,
245
255
  root: {
246
256
  form: this.rootForm.slug,
247
257
  formMeta: this.rootForm.raw.meta,
248
258
  },
249
- case: _case
250
- ? {
251
- form: _case.document?.form.slug,
252
- workflow: _case.workflow.slug,
253
- meta: _case.meta,
254
- root: {
255
- form: _case.family.document?.form.slug,
256
- workflow: _case.family.workflow.slug,
257
- meta: _case.family.meta,
258
- },
259
- }
260
- : null,
261
- workItem: workItem
262
- ? {
263
- task: workItem.task.slug,
264
- meta: workItem.meta,
265
- }
266
- : null,
267
259
  },
268
260
  }
269
261
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projectcaluma/ember-form",
3
- "version": "14.12.0",
3
+ "version": "14.14.0",
4
4
  "description": "Ember addon for rendering Caluma forms.",
5
5
  "keywords": [
6
6
  "ember-addon"
@@ -39,7 +39,7 @@
39
39
  "luxon": "^3.5.0",
40
40
  "reactiveweb": "^1.3.0",
41
41
  "tracked-toolbox": "^2.0.0",
42
- "@projectcaluma/ember-core": "^14.12.0"
42
+ "@projectcaluma/ember-core": "^14.14.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@ember/optional-features": "2.3.0",
@@ -74,12 +74,12 @@
74
74
  "uikit": "3.25.6",
75
75
  "uuid": "13.0.0",
76
76
  "webpack": "5.104.1",
77
- "@projectcaluma/ember-testing": "14.12.0",
78
- "@projectcaluma/ember-workflow": "14.12.0"
77
+ "@projectcaluma/ember-testing": "14.14.0",
78
+ "@projectcaluma/ember-workflow": "14.14.0"
79
79
  },
80
80
  "peerDependencies": {
81
81
  "ember-source": ">= 4.0.0",
82
- "@projectcaluma/ember-workflow": "^14.12.0"
82
+ "@projectcaluma/ember-workflow": "^14.14.0"
83
83
  },
84
84
  "dependenciesMeta": {
85
85
  "@projectcaluma/ember-core": {
@@ -1,30 +0,0 @@
1
- fragment CaseFormAndWorkflow on Case {
2
- id
3
- meta
4
- workflow {
5
- id
6
- slug
7
- }
8
- document {
9
- id
10
- form {
11
- id
12
- slug
13
- }
14
- }
15
- family {
16
- id
17
- meta
18
- workflow {
19
- id
20
- slug
21
- }
22
- document {
23
- id
24
- form {
25
- id
26
- slug
27
- }
28
- }
29
- }
30
- }