@conduction/nextcloud-vue 2.96.0 → 2.97.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 (79) hide show
  1. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  2. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +33 -1
  3. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  4. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +383 -359
  5. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  6. package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
  7. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +17 -3
  8. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
  9. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +61 -43
  10. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
  11. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +1 -1
  12. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +159 -18
  13. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
  14. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +9 -7
  15. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
  16. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  17. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +48 -3
  18. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  19. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +46 -12
  20. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  21. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  22. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +144 -0
  23. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  24. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +488 -399
  25. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  26. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +8 -2
  27. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
  28. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +301 -18
  29. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  30. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +29 -7
  31. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  32. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +1 -1
  33. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +2 -2
  34. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -1
  35. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +2 -2
  36. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -1
  37. package/dist/esm/components/CnIndexPage/useNamedSource.js +1 -1
  38. package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
  39. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
  40. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +13 -0
  41. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  42. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +5 -1
  43. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  44. package/dist/esm/composables/indexSources.js +182 -3
  45. package/dist/esm/composables/indexSources.js.map +1 -1
  46. package/dist/esm/l10n/en.json.js +7 -1
  47. package/dist/esm/l10n/en.json.js.map +1 -1
  48. package/dist/esm/l10n/nl.json.js +7 -1
  49. package/dist/esm/l10n/nl.json.js.map +1 -1
  50. package/dist/esm/library-style.js +1 -1
  51. package/dist/esm/nextcloud-vue.css +301 -177
  52. package/dist/esm/utils/queryPresets.js +13 -2
  53. package/dist/esm/utils/queryPresets.js.map +1 -1
  54. package/dist/esm/utils/scopeListLayout.js +37 -1
  55. package/dist/esm/utils/scopeListLayout.js.map +1 -1
  56. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  57. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  58. package/dist/nextcloud-vue.cjs.js +3009 -1907
  59. package/dist/nextcloud-vue.cjs.js.map +2 -2
  60. package/dist/nextcloud-vue.css +301 -177
  61. package/l10n/en.json +7 -1
  62. package/l10n/nl.json +7 -1
  63. package/package.json +1 -1
  64. package/src/components/CnCellRenderer/CnCellRenderer.vue +57 -1
  65. package/src/components/CnConversationThread/CnConversationThread.md +12 -0
  66. package/src/components/CnConversationThread/CnConversationThread.vue +30 -4
  67. package/src/components/CnConversationThread/CnConversationWidget.vue +167 -24
  68. package/src/components/CnFactsWidget/CnFactsWidget.vue +114 -3
  69. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +508 -236
  70. package/src/components/CnFilesBrowser/filesBrowserColumns.js +8 -2
  71. package/src/components/CnIndexPage/CnIndexPage.vue +324 -24
  72. package/src/components/CnIndexPage/CnIndexPageWidgets.vue +13 -4
  73. package/src/components/CnIndexPage/useNamedSource.js +1 -1
  74. package/src/components/CnObjectSidebar/CnFilesTab.vue +17 -0
  75. package/src/composables/indexSources.js +182 -3
  76. package/src/schemas/app-manifest-v2.schema.json +11 -4
  77. package/src/utils/queryPresets.js +13 -2
  78. package/src/utils/scopeListLayout.js +37 -1
  79. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -144,7 +144,8 @@ export function attributePropertiesFor(columns) {
144
144
  }
145
145
 
146
146
  /**
147
- * The value one column shows for one node.
147
+ * The value one column shows for one node. A `row` column's key may be a
148
+ * dotted path into the host's record.
148
149
  *
149
150
  * @param {object} column A resolved column.
150
151
  * @param {object} node The node.
@@ -169,7 +170,12 @@ export function fileColumnValue(column, node, rowData = null) {
169
170
  return undefined
170
171
  }
171
172
  const entry = byId[node.fileid] ?? byId[String(node.fileid)]
172
- return entry ? entry[column.key] : undefined
173
+ if (!entry) {
174
+ return undefined
175
+ }
176
+ // A key the record holds as written wins; otherwise a dotted key reads
177
+ // a nested field (`scanVerdict.state`, `integrity.value`).
178
+ return Object.hasOwn(entry, column.key) ? entry[column.key] : atPath(entry, column.key)
173
179
  }
174
180
  return atPath(node, column.nodeKey || column.key)
175
181
  }
@@ -13,15 +13,15 @@
13
13
  region (WCAG 2.4.6 / 1.3.1). -->
14
14
  <slot
15
15
  name="header"
16
- :title="title"
16
+ :title="presented('title')"
17
17
  :description="headerDescription"
18
18
  :icon="resolvedIcon"
19
19
  :showTitle="showTitle">
20
20
  <CnPageHeader
21
- :title="title"
21
+ :title="presented('title')"
22
22
  :description="headerDescription"
23
23
  :icon="showTitleIcon && !isBoardLook ? resolvedIcon : ''"
24
- :breadcrumb="breadcrumb"
24
+ :breadcrumb="presented('breadcrumb')"
25
25
  :meta="headerMetaText"
26
26
  :visuallyHidden="!showTitle">
27
27
  <!-- The board's header buttons (`headerButtons`), beside the
@@ -125,11 +125,11 @@
125
125
  <!-- Page tabs a named source declares (`pageTabs()`): sibling lists
126
126
  of the same thing, such as a flow list and its runs this week.
127
127
  Links, not tabs: each one is its own page. -->
128
- <nav v-if="sourcePageTabs.length > 0"
128
+ <nav v-if="effectivePageTabs.length > 0"
129
129
  class="cn-index-page__page-tabs"
130
130
  :aria-label="title || t('nextcloud-vue', 'Pages')"
131
131
  data-testid="cn-index-page-tabs">
132
- <a v-for="tab in sourcePageTabs"
132
+ <a v-for="tab in effectivePageTabs"
133
133
  :key="tab.id || tab.to"
134
134
  class="cn-index-page__page-tab"
135
135
  :class="{ 'cn-index-page__page-tab--active': isActivePageTab(tab) }"
@@ -193,7 +193,7 @@
193
193
  :sortValue="sortSelectValue"
194
194
  :showSearch="inlineSearch || isBoardLook"
195
195
  :searchValue="effectiveSearchValue"
196
- :searchPlaceholder="searchPlaceholder ? cnTranslate(searchPlaceholder) : searchPlaceholder"
196
+ :searchPlaceholder="presented('searchPlaceholder') ? cnTranslate(presented('searchPlaceholder')) : presented('searchPlaceholder')"
197
197
  :showCountWithSearch="showCountWithSearch"
198
198
  :refreshing="effectiveRefreshing"
199
199
  :refreshDisabled="refreshDisabled"
@@ -1075,6 +1075,23 @@
1075
1075
  class="cn-index-page__pagination"
1076
1076
  @pageChanged="onPageEvent"
1077
1077
  @pageSizeChanged="onPageSizeEvent" />
1078
+
1079
+ <!-- A notice under the list (`notice`): DqPrivacy's "Een
1080
+ AVG-verzoek is een zaak met ..." band. -->
1081
+ <NcNoteCard
1082
+ v-if="notice && notice.text"
1083
+ class="cn-index-page__notice"
1084
+ :type="noticeType"
1085
+ data-testid="cn-index-page-notice">
1086
+ {{ cnTranslate(notice.text) }}
1087
+ </NcNoteCard>
1088
+
1089
+ <!-- Cards after the list (`afterList`), stacked in order (PqSocial). -->
1090
+ <CnIndexPageWidgets
1091
+ v-if="presentedAfterList.length > 0"
1092
+ class="cn-index-page__after-list"
1093
+ variant="after-list"
1094
+ :widgets="presentedAfterList" />
1078
1095
  </div>
1079
1096
 
1080
1097
  <!-- The open record, beside the list on a wide screen and instead
@@ -1158,7 +1175,7 @@
1158
1175
  <script>
1159
1176
  import { getCurrentUser } from '@nextcloud/auth'
1160
1177
  import { translatePlural as n, translate as t } from '@nextcloud/l10n'
1161
- import { NcActionButton, NcActionCaption, NcActionCheckbox, NcActionLink, NcActions, NcActionSeparator, NcButton, NcCheckboxRadioSwitch, NcLoadingIcon } from '@nextcloud/vue'
1178
+ import { NcActionButton, NcActionCaption, NcActionCheckbox, NcActionLink, NcActions, NcActionSeparator, NcButton, NcCheckboxRadioSwitch, NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'
1162
1179
  import { getCurrentInstance, inject, markRaw, ref } from 'vue'
1163
1180
  import AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'
1164
1181
  import Close from 'vue-material-design-icons/Close.vue'
@@ -1200,7 +1217,7 @@ import {
1200
1217
  } from '../../utils/boardReferenceColumns.js'
1201
1218
  import { reportBindingProblems } from '../../utils/diagnostics.js'
1202
1219
  import { fetchGroupedCounts } from '../../utils/fetchAggregate.js'
1203
- import { fetchFilterCounts } from '../../utils/fetchFilterCounts.js'
1220
+ import { fetchFilterCounts, fetchListTotal } from '../../utils/fetchFilterCounts.js'
1204
1221
  import { buildExportUrl } from '../../utils/indexExportHelpers.js'
1205
1222
  import { lensUnavailableText } from '../../utils/lensAvailability.js'
1206
1223
  import { isModifiedClick, openRowTarget } from '../../utils/linkNavigation.js'
@@ -1219,7 +1236,7 @@ import { isNewTabClick, isNewTabHandled, isRowMiddleClick, markNewTabHandled, pr
1219
1236
  import { DEFAULT_ROW_INDICATOR_CAP } from '../../utils/rowIndicators.js'
1220
1237
  import { buildRouteQueryFromViewState, buildViewCreatePayload, extractViewState, extractViewStateFromRouteQuery, normalizeSharedWith, savedViewScope, viewMatchesScope } from '../../utils/savedViewHelpers.js'
1221
1238
  import { columnsFromSchema, fieldsFromSchema } from '../../utils/schema.js'
1222
- import { resolveScopeLayout } from '../../utils/scopeListLayout.js'
1239
+ import { resolveScopeLayout, resolveScopePresentation } from '../../utils/scopeListLayout.js'
1223
1240
  import { dispatchObjectCreated } from '../../utils/walkthroughSignals.js'
1224
1241
  import { CnActionButtons } from '../CnActionButtons/index.js'
1225
1242
  import { CnActionsBar } from '../CnActionsBar/index.js'
@@ -1490,6 +1507,7 @@ export default {
1490
1507
 
1491
1508
  components: {
1492
1509
  CnIndexPageWidgets,
1510
+ NcNoteCard,
1493
1511
  CnBuildiqEditButton,
1494
1512
  CnFollowToggle,
1495
1513
  NcLoadingIcon,
@@ -1658,6 +1676,35 @@ export default {
1658
1676
  default: '',
1659
1677
  },
1660
1678
 
1679
+ /**
1680
+ * Tabs to sibling pages above the toolbar (manifest `config.pageTabs`),
1681
+ * each `{ id?, label, route?, params?, query?, href?, countFrom?: {
1682
+ * register, schema, filter? } }`: DqPrivacy's "AVG-verzoeken |
1683
+ * Datalekken". The current page's tab is marked; `countFrom` adds the
1684
+ * count of that list. Wins over a named source's own page tabs.
1685
+ *
1686
+ * @type {Array<object>|null}
1687
+ * @spec openspec/changes/index-page-tabs-notice/specs/index-page/spec.md#requirement-an-index-page-can-link-its-sibling-pages-as-tabs
1688
+ */
1689
+ pageTabs: {
1690
+ type: Array,
1691
+ default: null,
1692
+ },
1693
+
1694
+ /**
1695
+ * A notice under the list (manifest `config.notice`): `{ text,
1696
+ * variant?: 'info' | 'success' | 'warning' }`, a band that explains
1697
+ * the list, such as who decides what. The text goes through the host
1698
+ * translate.
1699
+ *
1700
+ * @type {{text: string, variant?: string}|null}
1701
+ * @spec openspec/changes/index-page-tabs-notice/specs/index-page/spec.md#requirement-an-index-page-can-show-a-notice-under-its-list
1702
+ */
1703
+ notice: {
1704
+ type: Object,
1705
+ default: null,
1706
+ },
1707
+
1661
1708
  /** Optional description shown below the title */
1662
1709
  description: {
1663
1710
  type: String,
@@ -3045,6 +3092,33 @@ export default {
3045
3092
  default: () => [],
3046
3093
  },
3047
3094
 
3095
+ /**
3096
+ * Cards after the list (manifest `config.afterList`): dashboard widget
3097
+ * definitions `{ id, type, title?, content, headerLink? }` stacked
3098
+ * under the list in the listed order, the counterpart of `statRow`
3099
+ * above it. A query preset or a scope can set its own.
3100
+ *
3101
+ * @spec openspec/changes/index-scope-presentation/specs/index-page/spec.md#requirement-an-index-page-can-draw-widgets-after-its-list
3102
+ * @type {Array<object>}
3103
+ */
3104
+ afterList: {
3105
+ type: Array,
3106
+ default: () => [],
3107
+ },
3108
+
3109
+ /**
3110
+ * The route query pairs that chose this page's query preset, set by
3111
+ * CnPageRenderer. They scope the list, so they show no removable
3112
+ * active-filter chip.
3113
+ *
3114
+ * @spec openspec/changes/index-scope-presentation/specs/index-page/spec.md#requirement-the-query-that-chose-a-preset-is-not-a-removable-filter
3115
+ * @type {object|null}
3116
+ */
3117
+ fixedFilters: {
3118
+ type: Object,
3119
+ default: null,
3120
+ },
3121
+
3048
3122
  /**
3049
3123
  * Where a row carries the actions the server says this caller may run
3050
3124
  * on it: a dotted path, read off the rows the list already fetched, so
@@ -3645,6 +3719,8 @@ export default {
3645
3719
  tabCounts: null,
3646
3720
  // The counts of the `filterStrip` pills, by pill key ('all', 'v0', ...).
3647
3721
  filterStripCounts: null,
3722
+ /** The `countFrom` counts of the manifest page tabs, by index. */
3723
+ pageTabCounts: {},
3648
3724
  /** The tabs `quickFiltersFrom` produced, or null before they load. */
3649
3725
  generatedQuickFilters: null,
3650
3726
  /** Count per saved-view id, for the views control; null = none. */
@@ -4194,7 +4270,8 @@ export default {
4194
4270
  }
4195
4271
  // The index boards draw all four segments, so the board look does
4196
4272
  // too unless the manifest names its own list.
4197
- const listed = Array.isArray(this.viewSwitch) ? this.viewSwitch : ['table', 'cards', 'board', 'map']
4273
+ const viewSwitch = this.presented('viewSwitch')
4274
+ const listed = Array.isArray(viewSwitch) ? viewSwitch : ['table', 'cards', 'board', 'map']
4198
4275
  const offered = [...this.effectiveToggleModes, ...(this.showMapSegment ? ['map'] : [])]
4199
4276
  return listed.filter((mode) => ['table', 'cards', 'board', 'map'].includes(mode) && !offered.includes(mode))
4200
4277
  },
@@ -4665,6 +4742,58 @@ export default {
4665
4742
  })
4666
4743
  },
4667
4744
 
4745
+ /**
4746
+ * This page's own value of each presentation key, which a query preset
4747
+ * may have set: what `presented()` falls back to without a scope.
4748
+ *
4749
+ * @return {object}
4750
+ * @spec openspec/changes/index-scope-presentation/specs/index-page/spec.md#requirement-a-preset-or-a-scope-sets-the-list-s-presentation
4751
+ */
4752
+ ownPresentation() {
4753
+ return {
4754
+ title: this.title,
4755
+ quickFilters: this.quickFilters,
4756
+ cardFields: this.cardFields,
4757
+ countText: this.countText,
4758
+ searchPlaceholder: this.searchPlaceholder,
4759
+ footerNote: this.footerNote,
4760
+ viewSwitch: this.viewSwitch,
4761
+ addLabel: this.addLabel,
4762
+ headerButtons: this.headerButtons,
4763
+ breadcrumb: this.breadcrumb,
4764
+ headerMeta: this.headerMeta,
4765
+ afterList: this.afterList,
4766
+ }
4767
+ },
4768
+
4769
+ /**
4770
+ * The presentation the active scope declares (`title`, `countText`,
4771
+ * `quickFilters`, `addLabel`, `afterList` and the other query preset
4772
+ * keys), from its folder entry or its row's `x-index`. Empty without a
4773
+ * scope, so the page's own values stand.
4774
+ *
4775
+ * @return {object}
4776
+ * @spec openspec/changes/index-scope-presentation/specs/index-page/spec.md#requirement-a-preset-or-a-scope-sets-the-list-s-presentation
4777
+ */
4778
+ activeScopePresentation() {
4779
+ if (!this.folderSidebar) {
4780
+ return {}
4781
+ }
4782
+ return resolveScopePresentation({ scope: this.activeScope, row: this.activeScopeRow })
4783
+ },
4784
+
4785
+ /**
4786
+ * The widgets drawn after the list: the active scope's `afterList`,
4787
+ * else the page's.
4788
+ *
4789
+ * @return {Array<object>}
4790
+ * @spec openspec/changes/index-scope-presentation/specs/index-page/spec.md#requirement-an-index-page-can-draw-widgets-after-its-list
4791
+ */
4792
+ presentedAfterList() {
4793
+ const list = this.presented('afterList')
4794
+ return Array.isArray(list) ? list : []
4795
+ },
4796
+
4668
4797
  /**
4669
4798
  * The fields the search box searches over while a scope declaring
4670
4799
  * `searchFields` is active. Read by `useSelfFetchList`'s fixed-filter
@@ -5124,8 +5253,9 @@ export default {
5124
5253
  * @return {?Array<string>}
5125
5254
  */
5126
5255
  resolvedCardFields() {
5127
- if (Array.isArray(this.cardFields) && this.cardFields.length > 0) {
5128
- return this.cardFields
5256
+ const cardFields = this.presented('cardFields')
5257
+ if (Array.isArray(cardFields) && cardFields.length > 0) {
5258
+ return cardFields
5129
5259
  }
5130
5260
  if (!this.isBoardLook) {
5131
5261
  return null
@@ -5164,6 +5294,10 @@ export default {
5164
5294
  }
5165
5295
  })
5166
5296
  }
5297
+ // An `actions` column: the page's row actions it names, as buttons.
5298
+ cols = cols.map((col) => (col && typeof col === 'object' && col.widget === 'actions'
5299
+ ? { ...col, sortable: false, widgetProps: { ...(col.widgetProps || {}), items: (row) => this.actionColumnItems(col, row), run: this.runColumnAction } }
5300
+ : col))
5167
5301
  // The Recent lens owns the order: no column sorts while it is on.
5168
5302
  if (this.recentLensActive) {
5169
5303
  cols = cols.map((col) => (col && typeof col === 'object' ? { ...col, sortable: false } : col))
@@ -6040,6 +6174,51 @@ export default {
6040
6174
  return (source.pageTabs() || []).filter((tab) => tab && tab.to && tab.label)
6041
6175
  },
6042
6176
 
6177
+ /**
6178
+ * The page tabs to draw: the manifest's `pageTabs`, each with its
6179
+ * `countFrom` count once known, else a named source's own.
6180
+ *
6181
+ * @return {Array<object>} The tabs, `{ id, label, to, count? }`.
6182
+ * @spec openspec/changes/index-page-tabs-notice/specs/index-page/spec.md#requirement-an-index-page-can-link-its-sibling-pages-as-tabs
6183
+ */
6184
+ /**
6185
+ * The notice band's NcNoteCard type: its `variant`, else info.
6186
+ *
6187
+ * @return {string}
6188
+ * @spec openspec/changes/index-page-tabs-notice/specs/index-page/spec.md#requirement-an-index-page-can-show-a-notice-under-its-list
6189
+ */
6190
+ noticeType() {
6191
+ const variant = this.notice && this.notice.variant
6192
+ return ['success', 'warning', 'error'].includes(variant) ? variant : 'info'
6193
+ },
6194
+
6195
+ effectivePageTabs() {
6196
+ if (!Array.isArray(this.pageTabs) || this.pageTabs.length === 0) {
6197
+ return this.sourcePageTabs
6198
+ }
6199
+ return this.pageTabs
6200
+ .filter((tab) => tab && typeof tab.label === 'string' && tab.label !== '' && (tab.route || tab.to || tab.href))
6201
+ .map((tab, index) => {
6202
+ const to = tab.route
6203
+ ? { name: tab.route, ...(tab.params ? { params: tab.params } : {}), ...(tab.query ? { query: tab.query } : {}) }
6204
+ : (tab.to || tab.href)
6205
+ const count = this.pageTabCounts[index]
6206
+ return { id: tab.id || `page-tab-${index}`, label: this.cnTranslate(tab.label), to, ...(typeof count === 'number' ? { count } : {}) }
6207
+ })
6208
+ },
6209
+
6210
+ /**
6211
+ * What the page tab counts depend on, as one key.
6212
+ *
6213
+ * @return {string}
6214
+ * @spec openspec/changes/index-page-tabs-notice/specs/index-page/spec.md#requirement-an-index-page-can-link-its-sibling-pages-as-tabs
6215
+ */
6216
+ pageTabCountKey() {
6217
+ const tabs = Array.isArray(this.pageTabs) ? this.pageTabs : []
6218
+ const sources = tabs.map((tab) => (tab && tab.countFrom && tab.countFrom.register && tab.countFrom.schema ? tab.countFrom : null))
6219
+ return sources.some(Boolean) ? JSON.stringify(sources) : ''
6220
+ },
6221
+
6043
6222
  /**
6044
6223
  * The numbers on the quick filters: a named source counts its own
6045
6224
  * tabs (`tabCounts()`), a self-fetching page asks OpenRegister.
@@ -6061,11 +6240,15 @@ export default {
6061
6240
  * @return {string} The note, translated, or ''.
6062
6241
  */
6063
6242
  effectiveFooterNote() {
6064
- if (this.footerNote) {
6065
- return this.cnTranslate(this.footerNote)
6243
+ // The scope's note, else the page's (a preset may have set it),
6244
+ // else the named source's.
6245
+ const scoped = this.activeScopePresentation ? this.activeScopePresentation.footerNote : undefined
6246
+ const footerNote = scoped !== undefined ? scoped : this.footerNote
6247
+ if (footerNote) {
6248
+ return this.cnTranslate(footerNote)
6066
6249
  }
6067
6250
  const source = this.isNamedSource ? this.namedSource : null
6068
- return (source && source.footerNote) || this.footerNote
6251
+ return (source && source.footerNote) || footerNote
6069
6252
  },
6070
6253
 
6071
6254
  /**
@@ -6076,7 +6259,7 @@ export default {
6076
6259
  * @return {Array<object>|null} The tabs, or null when there are none.
6077
6260
  */
6078
6261
  effectiveQuickFilters() {
6079
- const own = withPersonalLenses(this.quickFilters || this.generatedQuickFilters, this.personalLenses)
6262
+ const own = withPersonalLenses(this.presented('quickFilters') || this.generatedQuickFilters, this.personalLenses)
6080
6263
  if (own && own.length > 0) {
6081
6264
  return own
6082
6265
  }
@@ -6142,7 +6325,8 @@ export default {
6142
6325
  * @return {Array<{key: string, label: string, action: string, variant: string, icon: string, format: string}>}
6143
6326
  */
6144
6327
  resolvedHeaderButtons() {
6145
- const declared = Array.isArray(this.headerButtons) ? this.headerButtons : []
6328
+ const headerButtons = this.presented('headerButtons')
6329
+ const declared = Array.isArray(headerButtons) ? headerButtons : []
6146
6330
  return declared
6147
6331
  .filter((button) => button && typeof button === 'object' && typeof button.action === 'string' && button.action !== '')
6148
6332
  .map((button, index) => {
@@ -6267,6 +6451,7 @@ export default {
6267
6451
  for (const [key, raw] of Object.entries(this.effectiveActiveFilters || {})) {
6268
6452
  const values = (Array.isArray(raw) ? raw : [raw])
6269
6453
  .filter((value) => value !== null && value !== undefined && value !== '')
6454
+ .filter((value) => !this.isFixedFilter(key, value))
6270
6455
  .map((value) => this.activeFilterValueLabel(key, value))
6271
6456
  if (values.length === 0) {
6272
6457
  continue
@@ -6450,11 +6635,12 @@ export default {
6450
6635
  * @return {string}
6451
6636
  */
6452
6637
  headerMetaText() {
6453
- if (!this.headerMeta) {
6638
+ const headerMeta = this.presented('headerMeta')
6639
+ if (!headerMeta) {
6454
6640
  return ''
6455
6641
  }
6456
6642
  const total = this.effectivePagination?.total
6457
- return this.cnTranslate(this.headerMeta)
6643
+ return this.cnTranslate(headerMeta)
6458
6644
  .replace('{shown}', String(this.effectiveObjects.length))
6459
6645
  .replace('{total}', typeof total === 'number' ? String(total) : '')
6460
6646
  },
@@ -6468,11 +6654,12 @@ export default {
6468
6654
  // The default count line is the library's own string, so it
6469
6655
  // reads "14 van 14" in Dutch; a page's own template goes
6470
6656
  // through the app's label lookup.
6471
- if (!this.countText && !this.countSubtitle) {
6657
+ const countText = this.presented('countText')
6658
+ if (!countText && !this.countSubtitle) {
6472
6659
  return t('nextcloud-vue', '{shown} of {total}', { shown, total })
6473
6660
  }
6474
- if (this.countText) {
6475
- return this.cnTranslate(this.countText)
6661
+ if (countText) {
6662
+ return this.cnTranslate(countText)
6476
6663
  .replace('{shown}', String(shown))
6477
6664
  .replace('{total}', String(total))
6478
6665
  }
@@ -6486,8 +6673,10 @@ export default {
6486
6673
 
6487
6674
  /** Add button label — derived from schema.title if not explicitly set */
6488
6675
  resolvedAddLabel() {
6489
- if (this.addLabel) {
6490
- return this.cnTranslate(this.addLabel)
6676
+ const scoped = this.activeScopePresentation ? this.activeScopePresentation.addLabel : undefined
6677
+ const addLabel = scoped !== undefined ? scoped : this.addLabel
6678
+ if (addLabel) {
6679
+ return this.cnTranslate(addLabel)
6491
6680
  }
6492
6681
  // A named source names its own create action. Without this the button
6493
6682
  // falls back to a schema-derived noun, and a named source has no
@@ -6712,6 +6901,14 @@ export default {
6712
6901
  },
6713
6902
  },
6714
6903
 
6904
+ pageTabCountKey: {
6905
+ immediate: true,
6906
+ /** @spec openspec/changes/index-page-tabs-notice/specs/index-page/spec.md#requirement-an-index-page-can-link-its-sibling-pages-as-tabs */
6907
+ handler() {
6908
+ this.loadPageTabCounts()
6909
+ },
6910
+ },
6911
+
6715
6912
  filterStripCountKey: {
6716
6913
  immediate: true,
6717
6914
  /** @spec openspec/changes/screens-filter-strip/specs/index-filter-strip/spec.md#requirement-an-index-page-can-draw-a-second-filter-strip */
@@ -6979,6 +7176,36 @@ export default {
6979
7176
  },
6980
7177
 
6981
7178
  methods: {
7179
+ /**
7180
+ * A presentation value: the active scope's when it declares the key,
7181
+ * else this page's own prop (which a query preset may have set).
7182
+ *
7183
+ * @param {string} key One of the query preset keys.
7184
+ * @return {unknown} The value.
7185
+ * @spec openspec/changes/index-scope-presentation/specs/index-page/spec.md#requirement-a-preset-or-a-scope-sets-the-list-s-presentation
7186
+ */
7187
+ presented(key) {
7188
+ const scoped = this.activeScopePresentation
7189
+ return scoped[key] !== undefined ? scoped[key] : this.ownPresentation[key]
7190
+ },
7191
+
7192
+ /**
7193
+ * Whether a filter value is one of the query pairs that chose the
7194
+ * preset, so it shows no removable chip.
7195
+ *
7196
+ * @param {string} key The filter key.
7197
+ * @param {unknown} value One filter value.
7198
+ * @return {boolean}
7199
+ * @spec openspec/changes/index-scope-presentation/specs/index-page/spec.md#requirement-the-query-that-chose-a-preset-is-not-a-removable-filter
7200
+ */
7201
+ isFixedFilter(key, value) {
7202
+ const fixed = this.fixedFilters
7203
+ if (!fixed || !Object.hasOwn(fixed, key)) {
7204
+ return false
7205
+ }
7206
+ return String(fixed[key]) === String(value)
7207
+ },
7208
+
6982
7209
  /**
6983
7210
  * The manifest page that shows one object of a schema, for a linked
6984
7211
  * label. Null without a manifest or a matching detail page.
@@ -7092,6 +7319,32 @@ export default {
7092
7319
  ]
7093
7320
  },
7094
7321
 
7322
+ /**
7323
+ * Count the lists behind the manifest page tabs (`countFrom`), one list
7324
+ * request each. A failed count shows no number.
7325
+ *
7326
+ * @return {Promise<void>}
7327
+ * @spec openspec/changes/index-page-tabs-notice/specs/index-page/spec.md#requirement-an-index-page-can-link-its-sibling-pages-as-tabs
7328
+ */
7329
+ async loadPageTabCounts() {
7330
+ const key = this.pageTabCountKey
7331
+ if (key === '') {
7332
+ this.pageTabCounts = {}
7333
+ return
7334
+ }
7335
+ const ctx = typeof this.selfFetchTokenCtx === 'function' ? this.selfFetchTokenCtx() : {}
7336
+ const counts = {}
7337
+ await Promise.all(this.pageTabs.map(async (tab, index) => {
7338
+ const from = tab && tab.countFrom
7339
+ if (from && from.register && from.schema) {
7340
+ counts[index] = await fetchListTotal(from.register, from.schema, from.filter || {}, ctx).catch(() => null)
7341
+ }
7342
+ }))
7343
+ if (key === this.pageTabCountKey) {
7344
+ this.pageTabCounts = counts
7345
+ }
7346
+ },
7347
+
7095
7348
  /**
7096
7349
  * Read the counts of the `filterStrip` pills in one grouped request,
7097
7350
  * narrowed by the page filter and the active quick filter. A failed
@@ -8525,6 +8778,50 @@ export default {
8525
8778
  this.$emit('quick-edit-save', { id, patch })
8526
8779
  },
8527
8780
 
8781
+ /**
8782
+ * The buttons an `actions` column shows on a row: the row actions its
8783
+ * `widgetProps.actions` names (by id, else by label), in that order,
8784
+ * that are visible for the row.
8785
+ *
8786
+ * @param {object} col The column.
8787
+ * @param {object} row The row.
8788
+ * @return {Array<{key: string, label: string, variant: string, disabled: boolean}>}
8789
+ * @spec openspec/changes/table-actions-column/specs/index-page/spec.md#requirement-a-column-can-show-row-actions-as-buttons
8790
+ */
8791
+ actionColumnItems(col, row) {
8792
+ const wanted = Array.isArray(col.widgetProps && col.widgetProps.actions) ? col.widgetProps.actions : []
8793
+ const available = this.rowActionsFor(row)
8794
+ return wanted
8795
+ .map((ref) => available.find((action) => action.id === ref) || available.find((action) => action.label === ref))
8796
+ .filter((action) => action && isRowActionVisible(action, row))
8797
+ .map((action, index) => ({
8798
+ key: action.id || action.label,
8799
+ label: this.cnTranslate(action.label),
8800
+ variant: action.variant || (index === 0 ? 'secondary' : 'tertiary'),
8801
+ disabled: typeof action.disabled === 'function' ? !!action.disabled(row) : action.disabled === true,
8802
+ }))
8803
+ },
8804
+
8805
+ /**
8806
+ * Run one `actions` column button: its handler, then the same `action`
8807
+ * event a row menu entry emits.
8808
+ *
8809
+ * @param {string} key The action's id, else its label.
8810
+ * @param {object} row The row.
8811
+ * @return {void}
8812
+ * @spec openspec/changes/table-actions-column/specs/index-page/spec.md#requirement-a-column-can-show-row-actions-as-buttons
8813
+ */
8814
+ runColumnAction(key, row) {
8815
+ const action = this.rowActionsFor(row).find((entry) => (entry.id || entry.label) === key)
8816
+ if (!action) {
8817
+ return
8818
+ }
8819
+ if (typeof action.handler === 'function') {
8820
+ action.handler(row)
8821
+ }
8822
+ this.onRowAction(rowActionPayload(action, row))
8823
+ },
8824
+
8528
8825
  /**
8529
8826
  * Take the server's version of a row after a conflict. Nothing of this
8530
8827
  * person's edit is written, and the row in the list is refreshed to
@@ -9377,6 +9674,9 @@ export default {
9377
9674
  * @return {boolean} True for the current page.
9378
9675
  */
9379
9676
  isActivePageTab(tab) {
9677
+ if (tab.to && typeof tab.to === 'object') {
9678
+ return !!this.$route && tab.to.name !== undefined && this.$route.name === tab.to.name
9679
+ }
9380
9680
  const path = this.$route && this.$route.path
9381
9681
  return !!path && String(path).replace(/\/+$/, '') === String(tab.to).replace(/\/+$/, '')
9382
9682
  },
@@ -11,7 +11,7 @@
11
11
  -->
12
12
  <template>
13
13
  <component
14
- :is="variant === 'stat-row' ? 'CnKpiGrid' : 'aside'"
14
+ :is="variant === 'stat-row' ? 'CnKpiGrid' : (variant === 'after-list' ? 'section' : 'aside')"
15
15
  v-if="entries.length > 0"
16
16
  :columns="variant === 'stat-row' ? 'auto' : undefined"
17
17
  class="cn-index-page-widgets"
@@ -25,7 +25,7 @@
25
25
  <CnWidgetWrapper
26
26
  :widgetId="entry.id"
27
27
  :title="entry.title"
28
- :showTitle="variant === 'side-panel' && entry.title !== ''"
28
+ :showTitle="variant !== 'stat-row' && entry.title !== ''"
29
29
  :showActions="false"
30
30
  :showRefresh="false"
31
31
  :headerLink="entry.headerLink"
@@ -77,11 +77,11 @@ export default {
77
77
  default: () => [],
78
78
  },
79
79
 
80
- /** Where the widgets sit: `stat-row` above the list, `side-panel` beside it. */
80
+ /** Where the widgets sit: `stat-row` above the list, `side-panel` beside it, `after-list` under it. */
81
81
  variant: {
82
82
  type: String,
83
83
  default: 'stat-row',
84
- validator: (v) => ['stat-row', 'side-panel'].includes(v),
84
+ validator: (v) => ['stat-row', 'side-panel', 'after-list'].includes(v),
85
85
  },
86
86
  },
87
87
 
@@ -172,6 +172,15 @@ export default {
172
172
  min-width: 0;
173
173
  }
174
174
 
175
+ /* The cards after the list, stacked full width (PqSocial). */
176
+ .cn-index-page-widgets--after-list {
177
+ display: flex;
178
+ flex-direction: column;
179
+ gap: var(--cn-board-section-gap, 16px);
180
+ margin-top: var(--cn-board-section-gap, 16px);
181
+ min-width: 0;
182
+ }
183
+
175
184
  /* The side panel column (DqTeamwachtrij: 300px, 16px between cards). Its
176
185
  place beside the toolbar and the list is set by CnIndexPage
177
186
  (`cn-index-page--with-side-panel`, index-page.css). */
@@ -75,7 +75,7 @@ export function useNamedSource(props, options = {}) {
75
75
  }
76
76
  }
77
77
 
78
- const source = resolveIndexSource(props.entitySource)
78
+ const source = resolveIndexSource(props.entitySource, props.sourceConfig)
79
79
 
80
80
  // An unknown source name resolves to null and was already warned about.
81
81
  // The page then behaves as an empty index rather than throwing.
@@ -21,6 +21,10 @@
21
21
  :preferenceApp="preferenceApp"
22
22
  :preferenceKey="preferenceKey"
23
23
  :bulkActions="bulkActions"
24
+ :selectionLabel="selectionLabel"
25
+ :selectionLabelOne="selectionLabelOne"
26
+ :showClearSelection="showClearSelection"
27
+ :sideCards="sideCards"
24
28
  :groupBy="groupBy"
25
29
  :facets="facets"
26
30
  @changed="onBrowserChanged" />
@@ -394,6 +398,19 @@ export default {
394
398
  * @type {Array<{id: string, label: string, icon?: string, type?: string, target?: string, props?: object}>}
395
399
  */
396
400
  bulkActions: { type: Array, default: () => [] },
401
+ /** Forwarded to CnFilesBrowser: the bulk bar's count, a template with `{count}`. */
402
+ selectionLabel: { type: String, default: '' },
403
+ /** Forwarded to CnFilesBrowser: the bulk bar's count for one file. */
404
+ selectionLabelOne: { type: String, default: '' },
405
+ /** Forwarded to CnFilesBrowser: whether the bulk bar ends in "Clear selection". */
406
+ showClearSelection: { type: Boolean, default: true },
407
+ /**
408
+ * Forwarded to CnFilesBrowser: cards beside the list about the last
409
+ * selected file.
410
+ *
411
+ * @type {Array<{id: string, title: string, fields?: Array<object>, actions?: Array<object>}>}
412
+ */
413
+ sideCards: { type: Array, default: () => [] },
397
414
  /** Forwarded to CnFilesBrowser: the key of a declared column to group the files on. */
398
415
  groupBy: { type: String, default: '' },
399
416
  /**