@conduction/nextcloud-vue 2.84.0 → 2.85.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.
Files changed (166) hide show
  1. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  2. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +131 -3
  3. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +54 -18
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  6. package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
  7. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +232 -4
  8. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  9. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +95 -51
  10. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  11. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
  12. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +114 -0
  13. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  14. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +2 -2
  15. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  16. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  17. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +45 -0
  18. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  19. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +22 -12
  20. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  21. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +23 -4
  22. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  23. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  24. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  25. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +3 -1
  26. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  27. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  28. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  29. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +18 -5
  30. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  31. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  32. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +548 -77
  33. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  34. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +191 -82
  35. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  36. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +11 -2
  37. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  38. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
  39. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +95 -0
  40. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  41. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +24 -2
  42. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  43. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  44. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +70 -2
  45. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  46. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  47. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
  48. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +67 -5
  49. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
  50. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js +181 -124
  51. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
  52. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
  53. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +5 -1
  54. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
  55. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
  56. package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
  57. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +134 -10
  58. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  59. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +6 -2
  60. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  61. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  62. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +6 -3
  63. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  64. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  65. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +1 -1
  66. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +140 -7
  67. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -1
  68. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -1
  69. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
  70. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js +37 -9
  71. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
  72. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +6 -5
  73. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
  74. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +1 -1
  75. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +2 -1
  76. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -1
  77. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -1
  78. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js +53 -1
  79. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js.map +1 -1
  80. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js +3 -2
  81. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js.map +1 -1
  82. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
  83. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +135 -0
  84. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  85. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  86. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
  87. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +19 -0
  88. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  89. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +37 -19
  90. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  91. package/dist/esm/composables/useListView.js +21 -0
  92. package/dist/esm/composables/useListView.js.map +1 -1
  93. package/dist/esm/index.js +1 -0
  94. package/dist/esm/index.js.map +1 -1
  95. package/dist/esm/l10n/en.json.js +23 -1
  96. package/dist/esm/l10n/en.json.js.map +1 -1
  97. package/dist/esm/l10n/nl.json.js +41 -1
  98. package/dist/esm/l10n/nl.json.js.map +1 -1
  99. package/dist/esm/library-style.js +1 -1
  100. package/dist/esm/nextcloud-vue.css +742 -397
  101. package/dist/esm/store/useObjectStore.js +16 -2
  102. package/dist/esm/store/useObjectStore.js.map +1 -1
  103. package/dist/esm/utils/boardColumns.js +8 -1
  104. package/dist/esm/utils/boardColumns.js.map +1 -1
  105. package/dist/esm/utils/boardReferenceColumns.js +278 -0
  106. package/dist/esm/utils/boardReferenceColumns.js.map +1 -0
  107. package/dist/esm/utils/buildManifest.js +4 -1
  108. package/dist/esm/utils/buildManifest.js.map +1 -1
  109. package/dist/esm/utils/gridStack.js +40 -1
  110. package/dist/esm/utils/gridStack.js.map +1 -1
  111. package/dist/esm/utils/humaniseKey.js +41 -0
  112. package/dist/esm/utils/humaniseKey.js.map +1 -0
  113. package/dist/esm/utils/notificationRuleLabel.js +6 -23
  114. package/dist/esm/utils/notificationRuleLabel.js.map +1 -1
  115. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  116. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  117. package/dist/esm/utils/widgetDispatch.js +8 -2
  118. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  119. package/dist/nextcloud-vue.cjs.js +15075 -12590
  120. package/dist/nextcloud-vue.cjs.js.map +2 -2
  121. package/dist/nextcloud-vue.css +742 -397
  122. package/l10n/en.json +23 -1
  123. package/l10n/nl.json +41 -1
  124. package/package.json +1 -1
  125. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +160 -8
  126. package/src/components/CnBoardView/CnBoardView.vue +262 -6
  127. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +116 -2
  128. package/src/components/CnDashboardPage/CnDashboardPage.vue +57 -2
  129. package/src/components/CnDetailPage/CnDetailPage.md +1 -1
  130. package/src/components/CnDetailPage/CnDetailPage.vue +23 -4
  131. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +3 -1
  132. package/src/components/CnFactsWidget/CnFactsWidget.vue +18 -5
  133. package/src/components/CnIndexPage/CnIndexPage.vue +586 -25
  134. package/src/components/CnIndexPage/useSelfFetchList.js +10 -1
  135. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +123 -3
  136. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +70 -2
  137. package/src/components/CnRegisterMapping/CnRegisterMapping.md +5 -0
  138. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +231 -96
  139. package/src/components/CnSettingsPage/CnSettingsPage.vue +5 -1
  140. package/src/components/CnStatWidget/CnStatWidget.vue +136 -11
  141. package/src/components/CnTabsWidget/CnTabsWidget.vue +6 -3
  142. package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +140 -7
  143. package/src/components/CnVersionInfoCard/CnVersionInfoCard.md +2 -1
  144. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +42 -13
  145. package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +2 -1
  146. package/src/components/CnWidgetCardGrid/CnWidgetCardGrid.vue +55 -2
  147. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +135 -0
  148. package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +10 -0
  149. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +44 -1
  150. package/src/composables/useListView.js +21 -0
  151. package/src/css/board.css +65 -4
  152. package/src/css/dialog.css +4 -2
  153. package/src/css/index-page.css +49 -0
  154. package/src/css/kpi-card.css +13 -0
  155. package/src/css/look-board.css +96 -7
  156. package/src/index.js +2 -0
  157. package/src/schemas/app-manifest-v2.schema.json +115 -5
  158. package/src/store/useObjectStore.js +16 -2
  159. package/src/utils/boardColumns.js +8 -1
  160. package/src/utils/boardReferenceColumns.js +274 -0
  161. package/src/utils/buildManifest.js +4 -1
  162. package/src/utils/gridStack.js +39 -0
  163. package/src/utils/humaniseKey.js +38 -0
  164. package/src/utils/notificationRuleLabel.js +5 -23
  165. package/src/utils/validateManifestV2.compiled.js +3 -3
  166. package/src/utils/widgetDispatch.js +8 -2
@@ -14,10 +14,11 @@
14
14
  :title="title"
15
15
  :widgetId="widgetId"
16
16
  :documentationUrl="documentationUrl"
17
+ :refreshing="epLoading"
17
18
  flush>
18
19
  <div class="cn-widget-card-grid">
19
20
  <CnObjectCard
20
- v-for="(item, index) in objects"
21
+ v-for="(item, index) in cards"
21
22
  :key="item.id || index"
22
23
  :object="item" />
23
24
  </div>
@@ -26,7 +27,10 @@
26
27
 
27
28
  <script>
28
29
  import { translate as t } from '@nextcloud/l10n'
30
+ import { inject, ref } from 'vue'
29
31
  import CnObjectCard from '../CnObjectCard/CnObjectCard.vue'
32
+ import { useEndpointSource } from '../../composables/useEndpointSource.js'
33
+ import { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'
30
34
  import { CnWidgetWrapper } from '../CnWidgetWrapper/index.js'
31
35
 
32
36
  /**
@@ -35,7 +39,9 @@ import { CnWidgetWrapper } from '../CnWidgetWrapper/index.js'
35
39
  * Renders the card grid on the shared CnWidgetWrapper chrome, which supplies
36
40
  * the widget title and the standard overflow Actions menu (Refresh /
37
41
  * Documentation / Request a feature). Each entry in `objects` is rendered as a
38
- * CnObjectCard.
42
+ * CnObjectCard. Without `objects`, an `endpointSource` (`{ url, method?,
43
+ * params?, responsePath? }`, as on the object table) supplies them: the
44
+ * payload after `responsePath` is the array of objects.
39
45
  */
40
46
  export default {
41
47
  name: 'CnWidgetCardGrid',
@@ -75,6 +81,53 @@ export default {
75
81
  type: Array,
76
82
  default: () => [],
77
83
  },
84
+
85
+ /**
86
+ * Read the objects from an app endpoint: `{ url, method?, params?,
87
+ * responsePath? }`, with the token grammar of widget filters. Ignored
88
+ * while `objects` holds any.
89
+ *
90
+ * @type {object|null}
91
+ */
92
+ endpointSource: {
93
+ type: Object,
94
+ default: null,
95
+ },
96
+ },
97
+
98
+ setup(props) {
99
+ const objectCtxRaw = inject('cnObjectContext', null)
100
+ const detailCtxRaw = inject('cnDetailObjectContext', null)
101
+ const workspaceRaw = inject('cnWorkspaceContext', ref(null))
102
+ const appConfigRaw = inject('cnAppConfig', ref({}))
103
+ const unwrap = (v) => ((v && typeof v === 'object' && 'value' in v) ? v.value : v)
104
+ const { data, loading } = useEndpointSource(
105
+ () => ((props.objects && props.objects.length > 0) ? null : props.endpointSource),
106
+ {
107
+ ctx: () => ({
108
+ ...(resolveObjectTokenContext(objectCtxRaw, detailCtxRaw) || {}),
109
+ workspace: unwrap(workspaceRaw) || {},
110
+ config: unwrap(appConfigRaw) || {},
111
+ }),
112
+ widgetId: () => props.widgetId,
113
+ },
114
+ )
115
+ return { epData: data, epLoading: loading }
116
+ },
117
+
118
+ computed: {
119
+ /**
120
+ * The objects drawn: `objects` when given, else the endpoint's array.
121
+ *
122
+ * @return {Array<object>}
123
+ * @spec openspec/changes/endpoint-source-card-grid-record-list/specs/dashboard-page/spec.md#requirement-a-record-list-and-a-card-grid-can-read-an-endpoint
124
+ */
125
+ cards() {
126
+ if (this.objects && this.objects.length > 0) {
127
+ return this.objects
128
+ }
129
+ return Array.isArray(this.epData) ? this.epData : []
130
+ },
78
131
  },
79
132
  }
80
133
  </script>
@@ -88,6 +88,7 @@ import { dispatchAction, resolveObjectOpType } from '../../utils/actionsDispatch
88
88
  import { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'
89
89
  import { pageFilter, pageSort } from '../../utils/pageControls.js'
90
90
  import { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
91
+ import { fieldsFromSchema } from '../../utils/schema.js'
91
92
  import { compareVisibleWhen, readVisibleWhenPath } from '../../utils/visibleWhen.js'
92
93
  import { CnIcon } from '../CnIcon/index.js'
93
94
  import { CnRowActions } from '../CnRowActions/index.js'
@@ -231,6 +232,12 @@ export default {
231
232
  * the object form (`{ key, label, sortable, width, cellClass,
232
233
  * formatter, widget, format, aggregate }`) both pass straight through.
233
234
  *
235
+ * A column that holds a reference shows the referenced object's label
236
+ * with `labelField` (the property to show) and/or `resolve`
237
+ * (`{ register, schema, labelField? }`, where the reference points).
238
+ * Without `resolve` the target is read off the source schema's `$ref`
239
+ * for that property. Such a column renders as an `fkResolve` cell.
240
+ *
234
241
  * @type {Array<object|string>}
235
242
  */
236
243
  columns: {
@@ -469,6 +476,8 @@ export default {
469
476
  confirmAction: null,
470
477
  /** @type {object|null} The row the pending confirm-gated action targets. */
471
478
  confirmRow: null,
479
+ /** Where each label column's references point, read off the source schema, by column key. */
480
+ refTargets: {},
472
481
  }
473
482
  },
474
483
 
@@ -638,6 +647,57 @@ export default {
638
647
  return params
639
648
  },
640
649
 
650
+ /**
651
+ * Columns that ask for the label of the object they reference
652
+ * (`labelField` or `resolve`) and draw no cell widget of their own.
653
+ *
654
+ * @return {Array<object>}
655
+ * @spec openspec/changes/dashboard-table-reference-labels/specs/dashboard-page/spec.md#requirement-a-dashboard-table-shows-a-referenced-objects-label
656
+ */
657
+ refLabelColumns() {
658
+ return (this.columns || []).filter((col) => col && typeof col === 'object' && !col.widget
659
+ && ((typeof col.labelField === 'string' && col.labelField !== '') || (col.resolve && typeof col.resolve === 'object')))
660
+ },
661
+
662
+ /**
663
+ * The columns handed to CnDataTable: a label column whose target is
664
+ * known renders as an `fkResolve` cell; every other column as given.
665
+ *
666
+ * @return {Array<object|string>}
667
+ * @spec openspec/changes/dashboard-table-reference-labels/specs/dashboard-page/spec.md#requirement-a-dashboard-table-shows-a-referenced-objects-label
668
+ */
669
+ renderedColumns() {
670
+ if (this.refLabelColumns.length === 0) {
671
+ return this.columns
672
+ }
673
+ return this.columns.map((col) => {
674
+ if (!this.refLabelColumns.includes(col)) {
675
+ return col
676
+ }
677
+ const target = this.refTargetOf(col)
678
+ if (!target) {
679
+ return col
680
+ }
681
+ const labelField = (col.resolve && typeof col.resolve.labelField === 'string' && col.resolve.labelField) || col.labelField || 'name'
682
+ return { ...col, widget: 'fkResolve', widgetProps: { ...(col.widgetProps || {}), register: target.register, schema: target.schema, labelField } }
683
+ })
684
+ },
685
+
686
+ /**
687
+ * The label columns whose target has to be read off the source schema.
688
+ *
689
+ * @return {string[]} Their keys.
690
+ */
691
+ schemaRefKeys() {
692
+ return this.refLabelColumns.filter((col) => !this.explicitTarget(col)).map((col) => col.key)
693
+ },
694
+
695
+ /** @return {string} The label columns to look up and the source schema, to read again when either changes. */
696
+ refTargetsKey() {
697
+ const src = this.source || {}
698
+ return `${src.register || ''}/${src.schema || ''}:${this.schemaRefKeys.join('|')}`
699
+ },
700
+
641
701
  /**
642
702
  * `$props` minus the chrome props (`title`, `documentationUrl`,
643
703
  * `widgetId`, `hideWrapper`) and the widget-only props (`source`,
@@ -669,6 +729,9 @@ export default {
669
729
  if (this.compiledRowClass) {
670
730
  inner.rowClass = this.compiledRowClass
671
731
  }
732
+ if (this.refLabelColumns.length > 0) {
733
+ inner.columns = this.renderedColumns
734
+ }
672
735
  if (this.selfFetchActive) {
673
736
  inner.register = this.source.register
674
737
  inner.schemaId = this.source.schema
@@ -804,6 +867,16 @@ export default {
804
867
  },
805
868
  },
806
869
 
870
+ watch: {
871
+ refTargetsKey() {
872
+ this.loadRefTargets()
873
+ },
874
+ },
875
+
876
+ mounted() {
877
+ this.loadRefTargets()
878
+ },
879
+
807
880
  methods: {
808
881
  /**
809
882
  * Entry point for every declarative action trigger (row menu or
@@ -914,6 +987,68 @@ export default {
914
987
  }
915
988
  },
916
989
 
990
+ /**
991
+ * Where a label column's references point when the column says so
992
+ * itself (`resolve.register` and `resolve.schema`).
993
+ *
994
+ * @param {object} col The column.
995
+ * @return {{register: string, schema: string}|null}
996
+ */
997
+ explicitTarget(col) {
998
+ const r = col && col.resolve
999
+ if (r && typeof r === 'object' && typeof r.register === 'string' && r.register !== '' && typeof r.schema === 'string' && r.schema !== '') {
1000
+ return { register: r.register, schema: r.schema }
1001
+ }
1002
+ return null
1003
+ },
1004
+
1005
+ /**
1006
+ * Where a label column's references point: the column's `resolve`, else
1007
+ * the source schema's `$ref` for the property, else null (the column
1008
+ * then shows the raw value).
1009
+ *
1010
+ * @param {object} col The column.
1011
+ * @return {{register: string, schema: string}|null}
1012
+ */
1013
+ refTargetOf(col) {
1014
+ return this.explicitTarget(col) || this.refTargets[col.key] || null
1015
+ },
1016
+
1017
+ /**
1018
+ * Read the source schema once and note, for each label column without a
1019
+ * `resolve`, the register and schema its property's `$ref` names. A
1020
+ * failed or missing schema leaves those columns showing raw values.
1021
+ *
1022
+ * @return {Promise<void>}
1023
+ * @spec openspec/changes/dashboard-table-reference-labels/specs/dashboard-page/spec.md#requirement-a-dashboard-table-shows-a-referenced-objects-label
1024
+ */
1025
+ async loadRefTargets() {
1026
+ const keys = this.schemaRefKeys
1027
+ const src = this.source
1028
+ if (keys.length === 0 || !src || typeof src.register !== 'string' || src.register === '' || src.register.startsWith('@') || typeof src.schema !== 'string' || src.schema === '') {
1029
+ return
1030
+ }
1031
+ const store = this.getObjectStore()
1032
+ if (!store || typeof store.fetchSchema !== 'function') {
1033
+ return
1034
+ }
1035
+ const type = resolveObjectOpType(store, { register: src.register, schema: src.schema }, { exactSchema: true })
1036
+ const schema = await store.fetchSchema(type).catch(() => null)
1037
+ if (!schema || typeof schema !== 'object') {
1038
+ return
1039
+ }
1040
+ const fields = fieldsFromSchema(schema, { includeReadOnly: true })
1041
+ const found = {}
1042
+ for (const key of keys) {
1043
+ const field = fields.find((f) => f.key === key)
1044
+ const ref = field && field.reference
1045
+ if (ref && ref.schema !== undefined && ref.schema !== null) {
1046
+ found[key] = { register: ref.register || src.register, schema: String(ref.schema) }
1047
+ }
1048
+ }
1049
+ this.refTargets = { ...this.refTargets, ...found }
1050
+ },
1051
+
917
1052
  /**
918
1053
  * The shared object store instance, or null when no Pinia is active
919
1054
  * (`dispatchAction` then warns and no-ops).
@@ -12,6 +12,16 @@ Basic — widget card with title and content:
12
12
  </div>
13
13
  ```
14
14
 
15
+ With an `eyebrow`, a short muted line above the title:
16
+
17
+ ```vue
18
+ <div style="width: 320px; height: 160px; position: relative; border: 1px solid var(--color-border); border-radius: 8px; overflow: hidden;">
19
+ <CnWidgetWrapper title="Forecast 13 weeks" eyebrow="Balance per week" :show-title="true">
20
+ <p style="margin: 0; padding: 0 16px;">Week 41</p>
21
+ </CnWidgetWrapper>
22
+ </div>
23
+ ```
24
+
15
25
  With footer buttons:
16
26
 
17
27
  ```vue
@@ -52,7 +52,15 @@
52
52
  v-else-if="iconClass"
53
53
  :class="iconClass"
54
54
  class="cn-widget-wrapper__icon" />
55
- <h3 :id="titleId" class="cn-widget-wrapper__title">
55
+ <!-- An eyebrow (`eyebrow`) is a muted line above the title:
56
+ "Saldo per week" above "Prognose 13 weken". -->
57
+ <div v-if="resolvedEyebrow" class="cn-widget-wrapper__title-stack">
58
+ <span class="cn-widget-wrapper__eyebrow" data-testid="cn-widget-wrapper-eyebrow">{{ resolvedEyebrow }}</span>
59
+ <h3 :id="titleId" class="cn-widget-wrapper__title">
60
+ {{ resolvedTitle }}
61
+ </h3>
62
+ </div>
63
+ <h3 v-else :id="titleId" class="cn-widget-wrapper__title">
56
64
  {{ resolvedTitle }}
57
65
  </h3>
58
66
  <!-- @slot title-meta Rendered inside the header's left group,
@@ -234,6 +242,15 @@ export default {
234
242
  default: () => t('nextcloud-vue', 'Widget'),
235
243
  },
236
244
 
245
+ /**
246
+ * A short muted line above the title ("Saldo per week" above
247
+ * "Prognose 13 weken"), through the host translate. Empty draws none.
248
+ */
249
+ eyebrow: {
250
+ type: String,
251
+ default: '',
252
+ },
253
+
237
254
  /** Whether to show the header with title */
238
255
  showTitle: {
239
256
  type: Boolean,
@@ -717,6 +734,16 @@ export default {
717
734
  return label ? this.effectiveTranslate(label) : ''
718
735
  },
719
736
 
737
+ /**
738
+ * The eyebrow through the host translate, or ''.
739
+ *
740
+ * @return {string}
741
+ * @spec openspec/changes/widget-title-eyebrow/specs/dashboard-page/spec.md#requirement-a-widget-card-can-carry-a-line-above-its-title
742
+ */
743
+ resolvedEyebrow() {
744
+ return this.eyebrow ? this.effectiveTranslate(this.eyebrow) : ''
745
+ },
746
+
720
747
  /**
721
748
  * The RENDERED widget title — `displayTitle` run through the host
722
749
  * translate function. Kept separate from `displayTitle` on purpose:
@@ -1152,6 +1179,22 @@ export default {
1152
1179
  color: var(--cn-widget-icon-color, var(--color-primary-element));
1153
1180
  }
1154
1181
 
1182
+ .cn-widget-wrapper__title-stack {
1183
+ display: flex;
1184
+ flex-direction: column;
1185
+ gap: 2px;
1186
+ min-width: 0;
1187
+ }
1188
+
1189
+ .cn-widget-wrapper__eyebrow {
1190
+ font-size: 13px;
1191
+ line-height: 1.3;
1192
+ color: var(--color-text-maxcontrast);
1193
+ white-space: nowrap;
1194
+ overflow: hidden;
1195
+ text-overflow: ellipsis;
1196
+ }
1197
+
1155
1198
  .cn-widget-wrapper__title {
1156
1199
  font-weight: 600;
1157
1200
  font-size: 14px;
@@ -291,6 +291,26 @@ export function useListView(objectTypeOrOptions, options) {
291
291
  refresh(page)
292
292
  }
293
293
 
294
+ /**
295
+ * Add the next page after the rows already loaded, for a view that shows
296
+ * every row at once (the board). Does nothing on the last page. A refresh
297
+ * started while the page is in flight drops it.
298
+ *
299
+ * @return {Promise<void>}
300
+ */
301
+ async function loadMore() {
302
+ const current = pagination.value
303
+ const next = Number(current.page || 1) + 1
304
+ if (next > Number(current.pages || 1)) {
305
+ return
306
+ }
307
+ const seq = refreshSeq
308
+ await objectStore.fetchCollection(getType(), buildParams(next), {
309
+ append: true,
310
+ isCurrent: () => seq === refreshSeq,
311
+ })
312
+ }
313
+
294
314
  /**
295
315
  * Handle page-size change. Resets to page 1.
296
316
  *
@@ -402,6 +422,7 @@ export function useListView(objectTypeOrOptions, options) {
402
422
  onPageSizeChange,
403
423
  // Explicit fetch
404
424
  refresh,
425
+ loadMore,
405
426
  // The query the list sends (search, sort, facet filters, fixed filters),
406
427
  // built from the current state, for an export that must follow the list.
407
428
  buildParams,
package/src/css/board.css CHANGED
@@ -96,19 +96,24 @@
96
96
  gap: 8px;
97
97
  }
98
98
 
99
- .cn-board-view.cn-board-view--board .cn-board-view__card-title {
99
+ /* The title is a link drawn as a heading (board DqWerkbord): main text, no
100
+ underline. `!important` because a theme's `a { color; text-decoration }`
101
+ carries it (thematiq), and a bare `a` with `!important` beats any class. */
102
+ .cn-board-view.cn-board-view--board .cn-board-view__card-title,
103
+ .cn-board-view.cn-board-view--board .cn-board-view__card-title:visited {
100
104
  flex: 1 1 auto;
101
105
  min-width: 0;
102
106
  font-size: 15px;
103
107
  font-weight: 700;
104
108
  line-height: 1.3;
105
- color: var(--color-main-text);
106
- text-decoration: none;
109
+ color: var(--color-main-text) !important;
110
+ text-decoration: none !important;
107
111
  overflow-wrap: anywhere;
108
112
  }
109
113
 
110
114
  .cn-board-view.cn-board-view--board .cn-board-view__card-title:hover {
111
- text-decoration: underline;
115
+ color: var(--color-main-text) !important;
116
+ text-decoration: underline !important;
112
117
  }
113
118
 
114
119
  .cn-board-view.cn-board-view--board .cn-board-view__card-title:focus-visible {
@@ -267,3 +272,59 @@
267
272
  outline: 2px solid var(--color-primary-element);
268
273
  outline-offset: 2px;
269
274
  }
275
+
276
+ /*
277
+ * screens-board-reference-columns: a wide board wraps its columns onto more
278
+ * rows (`config.board.wrap`), as many to a row as fit at 240px, instead of
279
+ * one row that scrolls sideways.
280
+ */
281
+ .cn-board-view.cn-board-view--wrap .cn-board-view__columns {
282
+ display: grid;
283
+ grid-auto-flow: row;
284
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
285
+ overflow-x: visible;
286
+ }
287
+
288
+ .cn-board-view.cn-board-view--wrap:not(.cn-board-view--board) .cn-board-view__columns {
289
+ gap: 12px;
290
+ }
291
+
292
+ /* A column the dragged card cannot reach fades; the label stays readable. */
293
+ .cn-board-view .cn-board-view__column--unreachable {
294
+ opacity: 0.45;
295
+ }
296
+
297
+ /* The selection checkbox: in the board look's card head, before the title. */
298
+ .cn-board-view .cn-board-view__select {
299
+ flex: none;
300
+ width: 16px;
301
+ height: 16px;
302
+ margin: 2px 0 0;
303
+ accent-color: var(--color-primary-element);
304
+ cursor: pointer;
305
+ }
306
+
307
+ /* Board look: the board draws no checkbox. It shows on a hovered card, on a
308
+ card holding focus (so Tab still reaches it), once anything is selected, and
309
+ always where there is no hover. Collapsed rather than hidden, so it stays in
310
+ the tab order. */
311
+ @media (hover: hover) {
312
+ .cn-board-view.cn-board-view--board:not(.cn-board-view--selecting) .cn-board-view__card:not(:hover, :focus-within) .cn-board-view__select {
313
+ width: 0;
314
+ margin-inline-end: -8px;
315
+ opacity: 0;
316
+ }
317
+ }
318
+
319
+ /* A menu target the card cannot reach: still listed, greyed, with its note. */
320
+ .cn-board-view.cn-board-view--board .cn-board-view__menu-item.cn-board-view__menu-item--blocked {
321
+ color: var(--color-text-maxcontrast);
322
+ cursor: not-allowed;
323
+ }
324
+
325
+ .cn-board-view .cn-board-view__menu-note {
326
+ margin-inline-start: auto;
327
+ padding-inline-start: 12px;
328
+ font-size: 12px;
329
+ color: var(--color-text-maxcontrast);
330
+ }
@@ -35,8 +35,10 @@
35
35
  }
36
36
 
37
37
  /* The header is ours: NcModal's own close button and NcDialog's heading give
38
- way (the heading stays as the accessible name, visually hidden). */
39
- .cn-dialog-board .modal-container__close {
38
+ way (the heading stays as the accessible name, visually hidden). The mask
39
+ class raises the rule above NcButton's `.button-vue[data-v-…]`, whose
40
+ stylesheet a spawned dialog can load after this one. */
41
+ .modal-mask.cn-dialog-board .modal-container__close {
40
42
  display: none;
41
43
  }
42
44
 
@@ -104,6 +104,19 @@
104
104
  margin-top: 0;
105
105
  }
106
106
 
107
+ /* The board view's footer: how many rows it holds of how many match, and
108
+ "Load more" while the list is not complete. */
109
+ .cn-index-page__board-load {
110
+ display: flex;
111
+ flex-wrap: wrap;
112
+ align-items: center;
113
+ justify-content: space-between;
114
+ gap: 8px 16px;
115
+ padding: 12px 16px;
116
+ color: var(--color-text-maxcontrast);
117
+ font-size: 14px;
118
+ }
119
+
107
120
  /* Table view owns both scroll axes. With the page column scrolling instead, the
108
121
  table's horizontal scrollbar sat at the bottom of the (taller-than-viewport)
109
122
  table, so it only appeared after scrolling down, and the pagination was
@@ -369,3 +382,39 @@
369
382
  outline: 2px solid var(--color-primary-element);
370
383
  outline-offset: 2px;
371
384
  }
385
+
386
+ /*
387
+ * screens-board-reference-columns: the board view's controls after the
388
+ * search (board DqWerkbord): "Case type:" and "Columns:" as a 600 label and a
389
+ * 36px select, then the hint in the secondary text colour.
390
+ */
391
+ .cn-index-page__board-control {
392
+ display: inline-flex;
393
+ align-items: center;
394
+ gap: 8px;
395
+ font-size: 14px;
396
+ font-weight: 600;
397
+ color: var(--color-main-text);
398
+ }
399
+
400
+ .cn-index-page__board-select {
401
+ height: 36px;
402
+ min-height: 36px;
403
+ margin: 0;
404
+ padding: 0 10px;
405
+ border: 1px solid var(--color-border-maxcontrast);
406
+ border-radius: 8px;
407
+ background-color: var(--color-main-background);
408
+ color: var(--color-main-text);
409
+ font: inherit;
410
+ font-weight: 400;
411
+ }
412
+
413
+ .cn-index-page__board-select--strong {
414
+ font-weight: 500;
415
+ }
416
+
417
+ .cn-index-page__board-hint {
418
+ font-size: 14px;
419
+ color: var(--color-text-maxcontrast);
420
+ }
@@ -444,6 +444,19 @@ a.cn-kpi-card--stacked:focus-visible {
444
444
  color: var(--color-main-text);
445
445
  }
446
446
 
447
+ /* A board tile whose label wraps grows into its cell instead of clipping its
448
+ own last line. dossiq's "Afgehandeld deze maand" at 1100 wide held 129px of
449
+ content in a 114px tile and lost its caption's bottom. The tile still fills
450
+ a cell it fits (`min-height: 100%`), so every tile that fits keeps its
451
+ size; one that does not takes the height of its content, and the card-fit
452
+ cell's `safe center` keeps its top in view.
453
+ @spec openspec/changes/board-stat-tile-grows/specs/stat-tile-board-look/spec.md#requirement-a-board-stat-tile-shows-all-its-lines */
454
+ .cn-kpi-card--board.cn-kpi-card--stacked {
455
+ flex: 0 0 auto;
456
+ height: auto;
457
+ min-height: 100%;
458
+ }
459
+
447
460
  .cn-kpi-card--board.cn-kpi-card--stacked .cn-kpi-card__caption-line,
448
461
  .cn-kpi-card--board.cn-kpi-card--stacked .cn-stats-block__count-label {
449
462
  font-size: 13px;