@conduction/nextcloud-vue 2.78.0 → 2.79.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 (115) hide show
  1. package/dist/bundled-packages.json +0 -2
  2. package/dist/esm/bundled-packages.json +0 -2
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +113 -104
  5. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  6. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +12 -2
  7. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  8. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  9. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +80 -0
  10. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  11. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +115 -91
  12. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  13. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +139 -1
  14. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  15. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +140 -92
  16. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  17. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +77 -0
  18. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  19. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +4 -2
  20. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  21. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +61 -1
  22. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  23. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  24. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +173 -5
  25. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  26. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +181 -99
  27. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  28. package/dist/esm/components/CnFilesBrowser/filesBrowser.js +29 -11
  29. package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
  30. package/dist/esm/components/CnFilesBrowser/openRegisterSource.js +252 -0
  31. package/dist/esm/components/CnFilesBrowser/openRegisterSource.js.map +1 -0
  32. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +1 -1
  33. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  34. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  35. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +25 -2
  36. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  37. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +3 -2
  38. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  39. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +100 -3
  40. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  41. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -2
  42. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  43. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +3 -2
  44. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
  45. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
  46. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
  47. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js +1 -1
  48. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
  49. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +30 -1
  50. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  51. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  52. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -0
  53. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  54. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  55. package/dist/esm/composables/useAppManifest.js +8 -3
  56. package/dist/esm/composables/useAppManifest.js.map +1 -1
  57. package/dist/esm/index.js +1 -0
  58. package/dist/esm/index.js.map +1 -1
  59. package/dist/esm/l10n/en.json.js +61 -1
  60. package/dist/esm/l10n/en.json.js.map +1 -1
  61. package/dist/esm/l10n/nl.json.js +106 -2
  62. package/dist/esm/l10n/nl.json.js.map +1 -1
  63. package/dist/esm/nextcloud-vue.css +314 -15
  64. package/dist/esm/utils/boardDate.js +193 -0
  65. package/dist/esm/utils/boardDate.js.map +1 -0
  66. package/dist/esm/utils/dynamicProperties.js +5 -1
  67. package/dist/esm/utils/dynamicProperties.js.map +1 -1
  68. package/dist/esm/utils/validateManifestV2.compiled.js +57 -6
  69. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  70. package/dist/nextcloud-vue.cjs.js +2039 -581
  71. package/dist/nextcloud-vue.cjs.js.map +2 -2
  72. package/dist/nextcloud-vue.css +314 -15
  73. package/l10n/en.json +61 -1
  74. package/l10n/nl.json +106 -2
  75. package/package.json +1 -1
  76. package/scripts/build-validators.js +26 -0
  77. package/src/components/CnActionsBar/CnActionsBar.vue +48 -39
  78. package/src/components/CnAppRoot/CnAppRoot.vue +12 -2
  79. package/src/components/CnCellRenderer/CnCellRenderer.vue +119 -2
  80. package/src/components/CnDashboardPage/CnDashboardPage.vue +219 -4
  81. package/src/components/CnDataTable/CnDataTable.md +2 -0
  82. package/src/components/CnDataTable/CnDataTable.vue +81 -2
  83. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +61 -1
  84. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +232 -11
  85. package/src/components/CnFilesBrowser/filesBrowser.js +43 -2
  86. package/src/components/CnFilesBrowser/index.js +1 -0
  87. package/src/components/CnFilesBrowser/openRegisterSource.js +248 -0
  88. package/src/components/CnFormDialog/CnFormDialog.vue +1 -1
  89. package/src/components/CnIndexPage/CnIndexPage.vue +27 -3
  90. package/src/components/CnObjectSidebar/CnFilesTab.vue +103 -4
  91. package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +3 -2
  92. package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +1 -1
  93. package/src/components/CnTabsWidget/CnTabsWidget.vue +30 -1
  94. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
  95. package/src/components/index.js +1 -1
  96. package/src/composables/useAppManifest.js +8 -3
  97. package/src/css/look-board-detail.css +10 -0
  98. package/src/css/look-board-index.css +92 -14
  99. package/src/css/look-board.css +77 -0
  100. package/src/css/table.css +37 -0
  101. package/src/index.js +4 -0
  102. package/src/schemas/app-manifest-v2.schema.json +46 -1
  103. package/src/utils/boardDate.js +189 -0
  104. package/src/utils/dynamicProperties.js +5 -1
  105. package/src/utils/validateManifestV2.compiled.js +57 -2
  106. package/dist/esm/_virtual/equal.js +0 -4
  107. package/dist/esm/_virtual/equal.js.map +0 -1
  108. package/dist/esm/_virtual/ucs2length.js +0 -4
  109. package/dist/esm/_virtual/ucs2length.js.map +0 -1
  110. package/dist/esm/node_modules/ajv/dist/runtime/equal.js +0 -9
  111. package/dist/esm/node_modules/ajv/dist/runtime/equal.js.map +0 -1
  112. package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js +0 -25
  113. package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js.map +0 -1
  114. package/dist/esm/node_modules/fast-deep-equal/index.js +0 -47
  115. package/dist/esm/node_modules/fast-deep-equal/index.js.map +0 -1
@@ -33,8 +33,8 @@
33
33
  <!-- @slot title-meta Content on the title's row, right of the title (e.g. a page-wide picker). Fill it from a manifest with `page.slots: { "title-meta": "<RegistryName>" }`. -->
34
34
  <slot name="title-meta" />
35
35
  </div>
36
- <p v-if="description" class="cn-dashboard-page__description">
37
- {{ resolvedDescription }}
36
+ <p v-if="subtitleText" class="cn-dashboard-page__description" data-testid="cn-dashboard-page-subtitle">
37
+ {{ subtitleText }}
38
38
  </p>
39
39
  </div>
40
40
  <div class="cn-dashboard-page__header-actions">
@@ -42,7 +42,7 @@
42
42
  widget on the page draws it. Its options control the view
43
43
  region below the grid. -->
44
44
  <CnSegmentedControl
45
- v-if="showsPageViewSwitch"
45
+ v-if="showsPageViewSwitch && !switchInRow"
46
46
  class="cn-page-view-switch"
47
47
  :options="viewSwitchOptions"
48
48
  :modelValue="activeViewId"
@@ -97,6 +97,7 @@
97
97
  Separate from the per-widget menus. `docs-anchor` deep-links
98
98
  the docs item to THIS page's section. -->
99
99
  <CnActionsMenu
100
+ v-if="showActionsMenu"
100
101
  :showRefresh="showRefresh"
101
102
  :showRequestFeature="showRequestFeature"
102
103
  :showReportBug="showReportBug"
@@ -138,6 +139,39 @@
138
139
  </div>
139
140
  </div>
140
141
 
142
+ <!-- The switch row (screens-dashboard-greeting-header): in the board
143
+ look the page's view switch sits under the header, left, and the
144
+ page's `viewLinks` sit right of it as small pills (DqMijnWerk). -->
145
+ <div
146
+ v-if="showHeader && (switchInRow || resolvedViewLinks.length > 0)"
147
+ class="cn-dashboard-page__switch-row"
148
+ data-testid="cn-dashboard-page-switch-row">
149
+ <CnSegmentedControl
150
+ v-if="switchInRow"
151
+ class="cn-page-view-switch"
152
+ :options="viewSwitchOptions"
153
+ :modelValue="activeViewId"
154
+ :ariaLabel="viewSwitchLabel"
155
+ :controls="viewRegionId"
156
+ data-testid="cn-dashboard-page-view-switch"
157
+ @update:modelValue="selectView" />
158
+ <span class="cn-dashboard-page__switch-row-spacer" />
159
+ <component
160
+ :is="link.tag"
161
+ v-for="link in resolvedViewLinks"
162
+ :key="link.key"
163
+ v-bind="link.attrs"
164
+ class="cn-dashboard-page__view-link"
165
+ data-testid="cn-dashboard-page-view-link">
166
+ <CnWidgetIcon
167
+ v-if="link.icon"
168
+ :name="link.icon"
169
+ :size="14"
170
+ class="cn-dashboard-page__view-link-icon" />
171
+ {{ link.label }}
172
+ </component>
173
+ </div>
174
+
141
175
  <!-- Date-range header (optional).
142
176
  Rendered only when `dateRange.enabled === true` AND
143
177
  `dateRange.showHeaderPicker !== false`. The range STATE
@@ -868,7 +902,8 @@
868
902
  </template>
869
903
 
870
904
  <script>
871
- import { translate as t } from '@nextcloud/l10n'
905
+ import { getCurrentUser } from '@nextcloud/auth'
906
+ import { getCanonicalLocale, translate as t } from '@nextcloud/l10n'
872
907
  import { NcActionButton, NcActionInput, NcActions, NcActionSeparator, NcButton, NcLoadingIcon, NcSelect } from '@nextcloud/vue'
873
908
  import { provide, ref, watch } from 'vue'
874
909
  import CalendarRange from 'vue-material-design-icons/CalendarRange.vue'
@@ -889,6 +924,7 @@ import CnKpiGrid from '../CnKpiGrid/CnKpiGrid.vue'
889
924
  import CnSegmentedControl from '../CnSegmentedControl/CnSegmentedControl.vue'
890
925
  import CnStatsBlockWidget from '../CnStatsBlockWidget/CnStatsBlockWidget.vue'
891
926
  import CnTileWidget from '../CnTileWidget/CnTileWidget.vue'
927
+ import CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'
892
928
  import CnWidgetRefItem from '../CnWidgetRefItem/CnWidgetRefItem.vue'
893
929
  import CnWidgetRenderer from '../CnWidgetRenderer/CnWidgetRenderer.vue'
894
930
  import CnWidgetWrapper from '../CnWidgetWrapper/CnWidgetWrapper.vue'
@@ -1056,6 +1092,7 @@ export default {
1056
1092
  CnLeafMountHost,
1057
1093
  CnSegmentedControl,
1058
1094
  CnKpiGrid,
1095
+ CnWidgetIcon,
1059
1096
  },
1060
1097
 
1061
1098
  mixins: [pageViews],
@@ -1156,6 +1193,50 @@ export default {
1156
1193
  default: () => [],
1157
1194
  },
1158
1195
 
1196
+ /**
1197
+ * Greet the reader in the header subtitle (manifest `config.greeting`):
1198
+ * `true` (or `"first"`) writes "Good afternoon, Pieter · Monday 5
1199
+ * October 2026", `"full"` uses the whole display name. The page
1200
+ * `description`, when set, follows after another middle dot. Off by
1201
+ * default: the subtitle is the description alone, as before.
1202
+ *
1203
+ * @spec openspec/changes/screens-dashboard-greeting-header/specs/dashboard-page/spec.md#requirement-the-header-subtitle-can-greet-the-reader
1204
+ * @type {boolean|string}
1205
+ */
1206
+ greeting: {
1207
+ type: [Boolean, String],
1208
+ default: false,
1209
+ },
1210
+
1211
+ /**
1212
+ * Links drawn as small pills on the switch row under the header
1213
+ * (manifest `config.viewLinks`), as DqMijnWerk's "Uw wachtrij / Aan
1214
+ * mij toegewezen / Dag afsluiten". Each `{ label, icon?, route?,
1215
+ * href? }`: `route` is a route name or a `{ name, params?, query? }`
1216
+ * location, `href` a plain URL; `label` goes through the host
1217
+ * translate function. An entry without a label or a target is skipped.
1218
+ *
1219
+ * @spec openspec/changes/screens-dashboard-greeting-header/specs/dashboard-page/spec.md#requirement-a-switch-row-carries-the-view-switch-and-link-pills
1220
+ * @type {Array<object>}
1221
+ */
1222
+ viewLinks: {
1223
+ type: Array,
1224
+ default: () => [],
1225
+ },
1226
+
1227
+ /**
1228
+ * Whether the header draws the page Actions menu (manifest
1229
+ * `config.showActionsMenu`). `false` removes it, for a board page
1230
+ * whose header carries only the buildiq square and the primary.
1231
+ *
1232
+ * @spec openspec/changes/screens-dashboard-greeting-header/specs/dashboard-page/spec.md#requirement-the-header-can-drop-the-page-actions-menu
1233
+ * @type {boolean}
1234
+ */
1235
+ showActionsMenu: {
1236
+ type: Boolean,
1237
+ default: true,
1238
+ },
1239
+
1159
1240
  /**
1160
1241
  * Whether a widget draws its overflow Actions menu when neither its
1161
1242
  * placement nor its definition says (`showActions`). `false` (manifest
@@ -1848,6 +1929,97 @@ export default {
1848
1929
  return typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key
1849
1930
  },
1850
1931
 
1932
+ /**
1933
+ * The greeting for the time of day, with the reader's first (or
1934
+ * full) name when known, or '' when `greeting` is off.
1935
+ *
1936
+ * @spec openspec/changes/screens-dashboard-greeting-header/specs/dashboard-page/spec.md#requirement-the-header-subtitle-can-greet-the-reader
1937
+ * @return {string}
1938
+ */
1939
+ greetingLine() {
1940
+ if (this.greeting !== true && this.greeting !== 'first' && this.greeting !== 'full') {
1941
+ return ''
1942
+ }
1943
+ let user
1944
+ try {
1945
+ user = getCurrentUser()
1946
+ } catch {
1947
+ user = null
1948
+ }
1949
+ const displayName = (user && typeof user.displayName === 'string') ? user.displayName.trim() : ''
1950
+ const name = this.greeting === 'full' ? displayName : displayName.split(/\s+/)[0]
1951
+ const hour = new Date().getHours()
1952
+ if (hour < 12) {
1953
+ return name ? t('nextcloud-vue', 'Good morning, {name}', { name }) : t('nextcloud-vue', 'Good morning')
1954
+ }
1955
+ if (hour < 18) {
1956
+ return name ? t('nextcloud-vue', 'Good afternoon, {name}', { name }) : t('nextcloud-vue', 'Good afternoon')
1957
+ }
1958
+ return name ? t('nextcloud-vue', 'Good evening, {name}', { name }) : t('nextcloud-vue', 'Good evening')
1959
+ },
1960
+
1961
+ /**
1962
+ * The header subtitle: with `greeting` on, the greeting, today's date
1963
+ * written out and the description, joined by a middle dot; else the
1964
+ * description alone, as before.
1965
+ *
1966
+ * @spec openspec/changes/screens-dashboard-greeting-header/specs/dashboard-page/spec.md#requirement-the-header-subtitle-can-greet-the-reader
1967
+ * @return {string}
1968
+ */
1969
+ subtitleText() {
1970
+ const description = this.description ? this.resolvedDescription : ''
1971
+ const greeting = this.greetingLine
1972
+ if (greeting === '') {
1973
+ return description
1974
+ }
1975
+ const options = { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }
1976
+ let date
1977
+ try {
1978
+ date = new Intl.DateTimeFormat(getCanonicalLocale() || undefined, options).format(new Date())
1979
+ } catch {
1980
+ date = new Intl.DateTimeFormat(undefined, options).format(new Date())
1981
+ }
1982
+ return [greeting, date, description].filter((part) => part !== '').join(' · ')
1983
+ },
1984
+
1985
+ /**
1986
+ * Whether the page's view switch sits on the switch row under the
1987
+ * header (board look) rather than among the header actions.
1988
+ *
1989
+ * @spec openspec/changes/screens-dashboard-greeting-header/specs/dashboard-page/spec.md#requirement-a-switch-row-carries-the-view-switch-and-link-pills
1990
+ * @return {boolean}
1991
+ */
1992
+ switchInRow() {
1993
+ return this.isBoardLook && this.showsPageViewSwitch
1994
+ },
1995
+
1996
+ /**
1997
+ * `viewLinks` as render descriptors: a router link for a route, an
1998
+ * anchor for an href, the label translated. Entries without a label
1999
+ * or a target are dropped.
2000
+ *
2001
+ * @spec openspec/changes/screens-dashboard-greeting-header/specs/dashboard-page/spec.md#requirement-a-switch-row-carries-the-view-switch-and-link-pills
2002
+ * @return {Array<{key: string, tag: string, attrs: object, label: string, icon: string}>}
2003
+ */
2004
+ resolvedViewLinks() {
2005
+ const links = Array.isArray(this.viewLinks) ? this.viewLinks : []
2006
+ const out = []
2007
+ links.forEach((link, index) => {
2008
+ if (!link || typeof link.label !== 'string' || link.label === '') {
2009
+ return
2010
+ }
2011
+ const icon = typeof link.icon === 'string' ? link.icon : ''
2012
+ const label = this.effectiveTranslate(link.label)
2013
+ if (link.route && this.$router) {
2014
+ const to = typeof link.route === 'string' ? { name: link.route } : link.route
2015
+ out.push({ key: `route-${index}`, tag: 'router-link', attrs: { to }, label, icon })
2016
+ } else if (typeof link.href === 'string' && link.href !== '') {
2017
+ out.push({ key: `href-${index}`, tag: 'a', attrs: { href: link.href }, label, icon })
2018
+ }
2019
+ })
2020
+ return out
2021
+ },
2022
+
1851
2023
  /**
1852
2024
  * The page title run through the host translate function so a
1853
2025
  * manifest-authored source string localises.
@@ -4392,6 +4564,49 @@ export default {
4392
4564
  height: var(--cn-board-control-height, 40px);
4393
4565
  }
4394
4566
 
4567
+ /* The switch row under the header (screens-dashboard-greeting-header): the
4568
+ view switch left, the `viewLinks` pills right (DqMijnWerk: pills 34px high,
4569
+ radius 17, 13px/600, a 14px icon, primary light on primary deep). */
4570
+ .cn-dashboard-page__switch-row {
4571
+ display: flex;
4572
+ flex-wrap: wrap;
4573
+ align-items: center;
4574
+ gap: 10px;
4575
+ margin-bottom: var(--cn-board-section-gap, 20px);
4576
+ }
4577
+
4578
+ .cn-dashboard-page__switch-row-spacer {
4579
+ flex: 1 1 auto;
4580
+ }
4581
+
4582
+ .cn-dashboard-page__view-link {
4583
+ display: inline-flex;
4584
+ align-items: center;
4585
+ gap: 6px;
4586
+ height: 34px;
4587
+ padding: 0 12px;
4588
+ border-radius: 17px;
4589
+ background: var(--color-primary-element-light);
4590
+ color: var(--color-primary-element-light-text);
4591
+ font-size: 13px;
4592
+ font-weight: 600;
4593
+ white-space: nowrap;
4594
+ text-decoration: none !important;
4595
+ }
4596
+
4597
+ .cn-dashboard-page__view-link:hover {
4598
+ background: var(--color-primary-element-light-hover, var(--color-primary-element-light));
4599
+ }
4600
+
4601
+ .cn-dashboard-page__view-link:focus-visible {
4602
+ outline: 2px solid var(--color-main-text);
4603
+ outline-offset: 2px;
4604
+ }
4605
+
4606
+ .cn-dashboard-page__view-link-icon {
4607
+ display: inline-flex;
4608
+ }
4609
+
4395
4610
  /* The period group, in a row of its own between the header and the KPI row. */
4396
4611
  .cn-dashboard-page__date-range--segmented {
4397
4612
  margin-bottom: 16px;
@@ -198,9 +198,11 @@ export default {
198
198
  | `rowIndicatorCap` | Number | `3` | How many declared indicators render on the row before the rest move into the row menu, reachable as `indicatorsFor(row).overflow`. |
199
199
  | `selectAllLabel` | String | `'Select all rows'` | Accessible name (`aria-label`) for the header select-all checkbox, so screen readers announce a named control (WCAG 4.1.2) |
200
200
  | `selectRowLabel` | String | `'Select row'` | Accessible name (`aria-label`) for each per-row select checkbox, so screen readers announce a named control (WCAG 4.1.2) |
201
+ | `rowTitle` | String | `'link'` | Board look only: how the title column draws its title. `link` underlines the title (not its secondary line). `plain` draws it as 15px bold text without an underline over a 13px muted secondary line, as the PqTickets board does; the row stays clickable. The Nextcloud look ignores it. |
201
202
  | `hideHeader` | Boolean | `false` | Hide the column-header row (`<thead>`) — for compact dashboard list widgets that want a plain bordered-row list without column labels |
202
203
  | `pinnedCount` | Number | `0` | How many data columns are pinned to the start of the table. They, and the selection and icon columns, stay in view when the table scrolls sideways |
203
204
  | `fixedLayout` | Boolean | `false` | Switch to `table-layout: fixed` so each column's `width` is authoritative rather than a hint the browser may override from cell content. Long unbreakable values (a PHP FQCN, a UUID) then wrap inside their cell instead of widening the column or painting past it, and no single unsized column soaks up all remaining width. Size every column when using it — percentages summing to 100 are easiest to reason about |
205
+ | `fitWidth` | Boolean | `false` | Fit the table to its container instead of growing past it: one column (a column's `grow: true`, else `title`, else `name`, else the first) takes the room that is left and ends in an ellipsis, and every other column keeps its content on one line, so trailing columns stay visible in a narrow tile. `CnWidgetObjectTable` turns it on |
204
206
  | `fillHeight` | Boolean | `false` | Fill the parent's height (a flex-column card / widget content area) so an optional `#footer` is pushed to the bottom instead of floating under a short list; the footer stays pinned via its sticky rule when the list overflows. No-op outside a height-constrained parent — opt-in |
205
207
  | `filterable` | Boolean | `false` | Show a filter button in every header whose column can filter: enum, boolean, text, number, date and reference columns backed by a schema property. A column opts out with `filterable: false`. Applying a filter emits `column-filter`; the host owns the state and passes it back as `activeFilters`. `CnIndexPage` turns it on by default. |
206
208
  | `activeFilters` | Object | `{}` | The active filter map, `{ paramKey: values[] }`: the same map the facet sidebar writes, so a header filter and a sidebar filter on one field show the same state. |
@@ -4,6 +4,7 @@
4
4
  data-testid="cn-object-list"
5
5
  :class="{
6
6
  'cn-table-container--board': isBoardLook,
7
+ 'cn-table-container--title-plain': isBoardLook && rowTitle === 'plain',
7
8
  'cn-table-container--scrollable': scrollable,
8
9
  'cn-table-container--borderless': borderless,
9
10
  'cn-table-container--fill': fillHeight,
@@ -96,7 +97,7 @@
96
97
  :aria-label="isScrollable ? scrollRegionLabel : undefined">
97
98
  <table
98
99
  class="cn-data-table"
99
- :class="{ 'cn-data-table--fixed': fixedLayout }"
100
+ :class="{ 'cn-data-table--fixed': fixedLayout, 'cn-data-table--fit': fitWidth }"
100
101
  data-testid="cn-object-list-table">
101
102
  <thead v-if="!hideHeader">
102
103
  <tr>
@@ -128,6 +129,7 @@
128
129
  col.sortable ? 'cn-table-header--sortable' : '',
129
130
  col.class || '',
130
131
  pinClass(leadingCount + colIndex),
132
+ isGrowColumn(col) ? 'cn-table-col--grow' : '',
131
133
  ]"
132
134
  :style="{ ...(col.width ? { width: col.width } : {}), ...pinStyle(leadingCount + colIndex) }"
133
135
  :aria-sort="ariaSortFor(col)"
@@ -256,7 +258,7 @@
256
258
  <td
257
259
  v-for="(col, colIndex) in effectiveColumns"
258
260
  :key="col.key"
259
- :class="[col.class || '', col.cellClass || '', cellClass ? cellClass(row, col) : '', pinClass(leadingCount + colIndex), colIndex === 0 ? 'cn-table-col--title' : '']"
261
+ :class="[col.class || '', col.cellClass || '', cellClass ? cellClass(row, col) : '', pinClass(leadingCount + colIndex), colIndex === 0 ? 'cn-table-col--title' : '', isGrowColumn(col) ? 'cn-table-col--grow' : '']"
260
262
  :style="{ ...(col.width ? { maxWidth: col.width } : {}), ...pinStyle(leadingCount + colIndex) }"
261
263
  @mouseenter="titleWhenClipped">
262
264
  <!-- A row with a `rowClickRoute` is a real link: this anchor
@@ -499,6 +501,16 @@ function fillSecondaryTemplate(template, valueOf) {
499
501
  * </CnDataTable>
500
502
  * ```
501
503
  */
504
+
505
+ /**
506
+ * Column keys that name an object's system dates in OpenRegister's `@self`
507
+ * block. A column may declare one as its key (`@self.created`); it renders as
508
+ * a date-time although the schema has no property behind it.
509
+ *
510
+ * @type {string[]}
511
+ */
512
+ const SYSTEM_DATE_KEYS = ['@self.created', '@self.updated', '@self.published', '@self.depublished']
513
+
502
514
  export default {
503
515
  name: 'CnDataTable',
504
516
 
@@ -761,6 +773,21 @@ export default {
761
773
  default: '',
762
774
  },
763
775
 
776
+ /**
777
+ * How the board look draws the title column: `link` (the default) is
778
+ * the 15px weight 600 underlined title of the DqZaken board; `plain` is
779
+ * the 15px bold title without an underline and a 13px muted secondary
780
+ * line, as the PqTickets and OcPublicaties boards draw it. The row stays
781
+ * clickable either way. The Nextcloud look ignores it.
782
+ *
783
+ * @spec openspec/changes/screens-table-rows-parity/specs/index-list-board-look/spec.md#requirement-a-row-title-can-be-plain-text
784
+ */
785
+ rowTitle: {
786
+ type: String,
787
+ default: 'link',
788
+ validator: (value) => ['link', 'plain'].includes(value),
789
+ },
790
+
764
791
  /**
765
792
  * Drop the container's card chrome (border, radius, shadow) so the table
766
793
  * sits flush inside a parent that already provides a card (e.g. a
@@ -820,6 +847,23 @@ export default {
820
847
  default: false,
821
848
  },
822
849
 
850
+ /**
851
+ * Fit the table to its container instead of letting it grow past it.
852
+ * One column takes the room that is left and cuts its text with an
853
+ * ellipsis (the full value stays in the tooltip); every other column
854
+ * keeps its content on one line at its own width, so trailing columns
855
+ * such as a date stay visible in a narrow tile. The column that grows
856
+ * is the first with `grow: true`, else the one keyed `title` or `name`,
857
+ * else the first column. CnWidgetObjectTable turns this on.
858
+ *
859
+ * @type {boolean}
860
+ * @spec openspec/changes/narrow-object-table-widgets/specs/narrow-object-table-widgets/spec.md#requirement-a-narrow-object-table-keeps-its-trailing-columns-in-view
861
+ */
862
+ fitWidth: {
863
+ type: Boolean,
864
+ default: false,
865
+ },
866
+
823
867
  /**
824
868
  * Max number of rows to display. When the total exceeds it, only the first
825
869
  * `limit` render and the "View all" footer appears (with `viewAllRoute`).
@@ -1025,6 +1069,25 @@ export default {
1025
1069
  },
1026
1070
 
1027
1071
  /** @return {number} Leading cells before the data columns (selection, icon). */
1072
+ /**
1073
+ * The key of the column that takes the spare width under `fitWidth`:
1074
+ * the first with `grow: true`, else `title` or `name`, else the first.
1075
+ *
1076
+ * @return {string|null} The column key, or null when not fitting.
1077
+ * @spec openspec/changes/narrow-object-table-widgets/specs/narrow-object-table-widgets/spec.md#requirement-a-narrow-object-table-keeps-its-trailing-columns-in-view
1078
+ */
1079
+ growColumnKey() {
1080
+ if (!this.fitWidth || this.effectiveColumns.length === 0) {
1081
+ return null
1082
+ }
1083
+ const cols = this.effectiveColumns
1084
+ const chosen = cols.find((c) => c && c.grow === true)
1085
+ || cols.find((c) => c && c.key === 'title')
1086
+ || cols.find((c) => c && c.key === 'name')
1087
+ || cols[0]
1088
+ return chosen ? chosen.key : null
1089
+ },
1090
+
1028
1091
  leadingCount() {
1029
1092
  return (this.selectable ? 1 : 0) + (this.rowIcon ? 1 : 0)
1030
1093
  },
@@ -1321,6 +1384,16 @@ export default {
1321
1384
  },
1322
1385
 
1323
1386
  methods: {
1387
+ /**
1388
+ * Whether a column is the one that takes the spare width (`fitWidth`).
1389
+ *
1390
+ * @param {object} col The column.
1391
+ * @return {boolean}
1392
+ */
1393
+ isGrowColumn(col) {
1394
+ return this.growColumnKey !== null && !!col && col.key === this.growColumnKey
1395
+ },
1396
+
1324
1397
  /**
1325
1398
  * Whether a row's record changed since the user last looked
1326
1399
  * (`@self.unread`). A row without the marker renders as it always did.
@@ -1651,11 +1724,17 @@ export default {
1651
1724
  * the schema property's own `colorMap` / `x-color-map`.
1652
1725
  *
1653
1726
  * @param {object} col Column definition.
1727
+ * @spec openspec/changes/screens-table-footer-and-system-dates-parity/specs/index-list-board-look/spec.md#requirement-a-system-date-can-be-a-column
1654
1728
  * @return {object} Property definition for CnCellRenderer.
1655
1729
  * @spec openspec/changes/screens-cell-pill-parity/specs/cell-pill-tones/spec.md#requirement-a-manifest-column-colours-its-enum-pills
1656
1730
  */
1657
1731
  columnProperty(col) {
1658
1732
  const base = this.getSchemaProperty(col.key)
1733
+ // A system date (`@self.created`, `@self.updated`, ...) has no
1734
+ // schema property behind it; it renders as a date-time.
1735
+ if (Object.keys(base).length === 0 && SYSTEM_DATE_KEYS.includes(col.key) && !col.type && !col.format) {
1736
+ return { type: 'string', format: 'date-time' }
1737
+ }
1659
1738
  const colorMap = col && col.colorMap && typeof col.colorMap === 'object' && !Array.isArray(col.colorMap) ? col.colorMap : null
1660
1739
  if (col && (col.format || col.type || col.enum || colorMap)) {
1661
1740
  return {
@@ -244,7 +244,9 @@ import CnObjectFilesWidget from '../CnObjectFilesWidget/CnObjectFilesWidget.vue'
244
244
  import CnObjectGeoWidget from '../CnObjectGeoWidget/CnObjectGeoWidget.vue'
245
245
  import CnRelatedObjectsWidget from '../CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue'
246
246
  import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'
247
+ import { normalizeLook } from '../../composables/useLook.js'
247
248
  import { isAppInstalled } from '../../utils/appInstalled.js'
249
+ import { isActivityWidget } from '../../utils/headerMeta.js'
248
250
  import { PANEL_ACTION_SINK } from '../../utils/panelActions.js'
249
251
  import {
250
252
  isCardWidgetDef,
@@ -332,6 +334,11 @@ export default {
332
334
  from: PANEL_ACTION_SINK,
333
335
  default: null,
334
336
  },
337
+
338
+ /** The app's look, provided by CnAppRoot or CnPageRenderer (`nextcloud` or `board`). */
339
+ cnLook: { default: 'nextcloud' },
340
+ /** The host's translate (CnAppRoot provides `cnTranslate`). */
341
+ cnTranslate: { default: () => (key) => key },
335
342
  },
336
343
 
337
344
  /**
@@ -780,7 +787,9 @@ export default {
780
787
  integrationProps() {
781
788
  return {
782
789
  surface: this.surface,
783
- title: this.widget?.title || '',
790
+ // Through the host's translate: an integration card (CnDetailCard)
791
+ // prints its title as given.
792
+ title: this.translateLabel(this.widget?.title || ''),
784
793
  // A provider opting into `bareWidget` is being rendered where the
785
794
  // tab strip already supplies the card and the title, so tell it to
786
795
  // drop its own. Without this the panel gets a titled card inside
@@ -836,9 +845,46 @@ export default {
836
845
  surface: this.surface,
837
846
  })
838
847
  }
848
+ // The board look: a self-titled card (an audit trail, a calendar)
849
+ // takes the manifest title, so the side column's History card says
850
+ // what the board says instead of the widget's own default.
851
+ const title = this.cardTitle
852
+ if (title) {
853
+ base.title = title
854
+ }
839
855
  return { ...base, ...this.content }
840
856
  },
841
857
 
858
+ /**
859
+ * The title a self-titled renderer gets under the board look: the
860
+ * manifest title through the host's translate, else History for the
861
+ * activity widget. Empty outside the board look, in a tab panel, and
862
+ * when `content.title` already names the card.
863
+ *
864
+ * @return {string} The title, or ''.
865
+ * @spec openspec/changes/screens-detail-labels-parity/specs/detail-labels-board-look/spec.md#requirement-a-side-card-takes-its-manifest-title
866
+ */
867
+ cardTitle() {
868
+ if (normalizeLook(this.cnLook) !== 'board' || this.isBare || !this.widget || this.isContentOnly || this.isCard) {
869
+ return ''
870
+ }
871
+ // Only a renderer that takes a `title` prop: on any other the value
872
+ // would land on its root element as a tooltip.
873
+ const declared = this.renderer && this.renderer.props
874
+ const takesTitle = Array.isArray(declared) ? declared.includes('title') : Boolean(declared && Object.hasOwn(declared, 'title'))
875
+ if (!takesTitle) {
876
+ return ''
877
+ }
878
+ if (typeof this.content.title === 'string' && this.content.title !== '') {
879
+ return ''
880
+ }
881
+ const title = widgetTitleOf(this.widget)
882
+ if (title) {
883
+ return this.translateLabel(title)
884
+ }
885
+ return isActivityWidget(this.widget) ? t('nextcloud-vue', 'History') : ''
886
+ },
887
+
842
888
  /**
843
889
  * Whether the type declares itself a container (registry `container`).
844
890
  *
@@ -946,6 +992,20 @@ export default {
946
992
  },
947
993
 
948
994
  methods: {
995
+ /**
996
+ * A manifest label through the host's translate; '' stays ''.
997
+ *
998
+ * @param {string} text The label as written in the manifest.
999
+ * @return {string} The label in the user's language.
1000
+ * @spec openspec/changes/screens-detail-labels-parity/specs/detail-labels-board-look/spec.md#requirement-detail-labels-read-in-the-users-language
1001
+ */
1002
+ translateLabel(text) {
1003
+ if (typeof text !== 'string' || text === '') {
1004
+ return ''
1005
+ }
1006
+ return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
1007
+ },
1008
+
949
1009
  /**
950
1010
  * Re-key a call on the channel this host provides onto the one it holds.
951
1011
  * A widget publishes `set(items)`; a host nested in this one speaks the