@conduction/nextcloud-vue 2.56.0-beta.48 → 2.56.0-beta.49

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.
Files changed (72) hide show
  1. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +46 -28
  2. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +17 -10
  4. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  5. package/dist/esm/components/CnBrandBar/CnBrandBar.vue2.js.map +1 -1
  6. package/dist/esm/components/CnBrandBar/CnBrandBar.vue3.js.map +1 -1
  7. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +56 -16
  8. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  9. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  10. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue.js +9 -0
  11. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue.js.map +1 -0
  12. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue2.js +240 -0
  13. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue2.js.map +1 -0
  14. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue3.js +65 -0
  15. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue3.js.map +1 -0
  16. package/dist/esm/components/CnDeviceFramesWidget/index.js +30 -0
  17. package/dist/esm/components/CnDeviceFramesWidget/index.js.map +1 -0
  18. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  19. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +17 -1
  20. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  21. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +30 -31
  22. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  23. package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue.js +6 -0
  24. package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue.js.map +1 -0
  25. package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue2.js +90 -0
  26. package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue2.js.map +1 -0
  27. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  28. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +48 -1
  29. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  30. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +20 -2
  31. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  32. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
  33. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +230 -25
  34. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
  35. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js +447 -221
  36. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
  37. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  38. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  39. package/dist/esm/index.js +1 -0
  40. package/dist/esm/index.js.map +1 -1
  41. package/dist/esm/l10n/en.json.js +18 -1
  42. package/dist/esm/l10n/en.json.js.map +1 -1
  43. package/dist/esm/l10n/nl.json.js +18 -1
  44. package/dist/esm/l10n/nl.json.js.map +1 -1
  45. package/dist/esm/library-style.js +1 -1
  46. package/dist/esm/nextcloud-vue.css +356 -114
  47. package/dist/esm/utils/libraryWidgetKeys.js +1 -0
  48. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  49. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  50. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  51. package/dist/nextcloud-vue.cjs.js +6964 -5943
  52. package/dist/nextcloud-vue.cjs.js.map +2 -2
  53. package/dist/nextcloud-vue.css +356 -114
  54. package/l10n/en.json +18 -1
  55. package/l10n/nl.json +18 -1
  56. package/package.json +1 -1
  57. package/src/components/CnAppRoot/CnAppRoot.vue +20 -0
  58. package/src/components/CnBrandBar/CnBrandBar.vue +5 -4
  59. package/src/components/CnDetailPage/CnDetailPage.vue +55 -15
  60. package/src/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue +360 -0
  61. package/src/components/CnDeviceFramesWidget/index.js +29 -0
  62. package/src/components/CnFactsWidget/CnFactsWidget.vue +46 -2
  63. package/src/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue +107 -0
  64. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +81 -1
  65. package/src/components/CnRegisterMapping/CnRegisterMapping.md +10 -0
  66. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +511 -28
  67. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  68. package/src/components/index.js +1 -0
  69. package/src/index.js +1 -0
  70. package/src/schemas/app-manifest-v2.schema.json +6 -1
  71. package/src/utils/libraryWidgetKeys.js +1 -0
  72. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -9,9 +9,9 @@
9
9
  :errorMessage="registersError || ''"
10
10
  :onRetry="loadRegisters">
11
11
  <!-- Action buttons -->
12
- <template #actions>
12
+ <template v-if="hasHeaderActions" #actions>
13
13
  <NcButton
14
- v-if="showSaveButton"
14
+ v-if="showSaveButton && !isBoardLook"
15
15
  variant="primary"
16
16
  :disabled="saving || !hasChanges"
17
17
  @click="handleSave">
@@ -36,7 +36,132 @@
36
36
  </template>
37
37
 
38
38
  <!-- Group sections -->
39
- <div v-if="!registersLoading && !registersError" class="cn-register-mapping">
39
+ <div
40
+ v-if="isBoardLook && !registersLoading && !registersError"
41
+ class="cn-register-mapping cn-register-mapping--board"
42
+ data-testid="cn-register-mapping-board">
43
+ <div class="cn-register-mapping__board-group" data-testid="cn-register-mapping-board-group">
44
+ <div class="cn-register-mapping__board-head">
45
+ <h3 class="cn-register-mapping__board-title">
46
+ {{ activeGroup.name }}
47
+ </h3>
48
+ <CnStatusBadge
49
+ :label="groupStatusLabel(activeGroupIdx, true)"
50
+ :variant="groupStatusVariant(activeGroupIdx)"
51
+ data-testid="cn-register-mapping-board-status" />
52
+ </div>
53
+ <p v-if="activeGroup.description" class="cn-register-mapping__group-description">
54
+ {{ activeGroup.description }}
55
+ </p>
56
+ <div class="cn-register-mapping__register-select cn-register-mapping__board-select">
57
+ <NcSelect
58
+ :modelValue="selectedRegister(activeGroupIdx)"
59
+ :options="registerSelectOptions"
60
+ :placeholder="labels.selectRegister"
61
+ :inputLabel="labels.register"
62
+ label="label"
63
+ trackBy="value"
64
+ @update:modelValue="handleRegisterChange(activeGroupIdx, $event)" />
65
+ </div>
66
+ <table v-if="selectedRegister(activeGroupIdx)" class="cn-register-mapping__board-table">
67
+ <thead>
68
+ <tr>
69
+ <th scope="col">
70
+ {{ t('nextcloud-vue', 'Object type') }}
71
+ </th>
72
+ <th scope="col">
73
+ {{ labels.schema }}
74
+ </th>
75
+ </tr>
76
+ </thead>
77
+ <tbody>
78
+ <template v-for="type in boardVisibleTypes" :key="type.slug + '-board'">
79
+ <tr data-testid="cn-register-mapping-board-row">
80
+ <td>{{ type.label }}</td>
81
+ <td>
82
+ <button
83
+ type="button"
84
+ class="cn-register-mapping__board-schema"
85
+ :aria-expanded="String(isExpanded(activeGroupIdx, type.slug))"
86
+ :aria-label="t('nextcloud-vue', 'Change the schema for {type}', { type: type.label })"
87
+ @click="toggleExpand(activeGroupIdx, type.slug)">
88
+ <CheckCircle
89
+ v-if="schemaValue(activeGroupIdx, type)"
90
+ class="cn-register-mapping__board-check"
91
+ :size="16" />
92
+ {{ schemaLabel(activeGroupIdx, type) || t('nextcloud-vue', 'Not set') }}
93
+ </button>
94
+ </td>
95
+ </tr>
96
+ <tr v-if="isExpanded(activeGroupIdx, type.slug)" class="cn-register-mapping__board-detail">
97
+ <td colspan="2">
98
+ <p v-if="type.description" class="cn-register-mapping__type-description">
99
+ {{ type.description }}
100
+ </p>
101
+ <NcSelect
102
+ :modelValue="selectedSchema(activeGroupIdx, type)"
103
+ :options="schemaSelectOptions(activeGroupIdx)"
104
+ :placeholder="labels.selectSchema"
105
+ :inputLabel="labels.schema"
106
+ label="label"
107
+ trackBy="value"
108
+ @update:modelValue="handleSchemaChange(activeGroupIdx, type, $event)" />
109
+ </td>
110
+ </tr>
111
+ </template>
112
+ </tbody>
113
+ </table>
114
+ <p
115
+ v-if="selectedRegister(activeGroupIdx) && boardHiddenTypes.length > 0"
116
+ class="cn-register-mapping__board-more"
117
+ data-testid="cn-register-mapping-board-more">
118
+ {{ boardHiddenSummary }}
119
+ <button type="button" class="cn-register-mapping__board-link" @click="showAllTypes = true">
120
+ {{ t('nextcloud-vue', 'Show all types') }}
121
+ </button>
122
+ </p>
123
+ <NcNoteCard v-if="!selectedRegister(activeGroupIdx)" type="info">
124
+ {{ labels.selectRegister }}
125
+ </NcNoteCard>
126
+ <NcNoteCard
127
+ v-if="selectedRegister(activeGroupIdx) && schemaSelectOptions(activeGroupIdx).length === 0"
128
+ type="warning">
129
+ {{ labels.noSchemas }}
130
+ </NcNoteCard>
131
+ </div>
132
+ <ul v-if="boardOtherGroups.length > 0" class="cn-register-mapping__board-groups">
133
+ <li v-for="item in boardOtherGroups" :key="item.idx">
134
+ <button
135
+ type="button"
136
+ class="cn-register-mapping__board-group-row"
137
+ data-testid="cn-register-mapping-board-group-row"
138
+ @click="openBoardGroup(item.idx)">
139
+ <ChevronRight :size="18" class="cn-register-mapping__board-chevron" />
140
+ <strong class="cn-register-mapping__board-group-name">{{ item.group.name }}</strong>
141
+ <span class="cn-register-mapping__board-group-count">
142
+ {{ n('nextcloud-vue', '%n type', '%n types', item.group.types.length) }}
143
+ </span>
144
+ <CnStatusBadge
145
+ :label="groupStatusLabel(item.idx, false)"
146
+ :variant="groupStatusVariant(item.idx)" />
147
+ </button>
148
+ </li>
149
+ </ul>
150
+ <div v-if="showSaveButton" class="cn-register-mapping__board-save">
151
+ <NcButton
152
+ variant="primary"
153
+ :disabled="saving || !hasChanges"
154
+ data-testid="cn-register-mapping-board-save"
155
+ @click="handleSave">
156
+ <template v-if="saving" #icon>
157
+ <NcLoadingIcon :size="20" />
158
+ </template>
159
+ {{ saving ? t('nextcloud-vue', 'Saving…') : saveButtonText }}
160
+ </NcButton>
161
+ </div>
162
+ </div>
163
+
164
+ <div v-else-if="!registersLoading && !registersError" class="cn-register-mapping">
40
165
  <div
41
166
  v-for="(group, groupIdx) in groups"
42
167
  :key="groupIdx"
@@ -186,14 +311,18 @@
186
311
  </template>
187
312
 
188
313
  <script>
189
- import { translate as t } from '@nextcloud/l10n'
314
+ import { translatePlural as n, translate as t } from '@nextcloud/l10n'
190
315
  import { NcButton, NcLoadingIcon, NcNoteCard, NcSelect } from '@nextcloud/vue'
316
+ import CheckCircle from 'vue-material-design-icons/CheckCircle.vue'
191
317
  import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'
318
+ import ChevronRight from 'vue-material-design-icons/ChevronRight.vue'
192
319
  import ChevronUp from 'vue-material-design-icons/ChevronUp.vue'
193
320
  import ContentSave from 'vue-material-design-icons/ContentSave.vue'
194
321
  import Refresh from 'vue-material-design-icons/Refresh.vue'
322
+ import { normalizeLook } from '../../composables/useLook.js'
195
323
  import { buildHeaders, prefixUrl } from '../../utils/headers.js'
196
324
  import { CnSettingsSection } from '../CnSettingsSection/index.js'
325
+ import { CnStatusBadge } from '../CnStatusBadge/index.js'
197
326
 
198
327
  let mappingCount = 0
199
328
 
@@ -205,6 +334,13 @@ let mappingCount = 0
205
334
  * Supports multiple register groups (stacked sections) with expandable
206
335
  * type rows for manual schema override.
207
336
  *
337
+ * Only the registers the configuration maps have their schemas fetched; the
338
+ * register select lists every register without its schemas.
339
+ *
340
+ * In the board look one group is open as a type/schema table under a status
341
+ * chip, the other groups are rows with their type count and status, and the
342
+ * save button closes the section.
343
+ *
208
344
  * Single register (Pipelinq)
209
345
  * ```vue
210
346
  * <CnRegisterMapping
@@ -238,16 +374,24 @@ export default {
238
374
 
239
375
  components: {
240
376
  CnSettingsSection,
377
+ CnStatusBadge,
241
378
  NcButton,
242
379
  NcLoadingIcon,
243
380
  NcNoteCard,
244
381
  NcSelect,
245
382
  ContentSave,
246
383
  Refresh,
384
+ CheckCircle,
247
385
  ChevronDown,
386
+ ChevronRight,
248
387
  ChevronUp,
249
388
  },
250
389
 
390
+ inject: {
391
+ /** The look CnAppRoot or CnAdminSettingsShell provides. */
392
+ cnLook: { default: 'nextcloud' },
393
+ },
394
+
251
395
  props: {
252
396
  /** Section title */
253
397
  name: {
@@ -335,6 +479,28 @@ export default {
335
479
  default: null,
336
480
  },
337
481
 
482
+ /**
483
+ * The look the mapping is drawn in: `board` or `nextcloud`. Empty
484
+ * follows the provided `cnLook`.
485
+ *
486
+ * @spec openspec/changes/register-mapping-board-look/specs/register-mapping/spec.md#requirement-the-board-look-draws-one-open-group-and-compact-rows
487
+ */
488
+ look: {
489
+ type: String,
490
+ default: '',
491
+ },
492
+
493
+ /**
494
+ * Board look: how many types the open group lists before a "show all"
495
+ * line. 0 lists every type.
496
+ *
497
+ * @spec openspec/changes/register-mapping-board-look/specs/register-mapping/spec.md#requirement-the-board-look-draws-one-open-group-and-compact-rows
498
+ */
499
+ boardRowLimit: {
500
+ type: Number,
501
+ default: 4,
502
+ },
503
+
338
504
  /** Auto-match schema titles to type slugs on register change */
339
505
  autoMatch: {
340
506
  type: Boolean,
@@ -372,6 +538,12 @@ export default {
372
538
  expandedRows: {},
373
539
  // The groups opened by the reader, by index.
374
540
  openGroups: {},
541
+ // Board look: the group drawn open, and whether it lists every type.
542
+ activeGroupIdx: 0,
543
+ // Schema requests by register id, so a register is fetched once.
544
+ schemaRequests: {},
545
+ registersLoaded: false,
546
+ showAllTypes: false,
375
547
  uid: ++mappingCount,
376
548
  }
377
549
  },
@@ -395,6 +567,79 @@ export default {
395
567
  return this.collapseGroups === null ? this.groups.length > 1 : this.collapseGroups
396
568
  },
397
569
 
570
+ /**
571
+ * Whether the board look applies.
572
+ *
573
+ * @return {boolean}
574
+ */
575
+ isBoardLook() {
576
+ return normalizeLook(this.look || this.cnLook) === 'board'
577
+ },
578
+
579
+ /**
580
+ * The group the board look draws open.
581
+ *
582
+ * @return {object}
583
+ */
584
+ activeGroup() {
585
+ return this.groups[this.activeGroupIdx] || this.groups[0]
586
+ },
587
+
588
+ /**
589
+ * The open group's types the board table lists.
590
+ *
591
+ * @return {Array<object>}
592
+ */
593
+ boardVisibleTypes() {
594
+ const types = this.activeGroup.types
595
+ if (this.showAllTypes || this.boardRowLimit <= 0) {
596
+ return types
597
+ }
598
+ return types.slice(0, this.boardRowLimit)
599
+ },
600
+
601
+ /**
602
+ * The open group's types below the row limit.
603
+ *
604
+ * @return {Array<object>}
605
+ */
606
+ boardHiddenTypes() {
607
+ return this.activeGroup.types.slice(this.boardVisibleTypes.length)
608
+ },
609
+
610
+ /**
611
+ * The line under the board table that counts the types not listed.
612
+ *
613
+ * @return {string}
614
+ */
615
+ boardHiddenSummary() {
616
+ const count = this.boardHiddenTypes.length
617
+ const allSet = this.boardHiddenTypes.every((type) => !!this.schemaValue(this.activeGroupIdx, type))
618
+ return allSet
619
+ ? n('nextcloud-vue', 'And %n more type in this group, set up.', 'And %n more types in this group, all set up.', count)
620
+ : n('nextcloud-vue', 'And %n more type in this group.', 'And %n more types in this group.', count)
621
+ },
622
+
623
+ /**
624
+ * The groups the board look draws as compact rows, with their index.
625
+ *
626
+ * @return {Array<{group: object, idx: number}>}
627
+ */
628
+ boardOtherGroups() {
629
+ return this.groups
630
+ .map((group, idx) => ({ group, idx }))
631
+ .filter((item) => item.idx !== this.activeGroupIdx)
632
+ },
633
+
634
+ /**
635
+ * Whether the section header draws buttons; the board look saves at the end.
636
+ *
637
+ * @return {boolean}
638
+ */
639
+ hasHeaderActions() {
640
+ return (this.showSaveButton && !this.isBoardLook) || this.showReimportButton || !!this.$slots.actions
641
+ },
642
+
398
643
  /** Whether local config differs from prop */
399
644
  hasChanges() {
400
645
  return JSON.stringify(this.localConfig) !== JSON.stringify(this.configuration)
@@ -405,11 +650,20 @@ export default {
405
650
  configuration: {
406
651
  handler(newVal) {
407
652
  this.localConfig = { ...newVal }
653
+ if (this.registersLoaded) {
654
+ this.mappedRegisterIds().forEach((id) => this.loadSchemasForRegister(id))
655
+ }
408
656
  },
409
657
 
410
658
  immediate: true,
411
659
  deep: true,
412
660
  },
661
+
662
+ groups() {
663
+ if (this.activeGroupIdx >= this.groups.length) {
664
+ this.activeGroupIdx = 0
665
+ }
666
+ },
413
667
  },
414
668
 
415
669
  async mounted() {
@@ -418,6 +672,65 @@ export default {
418
672
 
419
673
  methods: {
420
674
  t,
675
+ n,
676
+
677
+ /**
678
+ * The status chip text of a group: every type set up, none, or how many.
679
+ *
680
+ * @param {number} groupIdx Group index
681
+ * @param {boolean} open Whether the group is the open one, which says "All types set up"
682
+ * @return {string}
683
+ * @spec openspec/changes/register-mapping-board-look/specs/register-mapping/spec.md#requirement-the-board-look-draws-one-open-group-and-compact-rows
684
+ */
685
+ groupStatusLabel(groupIdx, open) {
686
+ const configured = this.configuredCount(groupIdx)
687
+ const total = this.groups[groupIdx].types.length
688
+ if (configured === total) {
689
+ return open ? t('nextcloud-vue', 'All types set up') : t('nextcloud-vue', 'Set up')
690
+ }
691
+ if (configured === 0) {
692
+ return t('nextcloud-vue', 'Not set')
693
+ }
694
+ return t('nextcloud-vue', '{configured} of {total} set up', { configured, total })
695
+ },
696
+
697
+ /**
698
+ * The status chip variant of a group.
699
+ *
700
+ * @param {number} groupIdx Group index
701
+ * @return {'success'|'warning'|'default'}
702
+ */
703
+ groupStatusVariant(groupIdx) {
704
+ const configured = this.configuredCount(groupIdx)
705
+ if (configured === this.groups[groupIdx].types.length) {
706
+ return 'success'
707
+ }
708
+ return configured === 0 ? 'default' : 'warning'
709
+ },
710
+
711
+ /**
712
+ * Board look: draw another group open.
713
+ *
714
+ * @param {number} groupIdx Group index
715
+ * @return {void}
716
+ * @spec openspec/changes/register-mapping-board-look/specs/register-mapping/spec.md#requirement-the-board-look-draws-one-open-group-and-compact-rows
717
+ */
718
+ openBoardGroup(groupIdx) {
719
+ this.activeGroupIdx = groupIdx
720
+ this.showAllTypes = false
721
+ },
722
+
723
+ /**
724
+ * The register ids the configuration maps, one per distinct value.
725
+ *
726
+ * @return {string[]}
727
+ */
728
+ mappedRegisterIds() {
729
+ const ids = this.groups
730
+ .map((group, idx) => String(this.localConfig[this.registerConfigKey(idx)] || ''))
731
+ .filter(Boolean)
732
+ return [...new Set(ids)]
733
+ },
421
734
 
422
735
  /**
423
736
  * Whether a group's body shows.
@@ -675,17 +988,24 @@ export default {
675
988
  },
676
989
 
677
990
  /**
678
- * Fetch all registers from OpenRegister API.
991
+ * Fetch the registers for the select, without their schemas, and the
992
+ * schemas of the registers the configuration maps. Expanding the schemas
993
+ * of every register costs megabytes on a busy instance.
994
+ *
995
+ * @spec openspec/changes/register-mapping-board-look/specs/register-mapping/spec.md#requirement-only-mapped-registers-have-their-schemas-fetched
679
996
  */
680
997
  async loadRegisters() {
681
998
  this.registersLoading = true
682
999
  this.registersError = null
683
1000
 
684
1001
  try {
685
- const response = await fetch(prefixUrl('/apps/openregister/api/registers?_extend[]=schemas'), {
686
- method: 'GET',
687
- headers: buildHeaders(),
688
- })
1002
+ const [response] = await Promise.all([
1003
+ fetch(prefixUrl('/apps/openregister/api/registers'), {
1004
+ method: 'GET',
1005
+ headers: buildHeaders(),
1006
+ }),
1007
+ ...this.mappedRegisterIds().map((id) => this.loadSchemasForRegister(id)),
1008
+ ])
689
1009
 
690
1010
  if (!response.ok) {
691
1011
  this.registersError = t('nextcloud-vue', 'The registers could not be loaded: {reason}', { reason: response.statusText || response.status })
@@ -695,19 +1015,7 @@ export default {
695
1015
  const data = await response.json()
696
1016
  const results = data.results || data
697
1017
  this.registers = Array.isArray(results) ? results : []
698
-
699
- // Cache expanded schemas
700
- for (const reg of this.registers) {
701
- if (Array.isArray(reg.schemas) && reg.schemas.length > 0) {
702
- const schemas = reg.schemas.filter((s) => s && typeof s === 'object' && s.id)
703
- if (schemas.length > 0) {
704
- this.schemasByRegister = {
705
- ...this.schemasByRegister,
706
- [String(reg.id)]: schemas,
707
- }
708
- }
709
- }
710
- }
1018
+ this.registersLoaded = true
711
1019
  } catch (error) {
712
1020
  this.registersError = error.message || t('nextcloud-vue', 'The registers could not be loaded: {reason}', { reason: t('nextcloud-vue', 'no connection') })
713
1021
  } finally {
@@ -716,32 +1024,54 @@ export default {
716
1024
  },
717
1025
 
718
1026
  /**
719
- * Fetch schemas for a specific register.
1027
+ * Fetch the schemas of one register, through `registers/{id}/schemas`,
1028
+ * or the register with its schemas expanded where that route is missing.
720
1029
  *
721
1030
  * @param {string} registerId Register ID
1031
+ * @spec openspec/changes/register-mapping-board-look/specs/register-mapping/spec.md#requirement-only-mapped-registers-have-their-schemas-fetched
722
1032
  */
723
1033
  async loadSchemasForRegister(registerId) {
724
1034
  const id = String(registerId)
725
1035
 
726
- // Return cached
727
1036
  if (this.schemasByRegister[id]?.length > 0) {
728
1037
  return
729
1038
  }
1039
+ if (!this.schemaRequests[id]) {
1040
+ this.schemaRequests[id] = this.fetchSchemas(id).finally(() => {
1041
+ delete this.schemaRequests[id]
1042
+ })
1043
+ }
1044
+ await this.schemaRequests[id]
1045
+ },
730
1046
 
1047
+ /**
1048
+ * One schema request for a register; fills `schemasByRegister`.
1049
+ *
1050
+ * @param {string} id Register ID
1051
+ * @return {Promise<void>}
1052
+ */
1053
+ async fetchSchemas(id) {
731
1054
  try {
732
- const response = await fetch(
733
- prefixUrl(`/apps/openregister/api/registers/${encodeURIComponent(id)}?_extend[]=schemas`),
1055
+ let response = await fetch(
1056
+ prefixUrl(`/apps/openregister/api/registers/${encodeURIComponent(id)}/schemas`),
734
1057
  { method: 'GET', headers: buildHeaders() },
735
1058
  )
1059
+ if (response.status === 404) {
1060
+ response = await fetch(
1061
+ prefixUrl(`/apps/openregister/api/registers/${encodeURIComponent(id)}?_extend[]=schemas`),
1062
+ { method: 'GET', headers: buildHeaders() },
1063
+ )
1064
+ }
736
1065
  if (!response.ok) {
737
1066
  return
738
1067
  }
739
1068
 
740
1069
  const data = await response.json()
741
- const schemas = (data.schemas || []).filter((s) => s && typeof s === 'object' && s.id)
1070
+ const list = data.results || data.schemas || []
1071
+ const schemas = (Array.isArray(list) ? list : []).filter((s) => s && typeof s === 'object' && s.id)
742
1072
  this.schemasByRegister = { ...this.schemasByRegister, [id]: schemas }
743
1073
  } catch {
744
- // Silently fail — register already selected, schemas just won't populate
1074
+ // The register stays selected; its schema select stays empty.
745
1075
  }
746
1076
  },
747
1077
  },
@@ -924,6 +1254,159 @@ export default {
924
1254
  margin: 0 0 8px 0;
925
1255
  }
926
1256
 
1257
+ /* Board look (PqBeheer): one open group, compact rows for the others. */
1258
+ .cn-register-mapping--board {
1259
+ display: flex;
1260
+ flex-direction: column;
1261
+ gap: 14px;
1262
+ }
1263
+
1264
+ .cn-register-mapping__board-group {
1265
+ display: flex;
1266
+ flex-direction: column;
1267
+ gap: 10px;
1268
+ padding: 14px;
1269
+ border: 1px solid var(--color-border);
1270
+ border-radius: var(--border-radius-large);
1271
+ }
1272
+
1273
+ .cn-register-mapping__board-head {
1274
+ display: flex;
1275
+ flex-wrap: wrap;
1276
+ align-items: flex-start;
1277
+ justify-content: space-between;
1278
+ gap: 10px 16px;
1279
+ }
1280
+
1281
+ .cn-register-mapping__board-title {
1282
+ margin: 0;
1283
+ font-size: 15px;
1284
+ font-weight: 700;
1285
+ }
1286
+
1287
+ .cn-register-mapping__board-select {
1288
+ max-width: 400px;
1289
+ margin-bottom: 0;
1290
+ }
1291
+
1292
+ .cn-register-mapping__board-table {
1293
+ width: 100%;
1294
+ border-collapse: collapse;
1295
+ }
1296
+
1297
+ .cn-register-mapping__board-table th {
1298
+ padding: 8px 12px 8px 0;
1299
+ border-bottom: 1px solid var(--color-border);
1300
+ font-size: 13px;
1301
+ font-weight: 600;
1302
+ color: var(--color-text-maxcontrast);
1303
+ text-align: start;
1304
+ white-space: nowrap;
1305
+ }
1306
+
1307
+ .cn-register-mapping__board-table td {
1308
+ padding: 10px 12px 10px 0;
1309
+ border-bottom: 1px solid var(--color-border);
1310
+ font-size: 14px;
1311
+ vertical-align: middle;
1312
+ }
1313
+
1314
+ .cn-register-mapping__board-schema {
1315
+ display: inline-flex;
1316
+ align-items: center;
1317
+ gap: 6px;
1318
+ min-height: 32px;
1319
+ margin: 0;
1320
+ padding: 0;
1321
+ border: 0;
1322
+ background: transparent;
1323
+ color: inherit;
1324
+ font: inherit;
1325
+ text-align: start;
1326
+ cursor: pointer;
1327
+ }
1328
+
1329
+ .cn-register-mapping__board-schema:hover {
1330
+ text-decoration: underline;
1331
+ }
1332
+
1333
+ .cn-register-mapping__board-schema:focus-visible,
1334
+ .cn-register-mapping__board-link:focus-visible,
1335
+ .cn-register-mapping__board-group-row:focus-visible {
1336
+ outline: 2px solid var(--color-primary-element);
1337
+ outline-offset: 2px;
1338
+ border-radius: var(--border-radius);
1339
+ }
1340
+
1341
+ .cn-register-mapping__board-check {
1342
+ color: var(--color-success-text, var(--color-success));
1343
+ }
1344
+
1345
+ .cn-register-mapping__board-more {
1346
+ margin: 0;
1347
+ font-size: 13px;
1348
+ line-height: 1.4;
1349
+ color: var(--color-text-maxcontrast);
1350
+ }
1351
+
1352
+ .cn-register-mapping__board-link {
1353
+ margin: 0;
1354
+ padding: 0;
1355
+ border: 0;
1356
+ background: transparent;
1357
+ color: var(--color-primary-element);
1358
+ font: inherit;
1359
+ text-decoration: underline;
1360
+ cursor: pointer;
1361
+ }
1362
+
1363
+ .cn-register-mapping__board-groups {
1364
+ display: flex;
1365
+ flex-direction: column;
1366
+ margin: 0;
1367
+ padding: 0;
1368
+ list-style: none;
1369
+ }
1370
+
1371
+ .cn-register-mapping__board-group-row {
1372
+ display: flex;
1373
+ align-items: center;
1374
+ gap: 10px;
1375
+ width: 100%;
1376
+ min-height: 44px;
1377
+ margin: 0;
1378
+ padding: 10px 2px;
1379
+ border: 0;
1380
+ border-bottom: 1px solid var(--color-border);
1381
+ background: transparent;
1382
+ color: inherit;
1383
+ font: inherit;
1384
+ font-size: 14px;
1385
+ text-align: start;
1386
+ cursor: pointer;
1387
+ }
1388
+
1389
+ .cn-register-mapping__board-group-row:hover {
1390
+ background: var(--color-background-hover);
1391
+ }
1392
+
1393
+ .cn-register-mapping__board-chevron,
1394
+ .cn-register-mapping__board-group-count {
1395
+ color: var(--color-text-maxcontrast);
1396
+ }
1397
+
1398
+ .cn-register-mapping__board-group-name {
1399
+ flex: 1;
1400
+ }
1401
+
1402
+ .cn-register-mapping__board-save {
1403
+ display: flex;
1404
+ flex-wrap: wrap;
1405
+ align-items: center;
1406
+ justify-content: flex-end;
1407
+ gap: 10px;
1408
+ }
1409
+
927
1410
  /* Slide transition */
928
1411
  .slide-enter-active,
929
1412
  .slide-leave-active {
@@ -74,6 +74,7 @@ import '../CnFlowRunsWidget/index.js'
74
74
  import '../CnTasksWidget/index.js'
75
75
  import '../CnSectionsWidget/index.js'
76
76
  import '../CnFactsWidget/index.js'
77
+ import '../CnDeviceFramesWidget/index.js'
77
78
  import '../CnFieldListWidget/index.js'
78
79
  import '../CnSettingsFormWidget/index.js'
79
80
  import '../CnSwitchRowsWidget/index.js'
@@ -220,6 +220,7 @@ export { CnTasksWidget } from './CnTasksWidget/index.js'
220
220
  export { CnTasksWidgetForm } from './CnTasksWidgetForm/index.js'
221
221
  export { CnSectionsWidget } from './CnSectionsWidget/index.js'
222
222
  export { CnFactsWidget } from './CnFactsWidget/index.js'
223
+ export { CnDeviceFramesWidget } from './CnDeviceFramesWidget/index.js'
223
224
  export { CnFieldListWidget } from './CnFieldListWidget/index.js'
224
225
  export { CnSettingsFormWidget } from './CnSettingsFormWidget/index.js'
225
226
  export { CnSwitchRowsWidget } from './CnSwitchRowsWidget/index.js'
package/src/index.js CHANGED
@@ -99,6 +99,7 @@ export {
99
99
  CnDetailGrid,
100
100
  CnDetailPage,
101
101
  CnDetailWidgetHost,
102
+ CnDeviceFramesWidget,
102
103
  CnDialog,
103
104
  CnDialogHeader,
104
105
  CnDividerWidget,