@conduction/nextcloud-vue 2.56.0-beta.63 → 2.56.0-beta.64

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.
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/dist/nextcloud-vue.cjs.js.map
3
- size: 44689364 bytes
4
- sha256: b83f2aba1e645e27b38f5a17226ea51f6df400ee03b80d2aadbda9b350cfc459
3
+ size: 44704290 bytes
4
+ sha256: e14648ce27bae8fed4271ddde5d95e10a1a5616bf123c75c6ae73769d697d1e6
@@ -28366,35 +28366,35 @@ button.cn-stepper__step[data-v-776581d4] {
28366
28366
  }
28367
28367
 
28368
28368
 
28369
- .cn-work-queue[data-v-793630f4] {
28369
+ .cn-work-queue[data-v-d5bc0a42] {
28370
28370
  display: flex;
28371
28371
  flex-direction: column;
28372
28372
  gap: 24px;
28373
28373
  }
28374
- .cn-work-queue__head[data-v-793630f4],
28375
- .cn-work-queue__group-head[data-v-793630f4] {
28374
+ .cn-work-queue__head[data-v-d5bc0a42],
28375
+ .cn-work-queue__group-head[data-v-d5bc0a42] {
28376
28376
  display: flex;
28377
28377
  flex-wrap: wrap;
28378
28378
  align-items: center;
28379
28379
  justify-content: space-between;
28380
28380
  gap: 12px;
28381
28381
  }
28382
- .cn-work-queue__title[data-v-793630f4] {
28382
+ .cn-work-queue__title[data-v-d5bc0a42] {
28383
28383
  margin: 0;
28384
28384
  font-size: 28px;
28385
28385
  font-weight: 700;
28386
28386
  }
28387
- .cn-work-queue__actions[data-v-793630f4] {
28387
+ .cn-work-queue__actions[data-v-d5bc0a42] {
28388
28388
  display: flex;
28389
28389
  gap: 8px;
28390
28390
  }
28391
- .cn-work-queue__group[data-v-793630f4] {
28391
+ .cn-work-queue__group[data-v-d5bc0a42] {
28392
28392
  padding: 20px 24px;
28393
28393
  border: 1px solid var(--color-border);
28394
28394
  border-radius: var(--border-radius-large);
28395
28395
  background: var(--color-main-background);
28396
28396
  }
28397
- .cn-work-queue__group-title[data-v-793630f4] {
28397
+ .cn-work-queue__group-title[data-v-d5bc0a42] {
28398
28398
  display: flex;
28399
28399
  align-items: center;
28400
28400
  gap: 8px;
@@ -28402,33 +28402,33 @@ button.cn-stepper__step[data-v-776581d4] {
28402
28402
  font-size: 18px;
28403
28403
  font-weight: 700;
28404
28404
  }
28405
- .cn-work-queue__count[data-v-793630f4] {
28405
+ .cn-work-queue__count[data-v-d5bc0a42] {
28406
28406
  padding: 0 8px;
28407
28407
  border-radius: 10px;
28408
28408
  background: var(--color-background-dark);
28409
28409
  font-size: 12px;
28410
28410
  font-weight: 600;
28411
28411
  }
28412
- .cn-work-queue__muted[data-v-793630f4] {
28412
+ .cn-work-queue__muted[data-v-d5bc0a42] {
28413
28413
  margin: 4px 0 8px;
28414
28414
  color: var(--color-text-maxcontrast);
28415
28415
  }
28416
- .cn-work-queue__items[data-v-793630f4] {
28416
+ .cn-work-queue__items[data-v-d5bc0a42] {
28417
28417
  margin: 0;
28418
28418
  padding: 0;
28419
28419
  list-style: none;
28420
28420
  }
28421
- .cn-work-queue__item[data-v-793630f4] {
28421
+ .cn-work-queue__item[data-v-d5bc0a42] {
28422
28422
  display: flex;
28423
28423
  align-items: center;
28424
28424
  gap: 16px;
28425
28425
  padding: 12px 0;
28426
28426
  border-block-end: 1px solid var(--color-border);
28427
28427
  }
28428
- .cn-work-queue__item[data-v-793630f4]:last-child {
28428
+ .cn-work-queue__item[data-v-d5bc0a42]:last-child {
28429
28429
  border-block-end: none;
28430
28430
  }
28431
- .cn-work-queue__avatar[data-v-793630f4] {
28431
+ .cn-work-queue__avatar[data-v-d5bc0a42] {
28432
28432
  display: inline-flex;
28433
28433
  flex: none;
28434
28434
  align-items: center;
@@ -28441,24 +28441,24 @@ button.cn-stepper__step[data-v-776581d4] {
28441
28441
  font-size: 12px;
28442
28442
  font-weight: 700;
28443
28443
  }
28444
- .cn-work-queue__body[data-v-793630f4] {
28444
+ .cn-work-queue__body[data-v-d5bc0a42] {
28445
28445
  display: flex;
28446
28446
  flex: 1 1 auto;
28447
28447
  flex-direction: column;
28448
28448
  min-width: 0;
28449
28449
  }
28450
- .cn-work-queue__link[data-v-793630f4] {
28450
+ .cn-work-queue__link[data-v-d5bc0a42] {
28451
28451
  color: var(--color-main-text);
28452
28452
  font-weight: 700;
28453
28453
  text-decoration: none;
28454
28454
  }
28455
- .cn-work-queue__link[data-v-793630f4]:hover {
28455
+ .cn-work-queue__link[data-v-d5bc0a42]:hover {
28456
28456
  text-decoration: underline;
28457
28457
  }
28458
- .cn-work-queue__item--late .cn-work-queue__link[data-v-793630f4] {
28458
+ .cn-work-queue__item--late .cn-work-queue__link[data-v-d5bc0a42] {
28459
28459
  color: var(--color-error);
28460
28460
  }
28461
- .cn-work-queue__sub[data-v-793630f4] {
28461
+ .cn-work-queue__sub[data-v-d5bc0a42] {
28462
28462
  display: flex;
28463
28463
  flex-wrap: wrap;
28464
28464
  gap: 12px;
@@ -41817,12 +41817,12 @@ button.cn-object-calendar__overflow[data-v-e1c69ee2] {
41817
41817
  }
41818
41818
 
41819
41819
 
41820
- .cn-action-dialog[data-v-53fea6f0] {
41820
+ .cn-action-dialog[data-v-0bb28fd1] {
41821
41821
  display: flex;
41822
41822
  flex-direction: column;
41823
41823
  gap: 16px;
41824
41824
  }
41825
- .cn-action-dialog__summary[data-v-53fea6f0] {
41825
+ .cn-action-dialog__summary[data-v-0bb28fd1] {
41826
41826
  display: flex;
41827
41827
  align-items: center;
41828
41828
  gap: 10px;
@@ -41830,13 +41830,13 @@ button.cn-object-calendar__overflow[data-v-e1c69ee2] {
41830
41830
  border: 1px solid var(--color-border);
41831
41831
  border-radius: var(--border-radius-large, 8px);
41832
41832
  }
41833
- .cn-action-dialog__summary-text[data-v-53fea6f0] {
41833
+ .cn-action-dialog__summary-text[data-v-0bb28fd1] {
41834
41834
  display: flex;
41835
41835
  flex: 1 1 auto;
41836
41836
  flex-direction: column;
41837
41837
  min-width: 0;
41838
41838
  }
41839
- .cn-action-dialog__badge[data-v-53fea6f0] {
41839
+ .cn-action-dialog__badge[data-v-0bb28fd1] {
41840
41840
  padding: 3px 10px;
41841
41841
  border-radius: 12px;
41842
41842
  background: var(--color-background-dark);
@@ -41844,25 +41844,25 @@ button.cn-object-calendar__overflow[data-v-e1c69ee2] {
41844
41844
  font-weight: 600;
41845
41845
  white-space: nowrap;
41846
41846
  }
41847
- .cn-action-dialog__title[data-v-53fea6f0] {
41847
+ .cn-action-dialog__title[data-v-0bb28fd1] {
41848
41848
  margin: 0 0 8px;
41849
41849
  font-size: 15px;
41850
41850
  font-weight: 700;
41851
41851
  }
41852
- .cn-action-dialog__facts[data-v-53fea6f0] {
41852
+ .cn-action-dialog__facts[data-v-0bb28fd1] {
41853
41853
  display: grid;
41854
41854
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
41855
41855
  gap: 8px 16px;
41856
41856
  margin: 0;
41857
41857
  }
41858
- .cn-action-dialog__facts dt[data-v-53fea6f0] {
41858
+ .cn-action-dialog__facts dt[data-v-0bb28fd1] {
41859
41859
  color: var(--color-text-maxcontrast);
41860
41860
  font-size: 13px;
41861
41861
  }
41862
- .cn-action-dialog__facts dd[data-v-53fea6f0] {
41862
+ .cn-action-dialog__facts dd[data-v-0bb28fd1] {
41863
41863
  margin: 0;
41864
41864
  }
41865
- .cn-action-dialog__checks[data-v-53fea6f0] {
41865
+ .cn-action-dialog__checks[data-v-0bb28fd1] {
41866
41866
  display: flex;
41867
41867
  flex-direction: column;
41868
41868
  gap: 8px;
@@ -41870,28 +41870,28 @@ button.cn-object-calendar__overflow[data-v-e1c69ee2] {
41870
41870
  padding: 0;
41871
41871
  list-style: none;
41872
41872
  }
41873
- .cn-action-dialog__check[data-v-53fea6f0] {
41873
+ .cn-action-dialog__check[data-v-0bb28fd1] {
41874
41874
  display: flex;
41875
41875
  flex-wrap: wrap;
41876
41876
  align-items: center;
41877
41877
  gap: 8px;
41878
41878
  }
41879
- .cn-action-dialog__check-icon[data-v-53fea6f0] {
41879
+ .cn-action-dialog__check-icon[data-v-0bb28fd1] {
41880
41880
  color: var(--color-text-maxcontrast);
41881
41881
  }
41882
- .cn-action-dialog__check-icon--met[data-v-53fea6f0] {
41882
+ .cn-action-dialog__check-icon--met[data-v-0bb28fd1] {
41883
41883
  color: var(--color-success-text, var(--color-success));
41884
41884
  }
41885
- .cn-action-dialog__muted[data-v-53fea6f0] {
41885
+ .cn-action-dialog__muted[data-v-0bb28fd1] {
41886
41886
  color: var(--color-text-maxcontrast);
41887
41887
  font-size: 14px;
41888
41888
  }
41889
- .cn-action-dialog__field[data-v-53fea6f0] {
41889
+ .cn-action-dialog__field[data-v-0bb28fd1] {
41890
41890
  display: flex;
41891
41891
  flex-direction: column;
41892
41892
  gap: 4px;
41893
41893
  }
41894
- .cn-action-dialog__error[data-v-53fea6f0] {
41894
+ .cn-action-dialog__error[data-v-0bb28fd1] {
41895
41895
  margin: 0;
41896
41896
  color: var(--color-error-text, var(--color-error));
41897
41897
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.56.0-beta.63",
3
+ "version": "2.56.0-beta.64",
4
4
  "description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
5
5
  "homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
6
6
  "bugs": {
@@ -5,7 +5,7 @@
5
5
  <template>
6
6
  <CnDialog
7
7
  :name="text(config.title) || t('nextcloud-vue', 'Confirm')"
8
- :eyebrow="text(config.eyebrow)"
8
+ :eyebrow="eyebrowText"
9
9
  :subtitle="text(config.subtitle)"
10
10
  width="form"
11
11
  :noClose="busy"
@@ -63,6 +63,24 @@
63
63
  </ul>
64
64
  </section>
65
65
 
66
+ <!-- Fields the action asks for, sent beside the reason. -->
67
+ <div v-for="field in fields"
68
+ :key="field.field"
69
+ class="cn-action-dialog__field"
70
+ :data-testid="`cn-action-dialog-field-${field.field}`">
71
+ <NcSelect v-if="field.type === 'select'"
72
+ :modelValue="selectedOption(field)"
73
+ :options="optionsOf(field)"
74
+ :inputLabel="text(field.label) || field.field"
75
+ :clearable="field.required === false"
76
+ @update:modelValue="(option) => pick(field, option)" />
77
+ <NcTextField v-else
78
+ :modelValue="values[field.field] || ''"
79
+ :type="field.type === 'datetime' ? 'datetime-local' : (field.type === 'date' ? 'date' : 'text')"
80
+ :label="text(field.label) || field.field"
81
+ @update:modelValue="(value) => setValue(field.field, value)" />
82
+ </div>
83
+
66
84
  <div v-if="config.reason" class="cn-action-dialog__field">
67
85
  <NcTextArea
68
86
  :id="reasonId"
@@ -116,7 +134,7 @@ import axios from '@nextcloud/axios'
116
134
  import { emit } from '@nextcloud/event-bus'
117
135
  import { translate as t } from '@nextcloud/l10n'
118
136
  import { generateUrl } from '@nextcloud/router'
119
- import { NcButton, NcNoteCard, NcTextArea } from '@nextcloud/vue'
137
+ import { NcButton, NcNoteCard, NcSelect, NcTextArea, NcTextField } from '@nextcloud/vue'
120
138
  import CheckCircle from 'vue-material-design-icons/CheckCircle.vue'
121
139
  import CircleOutline from 'vue-material-design-icons/CircleOutline.vue'
122
140
  import HelpCircleOutline from 'vue-material-design-icons/HelpCircleOutline.vue'
@@ -141,7 +159,8 @@ let reasonSeq = 0
141
159
  * note and Archive it.
142
160
  *
143
161
  * `config`:
144
- * - `title`, `eyebrow`, `subtitle`, `doneText`: texts, through the host translate.
162
+ * - `title`, `subtitle`, `doneText`: texts, through the host translate;
163
+ * `eyebrow` a template filled from the record ("Close off · {identifier}").
145
164
  * - `summary`: `{ source?, title, meta?, badge?, icon? }`: templates filled from
146
165
  * the record (`source` an endpointSource to read it from, else `object`).
147
166
  * - `facts`: `[{ title?, items: [{ label, value }] }]`, `value` a template.
@@ -149,8 +168,14 @@ let reasonSeq = 0
149
168
  * `{ id, met, label?, detail? }` read from `source`; `labels[id]` gives an
150
169
  * item's `{ label, detail?, unmetDetail? }`; the ids in `required` must be
151
170
  * met before the action can run. `met: null` reads as unknown.
152
- * - `permission`: `{ source, allowedPath, reasonPath? }`: the action runs only
153
- * while `allowedPath` reads true; `reasonPath` says why not.
171
+ * - `permission`: `{ source, listPath?, match?, allowedPath, reasonPath? }`:
172
+ * the action runs only while `allowedPath` reads true; `reasonPath` says
173
+ * why not. With `listPath` the paths read the entry of that list whose
174
+ * fields equal `match` (`{ act: "archive" }`).
175
+ * - `fields`: `[{ field, type?: "text"|"date"|"datetime"|"select", label?,
176
+ * required?, options?: [{ value, label, fills? }] }]`: inputs sent beside
177
+ * the reason, required by default; a select option's `fills` sets other
178
+ * fields that are still empty.
154
179
  * - `reason`: `{ label?, hint?, required?, field? }`: a text area; required by
155
180
  * default; sent as `field` (default `reason`).
156
181
  * - `note`: `{ variant?, title?, text }`: a read-only note.
@@ -163,7 +188,7 @@ let reasonSeq = 0
163
188
  export default {
164
189
  name: 'CnActionDialog',
165
190
 
166
- components: { CheckCircle, CircleOutline, CnDialog, CnIcon, HelpCircleOutline, NcButton, NcNoteCard, NcTextArea },
191
+ components: { CheckCircle, CircleOutline, CnDialog, CnIcon, HelpCircleOutline, NcButton, NcNoteCard, NcSelect, NcTextArea, NcTextField },
167
192
 
168
193
  inject: {
169
194
  /** The host app's translate (CnAppRoot `cnTranslate`). */
@@ -209,6 +234,7 @@ export default {
209
234
  checklistError: '',
210
235
  allowed: true,
211
236
  reason: '',
237
+ values: {},
212
238
  busy: false,
213
239
  refusal: '',
214
240
  done: false,
@@ -219,7 +245,21 @@ export default {
219
245
  computed: {
220
246
  /** @return {object} The token context. */
221
247
  tokenCtx() {
222
- return { objectId: this.objectId, object: this.record || {} }
248
+ return { objectId: this.recordId, object: this.record || {} }
249
+ },
250
+
251
+ /**
252
+ * The record's id: `objectId`, or the route's `id` when the prop is
253
+ * empty or a token the opener could not resolve.
254
+ *
255
+ * @return {string|number}
256
+ */
257
+ recordId() {
258
+ const given = this.objectId
259
+ if (given !== '' && given !== null && given !== undefined && !String(given).startsWith('@')) {
260
+ return given
261
+ }
262
+ return (this.$route && this.$route.params && this.$route.params.id) || ''
223
263
  },
224
264
 
225
265
  /** @return {object} The confirm config. */
@@ -305,7 +345,27 @@ export default {
305
345
  * @return {boolean}
306
346
  */
307
347
  canConfirm() {
308
- return !this.busy && this.allowed && this.requiredMet && (!this.reasonNeeded || this.reason.trim() !== '')
348
+ return !this.busy && this.allowed && this.requiredMet && this.fieldsFilled && (!this.reasonNeeded || this.reason.trim() !== '')
349
+ },
350
+
351
+ /** @return {string} The eyebrow, filled from the record once it is known. */
352
+ eyebrowText() {
353
+ return this.record ? this.fill(this.config.eyebrow) || this.text(this.config.eyebrow) : this.text(this.config.eyebrow)
354
+ },
355
+
356
+ /** @return {Array<object>} The fields with a name. */
357
+ fields() {
358
+ return (Array.isArray(this.config.fields) ? this.config.fields : []).filter((f) => f && f.field)
359
+ },
360
+
361
+ /**
362
+ * Whether every required field has a value.
363
+ *
364
+ * @spec openspec/changes/action-dialog-fields-and-acts/specs/action-dialog/spec.md#requirement-an-action-dialog-asks-for-fields-and-reads-a-permission-from-a-list
365
+ * @return {boolean}
366
+ */
367
+ fieldsFilled() {
368
+ return this.fields.every((f) => f.required === false || String(this.values[f.field] ?? '').trim() !== '')
309
369
  },
310
370
  },
311
371
 
@@ -380,6 +440,63 @@ export default {
380
440
  }
381
441
  },
382
442
 
443
+ /**
444
+ * The entry of a list whose fields equal `match`, or null.
445
+ *
446
+ * @param {unknown} list The list.
447
+ * @param {object} [match] The fields to match.
448
+ * @return {object|null}
449
+ */
450
+ matchIn(list, match) {
451
+ const wanted = Object.entries(match || {})
452
+ return (Array.isArray(list) ? list : []).find((entry) => entry && wanted.every(([key, value]) => String(entry[key]) === String(value))) || null
453
+ },
454
+
455
+ /**
456
+ * A select field's options, labels translated.
457
+ *
458
+ * @param {object} field The field.
459
+ * @return {Array<{id: string, label: string, fills?: object}>}
460
+ */
461
+ optionsOf(field) {
462
+ return (Array.isArray(field.options) ? field.options : []).map((o) => ({ id: String(o.value), label: this.text(o.label) || String(o.value), fills: o.fills }))
463
+ },
464
+
465
+ /**
466
+ * A select field's chosen option.
467
+ *
468
+ * @param {object} field The field.
469
+ * @return {object|null}
470
+ */
471
+ selectedOption(field) {
472
+ return this.optionsOf(field).find((o) => o.id === String(this.values[field.field] ?? '')) || null
473
+ },
474
+
475
+ /**
476
+ * Set a field's value.
477
+ *
478
+ * @param {string} name The field.
479
+ * @param {unknown} value The value.
480
+ */
481
+ setValue(name, value) {
482
+ this.values = { ...this.values, [name]: value === null || value === undefined ? '' : String(value) }
483
+ },
484
+
485
+ /**
486
+ * Choose a select option; its `fills` set the fields still empty.
487
+ *
488
+ * @param {object} field The select field.
489
+ * @param {{id: string, fills?: object}|null} option The option.
490
+ */
491
+ pick(field, option) {
492
+ this.setValue(field.field, option ? option.id : '')
493
+ for (const [name, value] of Object.entries((option && option.fills) || {})) {
494
+ if (String(this.values[name] ?? '').trim() === '') {
495
+ this.setValue(name, this.text(value) || value)
496
+ }
497
+ }
498
+ },
499
+
383
500
  /** @return {Promise<void>} */
384
501
  async loadPermission() {
385
502
  const def = this.config.permission
@@ -388,8 +505,9 @@ export default {
388
505
  }
389
506
  try {
390
507
  const data = await this.read(def.source)
391
- this.allowed = readPath(data, def.allowedPath || 'allowed') === true
392
- this.refusal = this.allowed || !def.reasonPath ? '' : String(readPath(data, def.reasonPath) || '')
508
+ const entry = def.listPath ? this.matchIn(readPath(data, def.listPath), def.match) : data
509
+ this.allowed = readPath(entry, def.allowedPath || 'allowed') === true
510
+ this.refusal = this.allowed || !def.reasonPath ? '' : String(readPath(entry, def.reasonPath) || '')
393
511
  } catch {
394
512
  this.allowed = false
395
513
  }
@@ -420,15 +538,18 @@ export default {
420
538
  if (this.config.reason) {
421
539
  body[this.config.reason.field || 'reason'] = reason
422
540
  }
541
+ for (const field of this.fields) {
542
+ body[field.field] = this.values[field.field] ?? ''
543
+ }
423
544
  const method = String(request.method || 'POST').toLowerCase()
424
545
  await axios[['post', 'put', 'patch', 'delete'].includes(method) ? method : 'post'](/^https?:\/\//i.test(url) ? url : generateUrl(url), body)
425
546
  } else if (this.confirmConfig.action && typeof this.cnDispatchAction === 'function') {
426
- await this.cnDispatchAction({ ...this.confirmConfig.action, args: [...(this.confirmConfig.action.args || []), { reason }] })
547
+ await this.cnDispatchAction({ ...this.confirmConfig.action, args: [...(this.confirmConfig.action.args || []), { ...this.values, reason }] })
427
548
  }
428
549
  this.done = true
429
550
  emit(PAGE_REFRESH, {})
430
- /** @event done The action ran. Payload: `{ reason }`. */
431
- this.$emit('done', { reason })
551
+ /** @event done The action ran. Payload: the field values and `reason`. */
552
+ this.$emit('done', { ...this.values, reason })
432
553
  } catch (failure) {
433
554
  const data = failure && failure.response && failure.response.data
434
555
  this.refusal = String((data && (data.message || data.error)) || t('nextcloud-vue', 'The action could not be completed.'))
@@ -93,6 +93,7 @@
93
93
 
94
94
  <script>
95
95
  import axios from '@nextcloud/axios'
96
+ import { subscribe, unsubscribe } from '@nextcloud/event-bus'
96
97
  import { translate as t } from '@nextcloud/l10n'
97
98
  import { generateUrl } from '@nextcloud/router'
98
99
  import { NcButton, NcEmptyContent, NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'
@@ -109,7 +110,8 @@ import { openRowTarget } from '../../utils/linkNavigation.js'
109
110
  * not be read is named at the top rather than folded into "nothing is
110
111
  * waiting". A group can be hidden until tomorrow, through the server, so the
111
112
  * choice follows the reader to another device. dossiq's personal queue is
112
- * the first; pipelinq's queue has the same shape.
113
+ * the first; pipelinq's queue has the same shape. It reads again on
114
+ * `cn:page:refresh`, so work planned or closed in a dialog shows at once.
113
115
  *
114
116
  * `content`:
115
117
  * - `source`: an endpointSource answering `{ groups, hiddenGroups?, unavailable? }`.
@@ -176,9 +178,14 @@ export default {
176
178
  },
177
179
 
178
180
  mounted() {
181
+ subscribe('cn:page:refresh', this.load)
179
182
  this.load()
180
183
  },
181
184
 
185
+ beforeUnmount() {
186
+ unsubscribe('cn:page:refresh', this.load)
187
+ },
188
+
182
189
  methods: {
183
190
  t,
184
191
 
@@ -857,7 +857,10 @@ export function dispatchAction(action, context = {}) {
857
857
  return
858
858
  }
859
859
 
860
- context.openModal(target, action.props ?? {})
860
+ // A prop that is exactly a token (`"objectId": "@objectId"`) resolves
861
+ // against the page's object; one the page cannot answer stays as written.
862
+ const props = context.tokenCtx ? resolveDeepTokens(action.props ?? {}, context.tokenCtx) : (action.props ?? {})
863
+ context.openModal(target, props)
861
864
  break
862
865
  }
863
866