@conduction/nextcloud-vue 2.88.0 → 2.89.1

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.
@@ -34796,28 +34796,28 @@ to {
34796
34796
  }
34797
34797
 
34798
34798
 
34799
- .cn-admin-settings-shell[data-v-0f41828c] {
34799
+ .cn-admin-settings-shell[data-v-c3ceda64] {
34800
34800
  max-width: 900px;
34801
34801
  }
34802
34802
 
34803
34803
  /* The board look lifts the width and lays the header and cards on a grid (look-board.css). */
34804
- .cn-support-info[data-v-0f41828c] {
34804
+ .cn-support-info[data-v-c3ceda64] {
34805
34805
  margin-top: 16px;
34806
34806
  padding-top: 16px;
34807
34807
  border-top: 1px solid var(--color-border);
34808
34808
  }
34809
- .cn-support-info h4[data-v-0f41828c] {
34809
+ .cn-support-info h4[data-v-c3ceda64] {
34810
34810
  margin: 0 0 8px 0;
34811
34811
  font-size: 14px;
34812
34812
  font-weight: 600;
34813
34813
  color: var(--color-main-text);
34814
34814
  }
34815
- .cn-support-info p[data-v-0f41828c] {
34815
+ .cn-support-info p[data-v-c3ceda64] {
34816
34816
  margin: 4px 0;
34817
34817
  color: var(--color-text-maxcontrast);
34818
34818
  font-size: 14px;
34819
34819
  }
34820
- .cn-support-info a[data-v-0f41828c] {
34820
+ .cn-support-info a[data-v-c3ceda64] {
34821
34821
  color: var(--color-primary-element);
34822
34822
  }
34823
34823
 
@@ -199632,6 +199632,11 @@ var script$3e = {
199632
199632
  cnTranslate: { default: () => (key) => key },
199633
199633
  },
199634
199634
 
199635
+ // A dashboard binds the widget's `content` keys as attributes too. Falling
199636
+ // through, `submitLabel` would reach CnFormWidgetBase and win over the
199637
+ // translated label the widget passes, so attributes go through rootAttrs.
199638
+ inheritAttrs: false,
199639
+
199635
199640
  props: {
199636
199641
  /**
199637
199642
  * Persisted configuration blob (see component description for the shape).
@@ -199667,6 +199672,19 @@ var script$3e = {
199667
199672
  },
199668
199673
 
199669
199674
  computed: {
199675
+ /**
199676
+ * The attributes for the root form: everything the widget was given,
199677
+ * minus the names CnFormWidgetBase takes as props, which the widget
199678
+ * sets itself.
199679
+ *
199680
+ * @spec openspec/changes/interaction-form-fits-and-reads-dutch/specs/interaction-form-widget/spec.md#requirement-the-interaction-form-fits-its-card-and-reads-in-the-readers-language
199681
+ * @return {object} The attributes to bind.
199682
+ */
199683
+ rootAttrs() {
199684
+ const own = new Set(Object.keys(__sfc_main$3f.props || {}));
199685
+ return Object.fromEntries(Object.entries(this.$attrs).filter(([key]) => !own.has(vue$1.camelize(key))))
199686
+ },
199687
+
199670
199688
  register() {
199671
199689
  return this.content.register || 'pipelinq'
199672
199690
  },
@@ -200034,7 +200052,7 @@ function render$3e(_ctx, _cache, $props, $setup, $data, $options) {
200034
200052
  const _component_NcButton = vue$1.resolveComponent("NcButton");
200035
200053
  const _component_CnFormWidgetBase = vue$1.resolveComponent("CnFormWidgetBase");
200036
200054
 
200037
- return (vue$1.openBlock(), vue$1.createBlock(_component_CnFormWidgetBase, {
200055
+ return (vue$1.openBlock(), vue$1.createBlock(_component_CnFormWidgetBase, vue$1.mergeProps($options.rootAttrs, {
200038
200056
  blockClass: "cn-interaction-form-widget",
200039
200057
  fields: $options.formFields,
200040
200058
  model: $data.form,
@@ -200046,7 +200064,7 @@ function render$3e(_ctx, _cache, $props, $setup, $data, $options) {
200046
200064
  errorMessage: $data.errorMessage,
200047
200065
  "onUpdate:field": $options.onFieldUpdate,
200048
200066
  onSubmit: $options.onRegister
200049
- }, {
200067
+ }), {
200050
200068
  "field-client": vue$1.withCtx(() => [
200051
200069
  vue$1.createVNode(_component_CnResourceSelect, {
200052
200070
  register: $options.register,
@@ -200079,7 +200097,7 @@ function render$3e(_ctx, _cache, $props, $setup, $data, $options) {
200079
200097
  : vue$1.createCommentVNode("v-if", true)
200080
200098
  ]),
200081
200099
  _: 1 /* STABLE */
200082
- }, 8 /* PROPS */, ["fields", "model", "errors", "canSubmit", "submitting", "submitLabel", "submittingLabel", "errorMessage", "onUpdate:field", "onSubmit"]))
200100
+ }, 16 /* FULL_PROPS */, ["fields", "model", "errors", "canSubmit", "submitting", "submitLabel", "submittingLabel", "errorMessage", "onUpdate:field", "onSubmit"]))
200083
200101
  }
200084
200102
 
200085
200103
  script$3e.render = render$3e;
@@ -257113,6 +257131,43 @@ function readTabFromAddress(key) {
257113
257131
  return new URLSearchParams(window.location.search).get(key) || ''
257114
257132
  }
257115
257133
 
257134
+ /**
257135
+ * The tab remembered under a storage key, or '' without a key, a value or
257136
+ * usable storage (a private window can refuse it).
257137
+ *
257138
+ * @param {string} key The storage key.
257139
+ * @return {string} The remembered tab id.
257140
+ */
257141
+ function readStoredTab(key) {
257142
+ if (!key) {
257143
+ return ''
257144
+ }
257145
+ try {
257146
+ return window.localStorage.getItem(key) || ''
257147
+ } catch {
257148
+ return ''
257149
+ }
257150
+ }
257151
+
257152
+ /**
257153
+ * Remember the open tab under a storage key; nothing without a key or
257154
+ * usable storage.
257155
+ *
257156
+ * @param {string} key The storage key.
257157
+ * @param {string} id The tab's id.
257158
+ * @return {void}
257159
+ */
257160
+ function storeTab(key, id) {
257161
+ if (!key) {
257162
+ return
257163
+ }
257164
+ try {
257165
+ window.localStorage.setItem(key, id);
257166
+ } catch {
257167
+ // Storage refused (private window): the address still holds the tab.
257168
+ }
257169
+ }
257170
+
257116
257171
  /**
257117
257172
  * Put the open tab in the address without a new history entry.
257118
257173
  *
@@ -257323,6 +257378,18 @@ var script$2k = {
257323
257378
  default: 'tab',
257324
257379
  },
257325
257380
 
257381
+ /**
257382
+ * A browser storage key under which the shell remembers the last
257383
+ * opened tab, so the page opens on it again when the address names no
257384
+ * tab. Empty (the default) remembers nothing. The address still wins.
257385
+ *
257386
+ * @spec openspec/changes/admin-shell-remembers-tab/specs/settings-board-look/spec.md#requirement-the-admin-shell-can-remember-the-last-tab
257387
+ */
257388
+ tabStorageKey: {
257389
+ type: String,
257390
+ default: '',
257391
+ },
257392
+
257326
257393
  /**
257327
257394
  * How the `tabs` strip is drawn, as CnTabs' `variant`: `line`, the
257328
257395
  * folder tabs (the default), or `underline`, plain labels on a rule
@@ -257495,7 +257562,7 @@ var script$2k = {
257495
257562
  reimporting: false,
257496
257563
  setupWizardOpen: false,
257497
257564
  // The open tab's id; empty until a tab is picked or the address names one.
257498
- chosenTabId: readTabFromAddress(this.tabQueryKey),
257565
+ chosenTabId: readTabFromAddress(this.tabQueryKey) || readStoredTab(this.tabStorageKey),
257499
257566
  }
257500
257567
  },
257501
257568
 
@@ -257655,6 +257722,19 @@ var script$2k = {
257655
257722
  },
257656
257723
  },
257657
257724
 
257725
+ /**
257726
+ * A page that opens on a tab other than the first (named in the address,
257727
+ * or remembered) tells the app once, as a click would, so a breadcrumb or
257728
+ * title that follows the tab is right from the first paint.
257729
+ *
257730
+ * @spec openspec/changes/admin-shell-remembers-tab/specs/settings-board-look/spec.md#requirement-the-admin-shell-can-remember-the-last-tab
257731
+ */
257732
+ mounted() {
257733
+ if (this.tabItems.length > 0 && this.activeTabId !== this.tabItems[0].id) {
257734
+ this.$emit('tab-change', this.activeTabId);
257735
+ }
257736
+ },
257737
+
257658
257738
  methods: {
257659
257739
  /**
257660
257740
  * A tab was picked in the strip: remember it, put it in the address
@@ -257671,6 +257751,7 @@ var script$2k = {
257671
257751
  }
257672
257752
  this.chosenTabId = tab.id;
257673
257753
  writeTabToAddress(this.tabQueryKey, tab.id);
257754
+ storeTab(this.tabStorageKey, tab.id);
257674
257755
  /**
257675
257756
  * @event tab-change Emitted when the reader opens another tab. Payload: its `id`.
257676
257757
  */
@@ -258008,7 +258089,7 @@ function render$2k(_ctx, _cache, $props, $setup, $data, $options) {
258008
258089
  }
258009
258090
 
258010
258091
  script$2k.render = render$2k;
258011
- script$2k.__scopeId = "data-v-0f41828c";
258092
+ script$2k.__scopeId = "data-v-c3ceda64";
258012
258093
  const __sfc_main$2k = script$2k;
258013
258094
 
258014
258095
  /**
@@ -358472,6 +358553,7 @@ var script$h = {
358472
358553
  // write lands; this warns the user while there are unsaved/in-flight
358473
358554
  // changes. Registered before the early-return so it always installs.
358474
358555
  window.addEventListener('beforeunload', this.onBeforeUnload);
358556
+ window.addEventListener(OBJECTS_CHANGED_EVENT, this.onObjectsChanged);
358475
358557
 
358476
358558
  // Opt-out fast-path: empty `requiresApps` already initialised
358477
358559
  // `capabilitiesLoading` to `false` in data(); skip the check.
@@ -358517,6 +358599,7 @@ var script$h = {
358517
358599
  this.removeDiagnostics = null;
358518
358600
  }
358519
358601
  window.removeEventListener('beforeunload', this.onBeforeUnload);
358602
+ window.removeEventListener(OBJECTS_CHANGED_EVENT, this.onObjectsChanged);
358520
358603
  this.cnScopedTheme.teardown(this.appId);
358521
358604
  // Scope the side effect to the shell's lifetime. Layers already written
358522
358605
  // stay on screen (see `uninstallModalStack`), so a dialog outliving the
@@ -359363,13 +359446,17 @@ var script$h = {
359363
359446
  * response `total` written to `cnMenuItemCounts[item.id]`. The
359364
359447
  * request goes straight through axios rather than the object store,
359365
359448
  * so the index page's whole-schema total in the store is left alone.
359366
- * Failures leave the badge unrendered.
359449
+ * Failures leave the badge unrendered. Given a changed register and
359450
+ * schema, only the entries counting that list are read again.
359367
359451
  *
359368
359452
  * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-menu-entry-counts-a-filtered-list
359453
+ * @param {{register?: string|null, schema?: string|null}|null} [changed] The list that changed; null reads every count.
359369
359454
  * @return {void}
359370
359455
  * @private
359371
359456
  */
359372
- _hydrateMenuItemCounts() {
359457
+ _hydrateMenuItemCounts(changed = null) {
359458
+ const register = changed?.register ?? null;
359459
+ const schema = changed?.schema ?? null;
359373
359460
  const collect = (items) => {
359374
359461
  const out = [];
359375
359462
  for (const item of items ?? []) {
@@ -359384,10 +359471,27 @@ var script$h = {
359384
359471
  return out
359385
359472
  };
359386
359473
  for (const item of collect(this.manifest?.menu)) {
359387
- this._fetchMenuItemCount(item);
359474
+ if ((register === null || item.count.register === register) && (schema === null || item.count.schema === schema)) {
359475
+ this._fetchMenuItemCount(item);
359476
+ }
359388
359477
  }
359389
359478
  },
359390
359479
 
359480
+ /**
359481
+ * Objects changed outside the list showing them (`cn:objects-changed`,
359482
+ * see `dispatchObjectsChanged`): read the filtered menu counts of that
359483
+ * register and schema again, so a badge follows a claim, a close or a
359484
+ * bulk action instead of keeping the number it had at mount. A signal
359485
+ * without a register or schema reads every filtered count again.
359486
+ *
359487
+ * @spec openspec/changes/menu-counts-follow-changed-objects/specs/layout-components/spec.md#requirement-a-filtered-menu-count-follows-changed-objects
359488
+ * @param {CustomEvent} event The signal, with `detail: { register, schema, id }`.
359489
+ * @return {void}
359490
+ */
359491
+ onObjectsChanged(event) {
359492
+ this._hydrateMenuItemCounts(event?.detail ?? null);
359493
+ },
359494
+
359391
359495
  /**
359392
359496
  * One filtered-count request for a menu entry; see
359393
359497
  * `_hydrateMenuItemCounts`.
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/dist/nextcloud-vue.cjs.js.map
3
- size: 40756512 bytes
4
- sha256: 944995c0769e4a09afde370b0f98c983e7749487f9a29ab5a1f49e7191a0c530
3
+ size: 40766945 bytes
4
+ sha256: 4d5c39c1fefeba7aa7f2181f991b71ad3354d5678ea63646f53060e929223d66
@@ -34796,28 +34796,28 @@ to {
34796
34796
  }
34797
34797
 
34798
34798
 
34799
- .cn-admin-settings-shell[data-v-0f41828c] {
34799
+ .cn-admin-settings-shell[data-v-c3ceda64] {
34800
34800
  max-width: 900px;
34801
34801
  }
34802
34802
 
34803
34803
  /* The board look lifts the width and lays the header and cards on a grid (look-board.css). */
34804
- .cn-support-info[data-v-0f41828c] {
34804
+ .cn-support-info[data-v-c3ceda64] {
34805
34805
  margin-top: 16px;
34806
34806
  padding-top: 16px;
34807
34807
  border-top: 1px solid var(--color-border);
34808
34808
  }
34809
- .cn-support-info h4[data-v-0f41828c] {
34809
+ .cn-support-info h4[data-v-c3ceda64] {
34810
34810
  margin: 0 0 8px 0;
34811
34811
  font-size: 14px;
34812
34812
  font-weight: 600;
34813
34813
  color: var(--color-main-text);
34814
34814
  }
34815
- .cn-support-info p[data-v-0f41828c] {
34815
+ .cn-support-info p[data-v-c3ceda64] {
34816
34816
  margin: 4px 0;
34817
34817
  color: var(--color-text-maxcontrast);
34818
34818
  font-size: 14px;
34819
34819
  }
34820
- .cn-support-info a[data-v-0f41828c] {
34820
+ .cn-support-info a[data-v-c3ceda64] {
34821
34821
  color: var(--color-primary-element);
34822
34822
  }
34823
34823
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.88.0",
3
+ "version": "2.89.1",
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": {
@@ -232,6 +232,43 @@ function readTabFromAddress(key) {
232
232
  return new URLSearchParams(window.location.search).get(key) || ''
233
233
  }
234
234
 
235
+ /**
236
+ * The tab remembered under a storage key, or '' without a key, a value or
237
+ * usable storage (a private window can refuse it).
238
+ *
239
+ * @param {string} key The storage key.
240
+ * @return {string} The remembered tab id.
241
+ */
242
+ function readStoredTab(key) {
243
+ if (!key) {
244
+ return ''
245
+ }
246
+ try {
247
+ return window.localStorage.getItem(key) || ''
248
+ } catch {
249
+ return ''
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Remember the open tab under a storage key; nothing without a key or
255
+ * usable storage.
256
+ *
257
+ * @param {string} key The storage key.
258
+ * @param {string} id The tab's id.
259
+ * @return {void}
260
+ */
261
+ function storeTab(key, id) {
262
+ if (!key) {
263
+ return
264
+ }
265
+ try {
266
+ window.localStorage.setItem(key, id)
267
+ } catch {
268
+ // Storage refused (private window): the address still holds the tab.
269
+ }
270
+ }
271
+
235
272
  /**
236
273
  * Put the open tab in the address without a new history entry.
237
274
  *
@@ -442,6 +479,18 @@ export default {
442
479
  default: 'tab',
443
480
  },
444
481
 
482
+ /**
483
+ * A browser storage key under which the shell remembers the last
484
+ * opened tab, so the page opens on it again when the address names no
485
+ * tab. Empty (the default) remembers nothing. The address still wins.
486
+ *
487
+ * @spec openspec/changes/admin-shell-remembers-tab/specs/settings-board-look/spec.md#requirement-the-admin-shell-can-remember-the-last-tab
488
+ */
489
+ tabStorageKey: {
490
+ type: String,
491
+ default: '',
492
+ },
493
+
445
494
  /**
446
495
  * How the `tabs` strip is drawn, as CnTabs' `variant`: `line`, the
447
496
  * folder tabs (the default), or `underline`, plain labels on a rule
@@ -614,7 +663,7 @@ export default {
614
663
  reimporting: false,
615
664
  setupWizardOpen: false,
616
665
  // The open tab's id; empty until a tab is picked or the address names one.
617
- chosenTabId: readTabFromAddress(this.tabQueryKey),
666
+ chosenTabId: readTabFromAddress(this.tabQueryKey) || readStoredTab(this.tabStorageKey),
618
667
  }
619
668
  },
620
669
 
@@ -774,6 +823,19 @@ export default {
774
823
  },
775
824
  },
776
825
 
826
+ /**
827
+ * A page that opens on a tab other than the first (named in the address,
828
+ * or remembered) tells the app once, as a click would, so a breadcrumb or
829
+ * title that follows the tab is right from the first paint.
830
+ *
831
+ * @spec openspec/changes/admin-shell-remembers-tab/specs/settings-board-look/spec.md#requirement-the-admin-shell-can-remember-the-last-tab
832
+ */
833
+ mounted() {
834
+ if (this.tabItems.length > 0 && this.activeTabId !== this.tabItems[0].id) {
835
+ this.$emit('tab-change', this.activeTabId)
836
+ }
837
+ },
838
+
777
839
  methods: {
778
840
  /**
779
841
  * A tab was picked in the strip: remember it, put it in the address
@@ -790,6 +852,7 @@ export default {
790
852
  }
791
853
  this.chosenTabId = tab.id
792
854
  writeTabToAddress(this.tabQueryKey, tab.id)
855
+ storeTab(this.tabStorageKey, tab.id)
793
856
  /**
794
857
  * @event tab-change Emitted when the reader opens another tab. Payload: its `id`.
795
858
  */
@@ -740,6 +740,7 @@ import { DEFAULT_FORGE, resolveForge } from '../../utils/forge.js'
740
740
  import { BUILT_IN_KB_PROVIDERS } from '../../utils/kbSearchProviders.js'
741
741
  import { createManifestTranslate } from '../../utils/manifestTranslate.js'
742
742
  import { installModalStack, uninstallModalStack } from '../../utils/modalStack.js'
743
+ import { OBJECTS_CHANGED_EVENT } from '../../utils/objectSignals.js'
743
744
  import { resolveBrand } from '../../utils/resolveBrand.js'
744
745
  import { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
745
746
  import { passesContextPredicates } from '../../utils/visibleIfContext.js'
@@ -3221,6 +3222,7 @@ export default {
3221
3222
  // write lands; this warns the user while there are unsaved/in-flight
3222
3223
  // changes. Registered before the early-return so it always installs.
3223
3224
  window.addEventListener('beforeunload', this.onBeforeUnload)
3225
+ window.addEventListener(OBJECTS_CHANGED_EVENT, this.onObjectsChanged)
3224
3226
 
3225
3227
  // Opt-out fast-path: empty `requiresApps` already initialised
3226
3228
  // `capabilitiesLoading` to `false` in data(); skip the check.
@@ -3266,6 +3268,7 @@ export default {
3266
3268
  this.removeDiagnostics = null
3267
3269
  }
3268
3270
  window.removeEventListener('beforeunload', this.onBeforeUnload)
3271
+ window.removeEventListener(OBJECTS_CHANGED_EVENT, this.onObjectsChanged)
3269
3272
  this.cnScopedTheme.teardown(this.appId)
3270
3273
  // Scope the side effect to the shell's lifetime. Layers already written
3271
3274
  // stay on screen (see `uninstallModalStack`), so a dialog outliving the
@@ -4112,13 +4115,17 @@ export default {
4112
4115
  * response `total` written to `cnMenuItemCounts[item.id]`. The
4113
4116
  * request goes straight through axios rather than the object store,
4114
4117
  * so the index page's whole-schema total in the store is left alone.
4115
- * Failures leave the badge unrendered.
4118
+ * Failures leave the badge unrendered. Given a changed register and
4119
+ * schema, only the entries counting that list are read again.
4116
4120
  *
4117
4121
  * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-menu-entry-counts-a-filtered-list
4122
+ * @param {{register?: string|null, schema?: string|null}|null} [changed] The list that changed; null reads every count.
4118
4123
  * @return {void}
4119
4124
  * @private
4120
4125
  */
4121
- _hydrateMenuItemCounts() {
4126
+ _hydrateMenuItemCounts(changed = null) {
4127
+ const register = changed?.register ?? null
4128
+ const schema = changed?.schema ?? null
4122
4129
  const collect = (items) => {
4123
4130
  const out = []
4124
4131
  for (const item of items ?? []) {
@@ -4133,10 +4140,27 @@ export default {
4133
4140
  return out
4134
4141
  }
4135
4142
  for (const item of collect(this.manifest?.menu)) {
4136
- this._fetchMenuItemCount(item)
4143
+ if ((register === null || item.count.register === register) && (schema === null || item.count.schema === schema)) {
4144
+ this._fetchMenuItemCount(item)
4145
+ }
4137
4146
  }
4138
4147
  },
4139
4148
 
4149
+ /**
4150
+ * Objects changed outside the list showing them (`cn:objects-changed`,
4151
+ * see `dispatchObjectsChanged`): read the filtered menu counts of that
4152
+ * register and schema again, so a badge follows a claim, a close or a
4153
+ * bulk action instead of keeping the number it had at mount. A signal
4154
+ * without a register or schema reads every filtered count again.
4155
+ *
4156
+ * @spec openspec/changes/menu-counts-follow-changed-objects/specs/layout-components/spec.md#requirement-a-filtered-menu-count-follows-changed-objects
4157
+ * @param {CustomEvent} event The signal, with `detail: { register, schema, id }`.
4158
+ * @return {void}
4159
+ */
4160
+ onObjectsChanged(event) {
4161
+ this._hydrateMenuItemCounts(event?.detail ?? null)
4162
+ },
4163
+
4140
4164
  /**
4141
4165
  * One filtered-count request for a menu entry; see
4142
4166
  * `_hydrateMenuItemCounts`.
@@ -4,6 +4,7 @@
4
4
  -->
5
5
  <template>
6
6
  <CnFormWidgetBase
7
+ v-bind="rootAttrs"
7
8
  blockClass="cn-interaction-form-widget"
8
9
  :fields="formFields"
9
10
  :model="form"
@@ -51,6 +52,7 @@
51
52
  <script>
52
53
  import { translate as t } from '@nextcloud/l10n'
53
54
  import { NcButton } from '@nextcloud/vue'
55
+ import { camelize } from 'vue'
54
56
  import OpenInNew from 'vue-material-design-icons/OpenInNew.vue'
55
57
  import CnFormWidgetBase from '../CnFormWidgetBase/CnFormWidgetBase.vue'
56
58
  import CnResourceSelect from '../CnResourceSelect/CnResourceSelect.vue'
@@ -125,6 +127,11 @@ export default {
125
127
  cnTranslate: { default: () => (key) => key },
126
128
  },
127
129
 
130
+ // A dashboard binds the widget's `content` keys as attributes too. Falling
131
+ // through, `submitLabel` would reach CnFormWidgetBase and win over the
132
+ // translated label the widget passes, so attributes go through rootAttrs.
133
+ inheritAttrs: false,
134
+
128
135
  props: {
129
136
  /**
130
137
  * Persisted configuration blob (see component description for the shape).
@@ -160,6 +167,19 @@ export default {
160
167
  },
161
168
 
162
169
  computed: {
170
+ /**
171
+ * The attributes for the root form: everything the widget was given,
172
+ * minus the names CnFormWidgetBase takes as props, which the widget
173
+ * sets itself.
174
+ *
175
+ * @spec openspec/changes/interaction-form-fits-and-reads-dutch/specs/interaction-form-widget/spec.md#requirement-the-interaction-form-fits-its-card-and-reads-in-the-readers-language
176
+ * @return {object} The attributes to bind.
177
+ */
178
+ rootAttrs() {
179
+ const own = new Set(Object.keys(CnFormWidgetBase.props || {}))
180
+ return Object.fromEntries(Object.entries(this.$attrs).filter(([key]) => !own.has(camelize(key))))
181
+ },
182
+
163
183
  register() {
164
184
  return this.content.register || 'pipelinq'
165
185
  },