@conduction/nextcloud-vue 2.82.0 → 2.83.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 (78) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +2 -2
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  4. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +24 -0
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  6. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +9 -4
  7. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  8. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  9. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +9 -0
  10. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  11. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  12. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +96 -12
  13. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  14. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +95 -50
  15. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  16. package/dist/esm/components/CnMassActionBar/CnMassActionBar.vue2.js +2 -2
  17. package/dist/esm/components/CnMassActionBar/CnMassActionBar.vue2.js.map +1 -1
  18. package/dist/esm/components/CnMassImportDialog/CnMassImportDialog.vue2.js +2 -2
  19. package/dist/esm/components/CnMassImportDialog/CnMassImportDialog.vue2.js.map +1 -1
  20. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js +1 -1
  21. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js +16 -2
  22. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js.map +1 -1
  23. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js +3 -3
  24. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js.map +1 -1
  25. package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
  26. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +43 -0
  27. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  28. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +130 -53
  29. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  30. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js +1 -1
  31. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js +70 -5
  32. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js.map +1 -1
  33. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js +61 -35
  34. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js.map +1 -1
  35. package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
  36. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +3 -1
  37. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  38. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  39. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue.js +1 -1
  40. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +55 -3
  41. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
  42. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +88 -71
  43. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
  44. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js +1 -1
  45. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js +18 -4
  46. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js.map +1 -1
  47. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js.map +1 -1
  48. package/dist/esm/l10n/nl.json.js +3 -3
  49. package/dist/esm/library-style.js +1 -1
  50. package/dist/esm/nextcloud-vue.css +549 -338
  51. package/dist/esm/utils/fetchAggregate.js +25 -2
  52. package/dist/esm/utils/fetchAggregate.js.map +1 -1
  53. package/dist/esm/utils/switchValues.js +62 -0
  54. package/dist/esm/utils/switchValues.js.map +1 -0
  55. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  56. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  57. package/dist/nextcloud-vue.cjs.js +36086 -35536
  58. package/dist/nextcloud-vue.cjs.js.map +2 -2
  59. package/dist/nextcloud-vue.css +549 -338
  60. package/l10n/nl.json +3 -3
  61. package/package.json +1 -1
  62. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +31 -2
  63. package/src/components/CnDashboardPage/CnDashboardPage.vue +9 -0
  64. package/src/components/CnIndexPage/CnIndexPage.vue +125 -15
  65. package/src/components/CnRadioCardsWidget/CnRadioCardsWidget.vue +54 -3
  66. package/src/components/CnReportsPage/CnReportsPage.vue +84 -4
  67. package/src/components/CnSettingsFormWidget/CnSettingsFormWidget.vue +110 -18
  68. package/src/components/CnStatWidget/CnStatWidget.vue +3 -1
  69. package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +92 -5
  70. package/src/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue +18 -4
  71. package/src/css/index-page.css +11 -0
  72. package/src/css/index.css +1 -0
  73. package/src/css/look-board-reports.css +117 -0
  74. package/src/css/look-board.css +32 -1
  75. package/src/schemas/app-manifest-v2.schema.json +2 -2
  76. package/src/utils/fetchAggregate.js +24 -1
  77. package/src/utils/switchValues.js +59 -0
  78. package/src/utils/validateManifestV2.compiled.js +3 -3
package/l10n/nl.json CHANGED
@@ -355,7 +355,7 @@
355
355
  "Copy items": "Items kopiëren",
356
356
  "Copy of {name}": "Kopie van {name}",
357
357
  "Copy property": "Eigenschap kopiëren",
358
- "Copy selected": "Geselecteerde kopiëren",
358
+ "Copy selected": "Selectie kopiëren",
359
359
  "Copy {key}": "Kopieer {key}",
360
360
  "Create": "Aanmaken",
361
361
  "Create (C)": "Aanmaken (C)",
@@ -386,7 +386,7 @@
386
386
  "Delete note": "Notitie verwijderen",
387
387
  "Delete objects": "Objecten verwijderen",
388
388
  "Delete property": "Eigenschap verwijderen",
389
- "Delete selected": "Geselecteerde verwijderen",
389
+ "Delete selected": "Selectie verwijderen",
390
390
  "Delete {key}": "Verwijder {key}",
391
391
  "Deprecated": "Verouderd",
392
392
  "Description": "Beschrijving",
@@ -1148,7 +1148,7 @@
1148
1148
  "Could not load entries.": "Kon registraties niet laden.",
1149
1149
  "Could not create client.": "Kon klant niet aanmaken.",
1150
1150
  "{count} selected": "{count} geselecteerd",
1151
- "Clear selection": "Selectie wissen",
1151
+ "Clear selection": "Selectie opheffen",
1152
1152
  "Overdue by 1 day": "1 dag over tijd",
1153
1153
  "Overdue by {days} days": "{days} dagen over tijd",
1154
1154
  "Overdue": "Over tijd",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.82.0",
3
+ "version": "2.83.0",
4
4
  "description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
5
5
  "homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
6
6
  "bugs": {
@@ -56,6 +56,11 @@
56
56
  </div>
57
57
  </div>
58
58
 
59
+ <!-- `versionCardPosition: "end"`: the app's sections come first, and the
60
+ version card and the credential broker follow them (board DcBeheer
61
+ starts with the app's own card). The same slot, rendered once. -->
62
+ <slot v-if="versionCardAtEnd" />
63
+
59
64
  <!-- Version information card (version, up-to-date check, re-import, support footer) -->
60
65
  <CnVersionInfoCard
61
66
  v-if="showVersionCard"
@@ -151,8 +156,8 @@
151
156
  :appCredentials="appCredentials"
152
157
  data-testid="cn-admin-organisation-credentials" />
153
158
 
154
- <!-- @slot default The app's own settings sections, rendered below the version card. -->
155
- <slot />
159
+ <!-- @slot default The app's own settings sections, rendered below the version card, or above it with `versionCardPosition: "end"`. -->
160
+ <slot v-if="!versionCardAtEnd" />
156
161
 
157
162
  <!-- First-time setup wizard, opened from the admin page (ADR-042). -->
158
163
  <CnSetupWizard
@@ -358,6 +363,20 @@ export default {
358
363
  default: true,
359
364
  },
360
365
 
366
+ /**
367
+ * Where the version card (and the organisation credential broker) sits:
368
+ * `start`, above the app's sections (the default), or `end`, after them,
369
+ * for a page whose board starts with the app's own card. The keyboard
370
+ * order follows, because the slot moves in the markup.
371
+ *
372
+ * @spec openspec/changes/screens-admin-settings-version-card-end/specs/settings-board-look/spec.md#requirement-the-version-card-can-follow-the-apps-sections
373
+ */
374
+ versionCardPosition: {
375
+ type: String,
376
+ default: 'start',
377
+ validator: (value) => ['start', 'end'].includes(value),
378
+ },
379
+
361
380
  /** Version card heading. */
362
381
  versionTitle: {
363
382
  type: String,
@@ -482,6 +501,16 @@ export default {
482
501
  },
483
502
 
484
503
  computed: {
504
+ /**
505
+ * Whether the app's sections come before the version card.
506
+ *
507
+ * @return {boolean}
508
+ * @spec openspec/changes/screens-admin-settings-version-card-end/specs/settings-board-look/spec.md#requirement-the-version-card-can-follow-the-apps-sections
509
+ */
510
+ versionCardAtEnd() {
511
+ return this.versionCardPosition === 'end'
512
+ },
513
+
485
514
  /**
486
515
  * The crumbs that have a label.
487
516
  *
@@ -4578,6 +4578,15 @@ export default {
4578
4578
  // because the dispatcher only forwards `content` — honour it with a
4579
4579
  // one-time deprecation warning so those cards render while authors
4580
4580
  // migrate the key under `content`.
4581
+ // Grid layout (`layout: "grid"`): stat tiles in columns.
4582
+ const stats = content.stats !== undefined ? content.stats : props.stats
4583
+ if (Array.isArray(stats)) {
4584
+ out.stats = stats
4585
+ }
4586
+ const columns = content.columns !== undefined ? content.columns : props.columns
4587
+ if (columns !== undefined) {
4588
+ out.columns = Number(columns)
4589
+ }
4581
4590
  const contentEntries = content.entries !== undefined ? content.entries : props.entries
4582
4591
  if (contentEntries !== undefined) {
4583
4592
  out.entries = contentEntries
@@ -47,8 +47,19 @@
47
47
  actions when it names none): a header action, a built-in by
48
48
  name, or a link (an entry with `href`, the documentation). -->
49
49
  <template v-for="entry in headerMenuEntries(button)" :key="entry.key">
50
+ <!-- A group heading and a separator (`{ heading }` and
51
+ `separator` in items), as the board's "Ga naar" and
52
+ "Selectie" groups. -->
53
+ <NcActionCaption
54
+ v-if="entry.kind === 'heading'"
55
+ class="cn-index-header-menu__heading"
56
+ :name="entry.label"
57
+ :data-testid="`cn-index-header-menu-${entry.key}`" />
58
+ <NcActionSeparator
59
+ v-else-if="entry.kind === 'separator'"
60
+ :data-testid="`cn-index-header-menu-${entry.key}`" />
50
61
  <NcActionLink
51
- v-if="entry.href"
62
+ v-else-if="entry.href"
52
63
  :href="entry.href"
53
64
  target="_blank"
54
65
  rel="noopener noreferrer"
@@ -58,6 +69,17 @@
58
69
  </template>
59
70
  {{ entry.label }}
60
71
  </NcActionLink>
72
+ <NcActionLink
73
+ v-else-if="entry.link"
74
+ :href="entry.link.href"
75
+ :closeAfterClick="true"
76
+ :data-testid="`cn-index-header-menu-${entry.key}`"
77
+ @click="onHeaderMenuLinkClick(entry, $event)">
78
+ <template v-if="entry.icon" #icon>
79
+ <CnIcon :name="entry.icon" :size="20" />
80
+ </template>
81
+ {{ entry.label }}
82
+ </NcActionLink>
61
83
  <NcActionButton
62
84
  v-else
63
85
  :disabled="entry.disabled"
@@ -65,8 +87,12 @@
65
87
  :closeAfterClick="true"
66
88
  :data-testid="`cn-index-header-menu-${entry.key}`"
67
89
  @click="entry.run()">
68
- <template v-if="entry.icon" #icon>
69
- <CnIcon :name="entry.icon" :size="20" />
90
+ <!-- A built-in ships its own icon (`iconComponent`), so it
91
+ never falls back to the "?" when the app has not
92
+ registered that icon name. -->
93
+ <template v-if="entry.iconComponent || entry.icon" #icon>
94
+ <component :is="entry.iconComponent" v-if="entry.iconComponent" :size="20" />
95
+ <CnIcon v-else :name="entry.icon" :size="20" />
70
96
  </template>
71
97
  {{ entry.label }}
72
98
  </NcActionButton>
@@ -1028,7 +1054,7 @@
1028
1054
  <script>
1029
1055
  import { getCurrentUser } from '@nextcloud/auth'
1030
1056
  import { translate as t } from '@nextcloud/l10n'
1031
- import { NcActionButton, NcActionCaption, NcActionCheckbox, NcActionLink, NcActions, NcButton, NcCheckboxRadioSwitch, NcLoadingIcon } from '@nextcloud/vue'
1057
+ import { NcActionButton, NcActionCaption, NcActionCheckbox, NcActionLink, NcActions, NcActionSeparator, NcButton, NcCheckboxRadioSwitch, NcLoadingIcon } from '@nextcloud/vue'
1032
1058
  import { getCurrentInstance, inject, markRaw, ref } from 'vue'
1033
1059
  import AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'
1034
1060
  import Close from 'vue-material-design-icons/Close.vue'
@@ -1038,6 +1064,8 @@ import DotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue'
1038
1064
  import Export from 'vue-material-design-icons/Export.vue'
1039
1065
  import Eye from 'vue-material-design-icons/Eye.vue'
1040
1066
  import FilterOutline from 'vue-material-design-icons/FilterOutline.vue'
1067
+ import ImportIcon from 'vue-material-design-icons/Import.vue'
1068
+ import TrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue'
1041
1069
  import Tune from 'vue-material-design-icons/Tune.vue'
1042
1070
  import ViewColumnOutline from 'vue-material-design-icons/ViewColumnOutline.vue'
1043
1071
  import CnConfirmDialog from '../../dialogs/CnConfirmDialog.vue'
@@ -1053,13 +1081,13 @@ import { createRefLabelResolver } from '../../composables/useRefLabels.js'
1053
1081
  import { useSavedViewsApi } from '../../composables/useSavedViewsApi.js'
1054
1082
  import { METADATA_COLUMNS } from '../../constants/metadata.js'
1055
1083
  import { useObjectStore } from '../../store/useObjectStore.js'
1056
- import { routeHref } from '../../utils/actionLink.js'
1084
+ import { followItemActionLink, resolveItemActionLink, routeHref } from '../../utils/actionLink.js'
1057
1085
  import { buildOnSuccessRoute, resolveRegisteredHandler } from '../../utils/actionsDispatcher.js'
1058
1086
  import { reportBindingProblems } from '../../utils/diagnostics.js'
1059
1087
  import { fetchFilterCounts } from '../../utils/fetchFilterCounts.js'
1060
1088
  import { buildExportUrl } from '../../utils/indexExportHelpers.js'
1061
1089
  import { lensUnavailableText } from '../../utils/lensAvailability.js'
1062
- import { openRowTarget } from '../../utils/linkNavigation.js'
1090
+ import { isModifiedClick, openRowTarget } from '../../utils/linkNavigation.js'
1063
1091
  import { resolveClaimedTeams, resolveClaimTokens, splitViewsIntoTabs, viewAsTab, viewIdOf } from '../../utils/listLenses.js'
1064
1092
  import { LIST_SHORTCUTS, listPaletteCommands, shortcutFor } from '../../utils/listShortcuts.js'
1065
1093
  import { multiKeySort } from '../../utils/multiKeySort.js'
@@ -1352,6 +1380,7 @@ export default {
1352
1380
  NcActionLink,
1353
1381
  NcActionButton,
1354
1382
  NcActionCaption,
1383
+ NcActionSeparator,
1355
1384
  NcActionCheckbox,
1356
1385
  NcButton,
1357
1386
  AlertCircleOutline,
@@ -1631,9 +1660,11 @@ export default {
1631
1660
  * actions, or what its `items` name (`header-actions`, a header action
1632
1661
  * id, `import`, `export`, `refresh`, `copy-selected`, `delete-selected`,
1633
1662
  * `documentation`), so the entries the bar's menu loses have a place.
1663
+ * `items` can group them: `{ heading }` draws a group heading (an i18n
1664
+ * key) and `separator` a rule, each only when an entry follows.
1634
1665
  *
1635
1666
  * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-index-page-can-take-the-board-header
1636
- * @type {Array<{label?: string, action: string, variant?: ('primary'|'secondary'), icon?: string, format?: ('csv'|'excel'), id?: string, items?: string[]}>}
1667
+ * @type {Array<{label?: string, action: string, variant?: ('primary'|'secondary'), icon?: string, format?: ('csv'|'excel'), id?: string, items?: Array<(string|{heading: string})>}>}
1637
1668
  */
1638
1669
  headerButtons: {
1639
1670
  type: Array,
@@ -5560,7 +5591,10 @@ export default {
5560
5591
  variant: button.variant === 'primary' ? 'primary' : 'secondary',
5561
5592
  icon,
5562
5593
  format: button.format === 'excel' ? 'excel' : 'csv',
5563
- items: Array.isArray(button.items) ? button.items.filter((item) => typeof item === 'string' && item !== '') : null,
5594
+ items: Array.isArray(button.items)
5595
+ ? button.items.filter((item) => (typeof item === 'string' && item !== '')
5596
+ || (item && typeof item === 'object' && typeof item.heading === 'string' && item.heading !== ''))
5597
+ : null,
5564
5598
  }
5565
5599
  })
5566
5600
  .filter((button) => button.label !== '')
@@ -6855,11 +6889,14 @@ export default {
6855
6889
  * `import`, `export`, `refresh`, `copy-selected`, `delete-selected`
6856
6890
  * (disabled until a row is selected) and `documentation` (a link to
6857
6891
  * `documentationUrl`, left out when the page has none). An unknown name
6858
- * is left out.
6892
+ * is left out. `{ heading }` and `separator` group the entries: an
6893
+ * entry of `kind` `heading` or `separator`, drawn only when an entry
6894
+ * follows it.
6859
6895
  *
6860
6896
  * @spec openspec/changes/screens-index-header-actions-menu/specs/index-header/spec.md#requirement-the-header-actions-menu-lists-what-its-items-name
6861
- * @param {{items?: string[]|null, format?: string}} button The resolved header button.
6862
- * @return {Array<{key: string, label: string, icon: string, href?: string, disabled?: boolean, title?: string, run?: Function}>}
6897
+ * @spec openspec/changes/screens-index-header-menu-headings/specs/index-header/spec.md#requirement-the-header-actions-menu-can-draw-group-headings-and-separators
6898
+ * @param {{items?: Array<(string|{heading: string})>|null, format?: string}} button The resolved header button.
6899
+ * @return {Array<{key: string, label: string, kind?: ('heading'|'separator'), icon?: string, href?: string, disabled?: boolean, title?: string, iconComponent?: object, run?: Function}>}
6863
6900
  */
6864
6901
  headerMenuEntries(button) {
6865
6902
  const fromAction = (entry) => ({
@@ -6867,14 +6904,18 @@ export default {
6867
6904
  label: entry.label ? this.cnTranslate(entry.label) : '',
6868
6905
  icon: entry.icon && typeof entry.icon === 'string' && !entry.icon.startsWith('icon-') ? entry.icon : '',
6869
6906
  href: typeof entry.href === 'string' && entry.href !== '' ? entry.href : '',
6907
+ // A `handler: "navigate"` entry whose route resolves carries `to`
6908
+ // (resolveHeaderHandler drops its handler): the menu draws it as
6909
+ // an in-app link, as the actions bar does, so it navigates.
6910
+ link: !entry.href && entry.to && !entry.disabled ? resolveItemActionLink({ to: entry.to }, null, this.$router) : null,
6870
6911
  disabled: Boolean(entry.disabled),
6871
6912
  run: () => this.onHeaderAction({ action: entry.id, id: entry.id }),
6872
6913
  })
6873
6914
  const names = button && Array.isArray(button.items) ? button.items : ['header-actions']
6874
6915
  const noSelection = this.internalSelectedIds.length < 1
6875
6916
  const builtIns = {
6876
- import: () => ({ key: 'import', label: t('nextcloud-vue', 'Import'), icon: 'Import', run: () => { this.showImportDialog = true } }),
6877
- export: () => ({ key: 'export', label: t('nextcloud-vue', 'Export'), icon: 'Export', run: () => this.onHeaderButton({ action: 'export', format: button.format }) }),
6917
+ import: () => ({ key: 'import', label: t('nextcloud-vue', 'Import'), icon: 'Import', iconComponent: ImportIcon, run: () => { this.showImportDialog = true } }),
6918
+ export: () => ({ key: 'export', label: t('nextcloud-vue', 'Export'), icon: 'Export', iconComponent: Export, run: () => this.onHeaderButton({ action: 'export', format: button.format }) }),
6878
6919
  refresh: () => ({ key: 'refresh', label: t('nextcloud-vue', 'Refresh'), icon: 'Refresh', run: () => this.onRefreshEvent() }),
6879
6920
  'copy-selected': () => ({
6880
6921
  key: 'copy-selected',
@@ -6889,6 +6930,7 @@ export default {
6889
6930
  key: 'delete-selected',
6890
6931
  label: t('nextcloud-vue', 'Delete selected'),
6891
6932
  icon: 'TrashCanOutline',
6933
+ iconComponent: TrashCanOutline,
6892
6934
  disabled: noSelection,
6893
6935
  title: noSelection ? t('nextcloud-vue', 'Select 1 or more items to delete') : '',
6894
6936
  run: () => { this.showMassDeleteDialog = true },
@@ -6906,8 +6948,17 @@ export default {
6906
6948
  entries.push(entry)
6907
6949
  }
6908
6950
  }
6951
+ // A group heading (`{ heading }`) or a `separator` is kept as a mark
6952
+ // and only drawn when an entry follows it (see groupMenuEntries).
6953
+ let mark = 0
6909
6954
  for (const name of names) {
6910
- if (name === 'header-actions') {
6955
+ if (name && typeof name === 'object') {
6956
+ mark += 1
6957
+ entries.push({ key: `__heading-${mark}`, kind: 'heading', label: this.cnTranslate(name.heading) })
6958
+ } else if (name === 'separator') {
6959
+ mark += 1
6960
+ entries.push({ key: `__separator-${mark}`, kind: 'separator', label: '' })
6961
+ } else if (name === 'header-actions') {
6911
6962
  this.mergedHeaderActions.forEach((entry) => push(fromAction(entry)))
6912
6963
  } else if (Object.hasOwn(builtIns, name)) {
6913
6964
  push(builtIns[name]())
@@ -6918,7 +6969,66 @@ export default {
6918
6969
  }
6919
6970
  }
6920
6971
  }
6921
- return entries
6972
+ return this.groupMenuEntries(entries)
6973
+ },
6974
+
6975
+ /**
6976
+ * Tidy the marks of the header's Actions menu: a heading or separator
6977
+ * with no entry after it (before the next mark or the end) is dropped,
6978
+ * a separator never opens the menu, two separators in a row are one,
6979
+ * and a heading followed by a separator before any entry is dropped. So a group whose entries are all left out (no documentation
6980
+ * URL, an unknown id) leaves no empty heading or double rule behind,
6981
+ * and a menu of marks only has no entries and is not drawn.
6982
+ *
6983
+ * @spec openspec/changes/screens-index-header-menu-headings/specs/index-header/spec.md#requirement-the-header-actions-menu-can-draw-group-headings-and-separators
6984
+ * @param {Array<{key: string, kind?: string}>} entries The entries with their marks, in order.
6985
+ * @return {Array<{key: string, kind?: string}>} The entries to draw.
6986
+ */
6987
+ groupMenuEntries(entries) {
6988
+ const out = []
6989
+ let separator = null
6990
+ let heading = null
6991
+ for (const entry of entries) {
6992
+ if (entry.kind === 'separator') {
6993
+ // A rule ends the group before it: a heading not yet followed
6994
+ // by an entry is dropped with it.
6995
+ separator = entry
6996
+ heading = null
6997
+ continue
6998
+ }
6999
+ if (entry.kind === 'heading') {
7000
+ heading = entry
7001
+ continue
7002
+ }
7003
+ if (separator && out.length > 0) {
7004
+ out.push(separator)
7005
+ }
7006
+ if (heading) {
7007
+ out.push(heading)
7008
+ }
7009
+ separator = null
7010
+ heading = null
7011
+ out.push(entry)
7012
+ }
7013
+ return out
7014
+ },
7015
+
7016
+ /**
7017
+ * An in-app link of the header's Actions menu was clicked: a plain click
7018
+ * goes through the router (a modified click is left to the browser),
7019
+ * and the entry is reported as a header action, as the actions bar does.
7020
+ *
7021
+ * @spec openspec/changes/screens-index-header-actions-menu/specs/index-header/spec.md#requirement-the-header-actions-menu-lists-what-its-items-name
7022
+ * @param {{key: string, link: object}} entry The menu entry.
7023
+ * @param {MouseEvent} event The click.
7024
+ * @return {void}
7025
+ */
7026
+ onHeaderMenuLinkClick(entry, event) {
7027
+ if (isModifiedClick(event)) {
7028
+ return
7029
+ }
7030
+ followItemActionLink(event, entry.link, this.$router)
7031
+ this.$emit('header-action', { action: entry.key, id: entry.key })
6922
7032
  },
6923
7033
 
6924
7034
  /**
@@ -4,7 +4,10 @@
4
4
  -->
5
5
 
6
6
  <template>
7
- <fieldset class="cn-radio-cards" data-testid="cn-radio-cards" :aria-busy="busy ? 'true' : 'false'">
7
+ <fieldset class="cn-radio-cards"
8
+ :class="{ 'cn-radio-cards--list': isList }"
9
+ data-testid="cn-radio-cards"
10
+ :aria-busy="busy ? 'true' : 'false'">
8
11
  <legend v-if="label" class="cn-radio-cards__legend">
9
12
  {{ label }}
10
13
  </legend>
@@ -52,7 +55,7 @@ let uid = 0
52
55
  * underneath, so the choice is perceivable without colour and works from the
53
56
  * keyboard.
54
57
  *
55
- * @spec openspec/changes/abstract-radio-cards/specs/radio-cards/spec.md
58
+ * @spec openspec/changes/abstract-radio-cards/specs/abstract-radio-cards/spec.md
56
59
  */
57
60
  export default {
58
61
  name: 'CnRadioCardsWidget',
@@ -72,6 +75,7 @@ export default {
72
75
  * - `options` (required) — `[{ value, label, description? }]`.
73
76
  * - `label` — a legend over the cards (visually shown).
74
77
  * - `columns` — 1, 2 or 3 (default 2).
78
+ * - `variant` — `cards` (the default) or `list`: a compact list of radio buttons, one per line, with the description under the label and no card around each (board DcBeheer). A list is one column.
75
79
  * - `readOnly`.
76
80
  *
77
81
  * @type {object}
@@ -133,7 +137,20 @@ export default {
133
137
  return this.tr(this.content.label || '')
134
138
  },
135
139
 
140
+ /**
141
+ * Whether the options draw as a compact list (`variant: "list"`).
142
+ *
143
+ * @return {boolean}
144
+ * @spec openspec/changes/screens-radio-cards-list/specs/radio-cards/spec.md#requirement-radio-cards-can-draw-as-a-compact-list
145
+ */
146
+ isList() {
147
+ return this.content.variant === 'list'
148
+ },
149
+
136
150
  columns() {
151
+ if (this.isList) {
152
+ return 1
153
+ }
137
154
  return [1, 3].includes(this.content.columns) ? this.content.columns : 2
138
155
  },
139
156
 
@@ -185,7 +202,7 @@ export default {
185
202
  *
186
203
  * @param {object} option The chosen option.
187
204
  * @return {Promise<void>}
188
- * @spec openspec/changes/abstract-radio-cards/specs/radio-cards/spec.md#requirement-choosing-a-card-saves-that-one-field
205
+ * @spec openspec/changes/abstract-radio-cards/specs/abstract-radio-cards/spec.md#requirement-choosing-a-card-saves-that-one-field
189
206
  */
190
207
  async choose(option) {
191
208
  if ((!this.objectUrl && !this.hasSave) || !this.content.field) {
@@ -310,6 +327,40 @@ export default {
310
327
  color: var(--color-text-maxcontrast);
311
328
  }
312
329
 
330
+ /* `variant: "list"`: one radio button per line, the description under the
331
+ label, no card around each (board DcBeheer, "Soort organisatie"). */
332
+ .cn-radio-cards--list .cn-radio-cards__grid {
333
+ gap: 8px;
334
+ }
335
+
336
+ .cn-radio-cards--list .cn-radio-cards__card,
337
+ .cn-radio-cards--list .cn-radio-cards__card--selected {
338
+ padding: 0;
339
+ border: 0;
340
+ border-radius: 0;
341
+ background: transparent;
342
+ box-shadow: none;
343
+ font-size: 15px;
344
+ }
345
+
346
+ .cn-radio-cards--list .cn-radio-cards__dot {
347
+ width: 18px;
348
+ height: 18px;
349
+ margin-top: 2px;
350
+ }
351
+
352
+ .cn-radio-cards--list .cn-radio-cards__card--selected .cn-radio-cards__dot {
353
+ border-width: 5px;
354
+ }
355
+
356
+ .cn-radio-cards--list .cn-radio-cards__title {
357
+ font-weight: 400;
358
+ }
359
+
360
+ .cn-radio-cards--list .cn-radio-cards__description {
361
+ font-size: 13px;
362
+ }
363
+
313
364
  @media (max-width: 640px) {
314
365
  .cn-radio-cards__grid--2,
315
366
  .cn-radio-cards__grid--3 {
@@ -32,7 +32,7 @@
32
32
  Spec: hydra openspec/architecture/adr-112-reports-are-one-page.md (ConductionNL/hydra#640).
33
33
  -->
34
34
  <template>
35
- <div class="cn-reports-page">
35
+ <div class="cn-reports-page" :class="{ 'cn-reports-page--board': isBoard }">
36
36
  <div class="cn-reports-page__header">
37
37
  <h2 class="cn-reports-page__title">
38
38
  {{ resolvedTitle }}
@@ -42,7 +42,44 @@
42
42
  </p>
43
43
  </div>
44
44
 
45
- <div v-if="categoryOptions.length > 1" class="cn-reports-page__filter">
45
+ <!-- The board look (board DcRapportages): one section per category,
46
+ headed by its label, instead of the category filter; each card
47
+ draws its icon in a tile. -->
48
+ <template v-if="isBoard">
49
+ <p v-if="resolvedCards.length === 0"
50
+ class="cn-reports-page__empty"
51
+ data-testid="cn-reports-empty">
52
+ {{ resolvedEmptyLabel }}
53
+ </p>
54
+ <section v-for="section in boardSections"
55
+ :key="section.key"
56
+ class="cn-reports-page__section"
57
+ data-testid="cn-reports-section">
58
+ <h3 v-if="section.label" class="cn-reports-page__section-title">
59
+ {{ section.label }}
60
+ </h3>
61
+ <ul class="cn-reports-page__grid" data-testid="cn-reports-grid">
62
+ <li v-for="card in section.cards" :key="card.id" class="cn-reports-page__cell">
63
+ <a class="cn-reports-page__card"
64
+ data-testid="cn-report-card"
65
+ :href="hrefFor(card)"
66
+ @click="open(card, $event)">
67
+ <span v-if="card.icon" class="cn-reports-page__card-icon" aria-hidden="true">
68
+ <CnIcon :name="card.icon" :size="22" />
69
+ </span>
70
+ <span class="cn-reports-page__card-text">
71
+ <span class="cn-reports-page__card-title">{{ card.label }}</span>
72
+ <span v-if="card.description" class="cn-reports-page__card-description">
73
+ {{ card.description }}
74
+ </span>
75
+ </span>
76
+ </a>
77
+ </li>
78
+ </ul>
79
+ </section>
80
+ </template>
81
+
82
+ <div v-if="!isBoard && categoryOptions.length > 1" class="cn-reports-page__filter">
46
83
  <label class="cn-reports-page__filter-label" :for="filterId">
47
84
  {{ resolvedCategoryLabel }}
48
85
  </label>
@@ -58,13 +95,13 @@
58
95
  </select>
59
96
  </div>
60
97
 
61
- <p v-if="visibleCards.length === 0"
98
+ <p v-if="!isBoard && visibleCards.length === 0"
62
99
  class="cn-reports-page__empty"
63
100
  data-testid="cn-reports-empty">
64
101
  {{ resolvedEmptyLabel }}
65
102
  </p>
66
103
 
67
- <ul v-else class="cn-reports-page__grid" data-testid="cn-reports-grid">
104
+ <ul v-else-if="!isBoard" class="cn-reports-page__grid" data-testid="cn-reports-grid">
68
105
  <li v-for="card in visibleCards" :key="card.id" class="cn-reports-page__cell">
69
106
  <a class="cn-reports-page__card"
70
107
  data-testid="cn-report-card"
@@ -98,6 +135,8 @@
98
135
  */
99
136
  import { translate as t } from '@nextcloud/l10n'
100
137
  import { defineAsyncComponent } from 'vue'
138
+ import CnIcon from '../CnIcon/CnIcon.vue'
139
+ import { useLook } from '../../composables/useLook.js'
101
140
  import { followLinkClick } from '../../utils/linkNavigation.js'
102
141
 
103
142
  export default {
@@ -107,6 +146,7 @@ export default {
107
146
  // Loaded only for a page that declares widgets: a cards-only reports
108
147
  // page does not pull the dashboard in.
109
148
  CnDashboardPage: defineAsyncComponent(() => import('../CnDashboardPage/CnDashboardPage.vue').then((m) => m.default)),
149
+ CnIcon,
110
150
  },
111
151
 
112
152
  inject: {
@@ -228,6 +268,17 @@ export default {
228
268
  default: null,
229
269
  },
230
270
 
271
+ /**
272
+ * The look to draw in: `board` or `nextcloud`. Absent: the look
273
+ * CnAppRoot provides (`cnLook`), else `nextcloud`.
274
+ *
275
+ * @type {string|null}
276
+ */
277
+ look: {
278
+ type: String,
279
+ default: null,
280
+ },
281
+
231
282
  /**
232
283
  * Translate function. Falls back to injected `cnTranslate`.
233
284
  *
@@ -239,6 +290,11 @@ export default {
239
290
  },
240
291
  },
241
292
 
293
+ setup(props) {
294
+ const { isBoard } = useLook(props)
295
+ return { isBoard }
296
+ },
297
+
242
298
  data() {
243
299
  return {
244
300
  activeCategory: 'all',
@@ -385,6 +441,30 @@ export default {
385
441
  return options
386
442
  },
387
443
 
444
+ /**
445
+ * The board look's sections: the cards without a category first, under
446
+ * no heading, then one section per declared category that holds a
447
+ * card, in the declared order, headed by its label.
448
+ *
449
+ * @spec openspec/changes/screens-reports-page-board-look/specs/reports-page/spec.md#requirement-the-reports-page-takes-the-board-look
450
+ * @return {Array<{key: string, label: string, cards: Array<object>}>} The sections.
451
+ */
452
+ boardSections() {
453
+ const declared = this.config.categories || {}
454
+ const sections = []
455
+ const loose = this.resolvedCards.filter((card) => !card.category || !Object.hasOwn(declared, card.category))
456
+ if (loose.length > 0) {
457
+ sections.push({ key: '__none', label: '', cards: loose })
458
+ }
459
+ for (const key of Object.keys(declared)) {
460
+ const cards = this.resolvedCards.filter((card) => card.category === key)
461
+ if (cards.length > 0) {
462
+ sections.push({ key, label: this.tr(declared[key]), cards })
463
+ }
464
+ }
465
+ return sections
466
+ },
467
+
388
468
  /**
389
469
  * The cards the active filter admits.
390
470
  *