@conduction/nextcloud-vue 2.96.0 → 2.98.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 (163) hide show
  1. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +17 -0
  2. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  4. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  5. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +33 -1
  6. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  7. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +383 -359
  8. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  9. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  10. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +106 -2
  11. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  12. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  13. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +9 -0
  14. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js.map +1 -0
  15. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js +964 -0
  16. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -0
  17. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js +282 -0
  18. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -0
  19. package/dist/esm/components/CnChecklistRunWidget/index.js +28 -0
  20. package/dist/esm/components/CnChecklistRunWidget/index.js.map +1 -0
  21. package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
  22. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +17 -3
  23. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
  24. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +61 -43
  25. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
  26. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +1 -1
  27. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +159 -18
  28. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
  29. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +9 -7
  30. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
  31. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  32. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +1 -0
  33. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  34. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  35. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +26 -8
  36. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  37. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  38. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +188 -5
  39. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  40. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  41. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  42. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +48 -3
  43. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  44. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +46 -12
  45. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  46. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue.js +1 -1
  47. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +3 -3
  48. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
  49. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
  50. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js +1 -1
  51. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js +27 -0
  52. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js.map +1 -1
  53. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js +16 -2
  54. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js.map +1 -1
  55. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  56. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +144 -0
  57. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  58. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +488 -399
  59. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  60. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +8 -2
  61. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
  62. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +344 -21
  63. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  64. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +29 -7
  65. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  66. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +1 -1
  67. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +2 -2
  68. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -1
  69. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +2 -2
  70. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -1
  71. package/dist/esm/components/CnIndexPage/useNamedSource.js +1 -1
  72. package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
  73. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
  74. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +13 -0
  75. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  76. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +5 -1
  77. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  78. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js +9 -0
  79. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js.map +1 -0
  80. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js +220 -0
  81. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js.map +1 -0
  82. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js +121 -0
  83. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js.map +1 -0
  84. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +1 -1
  85. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js +83 -9
  86. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -1
  87. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js +80 -11
  88. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -1
  89. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  90. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  91. package/dist/esm/composables/indexSources.js +420 -5
  92. package/dist/esm/composables/indexSources.js.map +1 -1
  93. package/dist/esm/composables/useOrganisationStore.js +115 -0
  94. package/dist/esm/composables/useOrganisationStore.js.map +1 -0
  95. package/dist/esm/index.js +2 -0
  96. package/dist/esm/index.js.map +1 -1
  97. package/dist/esm/l10n/en.json.js +21 -1
  98. package/dist/esm/l10n/en.json.js.map +1 -1
  99. package/dist/esm/l10n/nl.json.js +36 -1
  100. package/dist/esm/l10n/nl.json.js.map +1 -1
  101. package/dist/esm/library-style.js +1 -1
  102. package/dist/esm/nextcloud-vue.css +725 -383
  103. package/dist/esm/utils/builtInFormatters.js +34 -1
  104. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  105. package/dist/esm/utils/libraryWidgetKeys.js +1 -0
  106. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  107. package/dist/esm/utils/matrixCells.js +158 -0
  108. package/dist/esm/utils/matrixCells.js.map +1 -0
  109. package/dist/esm/utils/nextcloudDirectory.js +121 -0
  110. package/dist/esm/utils/nextcloudDirectory.js.map +1 -0
  111. package/dist/esm/utils/queryPresets.js +13 -2
  112. package/dist/esm/utils/queryPresets.js.map +1 -1
  113. package/dist/esm/utils/scopeListLayout.js +37 -1
  114. package/dist/esm/utils/scopeListLayout.js.map +1 -1
  115. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  116. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  117. package/dist/nextcloud-vue.cjs.js +14443 -10519
  118. package/dist/nextcloud-vue.cjs.js.map +2 -2
  119. package/dist/nextcloud-vue.css +725 -383
  120. package/l10n/en.json +21 -1
  121. package/l10n/nl.json +36 -1
  122. package/package.json +2 -2
  123. package/scripts/merge-l10n.js +196 -0
  124. package/src/components/CnAppRoot/CnAppRoot.md +1 -0
  125. package/src/components/CnAppRoot/CnAppRoot.vue +17 -0
  126. package/src/components/CnCellRenderer/CnCellRenderer.vue +57 -1
  127. package/src/components/CnChartWidget/CnChartWidget.md +2 -1
  128. package/src/components/CnChartWidget/CnChartWidget.vue +106 -2
  129. package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +1269 -0
  130. package/src/components/CnChecklistRunWidget/index.js +27 -0
  131. package/src/components/CnConversationThread/CnConversationThread.md +12 -0
  132. package/src/components/CnConversationThread/CnConversationThread.vue +30 -4
  133. package/src/components/CnConversationThread/CnConversationWidget.vue +167 -24
  134. package/src/components/CnDashboardPage/CnDashboardPage.vue +1 -0
  135. package/src/components/CnDataTable/CnDataTable.vue +26 -8
  136. package/src/components/CnDetailPage/CnDetailPage.md +48 -0
  137. package/src/components/CnDetailPage/CnDetailPage.vue +188 -5
  138. package/src/components/CnFactsWidget/CnFactsWidget.vue +114 -3
  139. package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +3 -3
  140. package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +38 -2
  141. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +508 -236
  142. package/src/components/CnFilesBrowser/filesBrowserColumns.js +8 -2
  143. package/src/components/CnIndexPage/CnIndexPage.md +28 -0
  144. package/src/components/CnIndexPage/CnIndexPage.vue +367 -27
  145. package/src/components/CnIndexPage/CnIndexPageWidgets.vue +13 -4
  146. package/src/components/CnIndexPage/useNamedSource.js +1 -1
  147. package/src/components/CnObjectSidebar/CnFilesTab.vue +17 -0
  148. package/src/components/CnOrderedListField/CnOrderedListField.vue +386 -0
  149. package/src/components/CnOrderedListField/index.js +7 -0
  150. package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +139 -10
  151. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  152. package/src/components/index.js +2 -0
  153. package/src/composables/indexSources.js +419 -4
  154. package/src/composables/useOrganisationStore.js +111 -0
  155. package/src/index.js +2 -0
  156. package/src/schemas/app-manifest-v2.schema.json +42 -5
  157. package/src/utils/builtInFormatters.js +33 -0
  158. package/src/utils/libraryWidgetKeys.js +1 -0
  159. package/src/utils/matrixCells.js +154 -0
  160. package/src/utils/nextcloudDirectory.js +117 -0
  161. package/src/utils/queryPresets.js +13 -2
  162. package/src/utils/scopeListLayout.js +37 -1
  163. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -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,13 +1217,14 @@ 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'
1207
1224
  import { resolveClaimedTeams, resolveClaimTokens, splitViewsIntoTabs, viewAsTab, viewIdOf } from '../../utils/listLenses.js'
1208
1225
  import { LIST_SHORTCUTS, listPaletteCommands, shortcutFor } from '../../utils/listShortcuts.js'
1209
1226
  import { multiKeySort } from '../../utils/multiKeySort.js'
1227
+ import { fetchGroups } from '../../utils/nextcloudDirectory.js'
1210
1228
  import { OBJECTS_CHANGED_EVENT } from '../../utils/objectSignals.js'
1211
1229
  import { objectUrlOf, patchObject } from '../../utils/objectWrite.js'
1212
1230
  import { withPersonalLenses } from '../../utils/personalLenses.js'
@@ -1219,7 +1237,7 @@ import { isNewTabClick, isNewTabHandled, isRowMiddleClick, markNewTabHandled, pr
1219
1237
  import { DEFAULT_ROW_INDICATOR_CAP } from '../../utils/rowIndicators.js'
1220
1238
  import { buildRouteQueryFromViewState, buildViewCreatePayload, extractViewState, extractViewStateFromRouteQuery, normalizeSharedWith, savedViewScope, viewMatchesScope } from '../../utils/savedViewHelpers.js'
1221
1239
  import { columnsFromSchema, fieldsFromSchema } from '../../utils/schema.js'
1222
- import { resolveScopeLayout } from '../../utils/scopeListLayout.js'
1240
+ import { resolveScopeLayout, resolveScopePresentation } from '../../utils/scopeListLayout.js'
1223
1241
  import { dispatchObjectCreated } from '../../utils/walkthroughSignals.js'
1224
1242
  import { CnActionButtons } from '../CnActionButtons/index.js'
1225
1243
  import { CnActionsBar } from '../CnActionsBar/index.js'
@@ -1490,6 +1508,7 @@ export default {
1490
1508
 
1491
1509
  components: {
1492
1510
  CnIndexPageWidgets,
1511
+ NcNoteCard,
1493
1512
  CnBuildiqEditButton,
1494
1513
  CnFollowToggle,
1495
1514
  NcLoadingIcon,
@@ -1658,6 +1677,35 @@ export default {
1658
1677
  default: '',
1659
1678
  },
1660
1679
 
1680
+ /**
1681
+ * Tabs to sibling pages above the toolbar (manifest `config.pageTabs`),
1682
+ * each `{ id?, label, route?, params?, query?, href?, countFrom?: {
1683
+ * register, schema, filter? } }`: DqPrivacy's "AVG-verzoeken |
1684
+ * Datalekken". The current page's tab is marked; `countFrom` adds the
1685
+ * count of that list. Wins over a named source's own page tabs.
1686
+ *
1687
+ * @type {Array<object>|null}
1688
+ * @spec openspec/changes/index-page-tabs-notice/specs/index-page/spec.md#requirement-an-index-page-can-link-its-sibling-pages-as-tabs
1689
+ */
1690
+ pageTabs: {
1691
+ type: Array,
1692
+ default: null,
1693
+ },
1694
+
1695
+ /**
1696
+ * A notice under the list (manifest `config.notice`): `{ text,
1697
+ * variant?: 'info' | 'success' | 'warning' }`, a band that explains
1698
+ * the list, such as who decides what. The text goes through the host
1699
+ * translate.
1700
+ *
1701
+ * @type {{text: string, variant?: string}|null}
1702
+ * @spec openspec/changes/index-page-tabs-notice/specs/index-page/spec.md#requirement-an-index-page-can-show-a-notice-under-its-list
1703
+ */
1704
+ notice: {
1705
+ type: Object,
1706
+ default: null,
1707
+ },
1708
+
1661
1709
  /** Optional description shown below the title */
1662
1710
  description: {
1663
1711
  type: String,
@@ -3045,6 +3093,33 @@ export default {
3045
3093
  default: () => [],
3046
3094
  },
3047
3095
 
3096
+ /**
3097
+ * Cards after the list (manifest `config.afterList`): dashboard widget
3098
+ * definitions `{ id, type, title?, content, headerLink? }` stacked
3099
+ * under the list in the listed order, the counterpart of `statRow`
3100
+ * above it. A query preset or a scope can set its own.
3101
+ *
3102
+ * @spec openspec/changes/index-scope-presentation/specs/index-page/spec.md#requirement-an-index-page-can-draw-widgets-after-its-list
3103
+ * @type {Array<object>}
3104
+ */
3105
+ afterList: {
3106
+ type: Array,
3107
+ default: () => [],
3108
+ },
3109
+
3110
+ /**
3111
+ * The route query pairs that chose this page's query preset, set by
3112
+ * CnPageRenderer. They scope the list, so they show no removable
3113
+ * active-filter chip.
3114
+ *
3115
+ * @spec openspec/changes/index-scope-presentation/specs/index-page/spec.md#requirement-the-query-that-chose-a-preset-is-not-a-removable-filter
3116
+ * @type {object|null}
3117
+ */
3118
+ fixedFilters: {
3119
+ type: Object,
3120
+ default: null,
3121
+ },
3122
+
3048
3123
  /**
3049
3124
  * Where a row carries the actions the server says this caller may run
3050
3125
  * on it: a dotted path, read off the rows the list already fetched, so
@@ -3645,6 +3720,8 @@ export default {
3645
3720
  tabCounts: null,
3646
3721
  // The counts of the `filterStrip` pills, by pill key ('all', 'v0', ...).
3647
3722
  filterStripCounts: null,
3723
+ /** The `countFrom` counts of the manifest page tabs, by index. */
3724
+ pageTabCounts: {},
3648
3725
  /** The tabs `quickFiltersFrom` produced, or null before they load. */
3649
3726
  generatedQuickFilters: null,
3650
3727
  /** Count per saved-view id, for the views control; null = none. */
@@ -3673,6 +3750,8 @@ export default {
3673
3750
  */
3674
3751
  folderSidebarAllValues: [],
3675
3752
  folderRegisterList: [],
3753
+ /** The Nextcloud groups a `folderSidebar.source: 'groups'` pane lists. */
3754
+ folderGroupList: [],
3676
3755
  // `x-index` blocks read off the schema rows a `source: 'register'`
3677
3756
  // folder list was built from, keyed by folder id. The layout a case
3678
3757
  // type carries travels with the record rather than being restated
@@ -4194,7 +4273,8 @@ export default {
4194
4273
  }
4195
4274
  // The index boards draw all four segments, so the board look does
4196
4275
  // too unless the manifest names its own list.
4197
- const listed = Array.isArray(this.viewSwitch) ? this.viewSwitch : ['table', 'cards', 'board', 'map']
4276
+ const viewSwitch = this.presented('viewSwitch')
4277
+ const listed = Array.isArray(viewSwitch) ? viewSwitch : ['table', 'cards', 'board', 'map']
4198
4278
  const offered = [...this.effectiveToggleModes, ...(this.showMapSegment ? ['map'] : [])]
4199
4279
  return listed.filter((mode) => ['table', 'cards', 'board', 'map'].includes(mode) && !offered.includes(mode))
4200
4280
  },
@@ -4589,7 +4669,7 @@ export default {
4589
4669
  */
4590
4670
  folderSidebarSource() {
4591
4671
  const s = this.folderSidebar && this.folderSidebar.source
4592
- return (s === 'register' || !s) ? 'custom' : s
4672
+ return (s === 'register' || s === 'groups' || !s) ? 'custom' : s
4593
4673
  },
4594
4674
 
4595
4675
  /**
@@ -4606,6 +4686,9 @@ export default {
4606
4686
  if (this.folderSidebar.source === 'register') {
4607
4687
  return this.folderRegisterList
4608
4688
  }
4689
+ if (this.folderSidebar.source === 'groups') {
4690
+ return this.folderGroupList
4691
+ }
4609
4692
  return this.folderSidebar.folders || []
4610
4693
  },
4611
4694
 
@@ -4665,6 +4748,58 @@ export default {
4665
4748
  })
4666
4749
  },
4667
4750
 
4751
+ /**
4752
+ * This page's own value of each presentation key, which a query preset
4753
+ * may have set: what `presented()` falls back to without a scope.
4754
+ *
4755
+ * @return {object}
4756
+ * @spec openspec/changes/index-scope-presentation/specs/index-page/spec.md#requirement-a-preset-or-a-scope-sets-the-list-s-presentation
4757
+ */
4758
+ ownPresentation() {
4759
+ return {
4760
+ title: this.title,
4761
+ quickFilters: this.quickFilters,
4762
+ cardFields: this.cardFields,
4763
+ countText: this.countText,
4764
+ searchPlaceholder: this.searchPlaceholder,
4765
+ footerNote: this.footerNote,
4766
+ viewSwitch: this.viewSwitch,
4767
+ addLabel: this.addLabel,
4768
+ headerButtons: this.headerButtons,
4769
+ breadcrumb: this.breadcrumb,
4770
+ headerMeta: this.headerMeta,
4771
+ afterList: this.afterList,
4772
+ }
4773
+ },
4774
+
4775
+ /**
4776
+ * The presentation the active scope declares (`title`, `countText`,
4777
+ * `quickFilters`, `addLabel`, `afterList` and the other query preset
4778
+ * keys), from its folder entry or its row's `x-index`. Empty without a
4779
+ * scope, so the page's own values stand.
4780
+ *
4781
+ * @return {object}
4782
+ * @spec openspec/changes/index-scope-presentation/specs/index-page/spec.md#requirement-a-preset-or-a-scope-sets-the-list-s-presentation
4783
+ */
4784
+ activeScopePresentation() {
4785
+ if (!this.folderSidebar) {
4786
+ return {}
4787
+ }
4788
+ return resolveScopePresentation({ scope: this.activeScope, row: this.activeScopeRow })
4789
+ },
4790
+
4791
+ /**
4792
+ * The widgets drawn after the list: the active scope's `afterList`,
4793
+ * else the page's.
4794
+ *
4795
+ * @return {Array<object>}
4796
+ * @spec openspec/changes/index-scope-presentation/specs/index-page/spec.md#requirement-an-index-page-can-draw-widgets-after-its-list
4797
+ */
4798
+ presentedAfterList() {
4799
+ const list = this.presented('afterList')
4800
+ return Array.isArray(list) ? list : []
4801
+ },
4802
+
4668
4803
  /**
4669
4804
  * The fields the search box searches over while a scope declaring
4670
4805
  * `searchFields` is active. Read by `useSelfFetchList`'s fixed-filter
@@ -5124,8 +5259,9 @@ export default {
5124
5259
  * @return {?Array<string>}
5125
5260
  */
5126
5261
  resolvedCardFields() {
5127
- if (Array.isArray(this.cardFields) && this.cardFields.length > 0) {
5128
- return this.cardFields
5262
+ const cardFields = this.presented('cardFields')
5263
+ if (Array.isArray(cardFields) && cardFields.length > 0) {
5264
+ return cardFields
5129
5265
  }
5130
5266
  if (!this.isBoardLook) {
5131
5267
  return null
@@ -5164,6 +5300,10 @@ export default {
5164
5300
  }
5165
5301
  })
5166
5302
  }
5303
+ // An `actions` column: the page's row actions it names, as buttons.
5304
+ cols = cols.map((col) => (col && typeof col === 'object' && col.widget === 'actions'
5305
+ ? { ...col, sortable: false, widgetProps: { ...(col.widgetProps || {}), items: (row) => this.actionColumnItems(col, row), run: this.runColumnAction } }
5306
+ : col))
5167
5307
  // The Recent lens owns the order: no column sorts while it is on.
5168
5308
  if (this.recentLensActive) {
5169
5309
  cols = cols.map((col) => (col && typeof col === 'object' ? { ...col, sortable: false } : col))
@@ -5314,7 +5454,7 @@ export default {
5314
5454
  * @return {string} The id field key.
5315
5455
  */
5316
5456
  folderPassthroughIdField() {
5317
- if (this.folderSidebar && this.folderSidebar.source === 'register') {
5457
+ if (this.folderSidebar && (this.folderSidebar.source === 'register' || this.folderSidebar.source === 'groups')) {
5318
5458
  return 'id'
5319
5459
  }
5320
5460
  return (this.folderSidebar && this.folderSidebar.idField) || 'id'
@@ -5324,7 +5464,7 @@ export default {
5324
5464
  * @return {string} The name field key for CnFolderSidebar's custom list.
5325
5465
  */
5326
5466
  folderPassthroughNameField() {
5327
- if (this.folderSidebar && this.folderSidebar.source === 'register') {
5467
+ if (this.folderSidebar && (this.folderSidebar.source === 'register' || this.folderSidebar.source === 'groups')) {
5328
5468
  return 'name'
5329
5469
  }
5330
5470
  return (this.folderSidebar && this.folderSidebar.nameField) || 'name'
@@ -6040,6 +6180,51 @@ export default {
6040
6180
  return (source.pageTabs() || []).filter((tab) => tab && tab.to && tab.label)
6041
6181
  },
6042
6182
 
6183
+ /**
6184
+ * The page tabs to draw: the manifest's `pageTabs`, each with its
6185
+ * `countFrom` count once known, else a named source's own.
6186
+ *
6187
+ * @return {Array<object>} The tabs, `{ id, label, to, count? }`.
6188
+ * @spec openspec/changes/index-page-tabs-notice/specs/index-page/spec.md#requirement-an-index-page-can-link-its-sibling-pages-as-tabs
6189
+ */
6190
+ /**
6191
+ * The notice band's NcNoteCard type: its `variant`, else info.
6192
+ *
6193
+ * @return {string}
6194
+ * @spec openspec/changes/index-page-tabs-notice/specs/index-page/spec.md#requirement-an-index-page-can-show-a-notice-under-its-list
6195
+ */
6196
+ noticeType() {
6197
+ const variant = this.notice && this.notice.variant
6198
+ return ['success', 'warning', 'error'].includes(variant) ? variant : 'info'
6199
+ },
6200
+
6201
+ effectivePageTabs() {
6202
+ if (!Array.isArray(this.pageTabs) || this.pageTabs.length === 0) {
6203
+ return this.sourcePageTabs
6204
+ }
6205
+ return this.pageTabs
6206
+ .filter((tab) => tab && typeof tab.label === 'string' && tab.label !== '' && (tab.route || tab.to || tab.href))
6207
+ .map((tab, index) => {
6208
+ const to = tab.route
6209
+ ? { name: tab.route, ...(tab.params ? { params: tab.params } : {}), ...(tab.query ? { query: tab.query } : {}) }
6210
+ : (tab.to || tab.href)
6211
+ const count = this.pageTabCounts[index]
6212
+ return { id: tab.id || `page-tab-${index}`, label: this.cnTranslate(tab.label), to, ...(typeof count === 'number' ? { count } : {}) }
6213
+ })
6214
+ },
6215
+
6216
+ /**
6217
+ * What the page tab counts depend on, as one key.
6218
+ *
6219
+ * @return {string}
6220
+ * @spec openspec/changes/index-page-tabs-notice/specs/index-page/spec.md#requirement-an-index-page-can-link-its-sibling-pages-as-tabs
6221
+ */
6222
+ pageTabCountKey() {
6223
+ const tabs = Array.isArray(this.pageTabs) ? this.pageTabs : []
6224
+ const sources = tabs.map((tab) => (tab && tab.countFrom && tab.countFrom.register && tab.countFrom.schema ? tab.countFrom : null))
6225
+ return sources.some(Boolean) ? JSON.stringify(sources) : ''
6226
+ },
6227
+
6043
6228
  /**
6044
6229
  * The numbers on the quick filters: a named source counts its own
6045
6230
  * tabs (`tabCounts()`), a self-fetching page asks OpenRegister.
@@ -6061,11 +6246,15 @@ export default {
6061
6246
  * @return {string} The note, translated, or ''.
6062
6247
  */
6063
6248
  effectiveFooterNote() {
6064
- if (this.footerNote) {
6065
- return this.cnTranslate(this.footerNote)
6249
+ // The scope's note, else the page's (a preset may have set it),
6250
+ // else the named source's.
6251
+ const scoped = this.activeScopePresentation ? this.activeScopePresentation.footerNote : undefined
6252
+ const footerNote = scoped !== undefined ? scoped : this.footerNote
6253
+ if (footerNote) {
6254
+ return this.cnTranslate(footerNote)
6066
6255
  }
6067
6256
  const source = this.isNamedSource ? this.namedSource : null
6068
- return (source && source.footerNote) || this.footerNote
6257
+ return (source && source.footerNote) || footerNote
6069
6258
  },
6070
6259
 
6071
6260
  /**
@@ -6076,7 +6265,7 @@ export default {
6076
6265
  * @return {Array<object>|null} The tabs, or null when there are none.
6077
6266
  */
6078
6267
  effectiveQuickFilters() {
6079
- const own = withPersonalLenses(this.quickFilters || this.generatedQuickFilters, this.personalLenses)
6268
+ const own = withPersonalLenses(this.presented('quickFilters') || this.generatedQuickFilters, this.personalLenses)
6080
6269
  if (own && own.length > 0) {
6081
6270
  return own
6082
6271
  }
@@ -6142,7 +6331,8 @@ export default {
6142
6331
  * @return {Array<{key: string, label: string, action: string, variant: string, icon: string, format: string}>}
6143
6332
  */
6144
6333
  resolvedHeaderButtons() {
6145
- const declared = Array.isArray(this.headerButtons) ? this.headerButtons : []
6334
+ const headerButtons = this.presented('headerButtons')
6335
+ const declared = Array.isArray(headerButtons) ? headerButtons : []
6146
6336
  return declared
6147
6337
  .filter((button) => button && typeof button === 'object' && typeof button.action === 'string' && button.action !== '')
6148
6338
  .map((button, index) => {
@@ -6267,6 +6457,7 @@ export default {
6267
6457
  for (const [key, raw] of Object.entries(this.effectiveActiveFilters || {})) {
6268
6458
  const values = (Array.isArray(raw) ? raw : [raw])
6269
6459
  .filter((value) => value !== null && value !== undefined && value !== '')
6460
+ .filter((value) => !this.isFixedFilter(key, value))
6270
6461
  .map((value) => this.activeFilterValueLabel(key, value))
6271
6462
  if (values.length === 0) {
6272
6463
  continue
@@ -6450,11 +6641,12 @@ export default {
6450
6641
  * @return {string}
6451
6642
  */
6452
6643
  headerMetaText() {
6453
- if (!this.headerMeta) {
6644
+ const headerMeta = this.presented('headerMeta')
6645
+ if (!headerMeta) {
6454
6646
  return ''
6455
6647
  }
6456
6648
  const total = this.effectivePagination?.total
6457
- return this.cnTranslate(this.headerMeta)
6649
+ return this.cnTranslate(headerMeta)
6458
6650
  .replace('{shown}', String(this.effectiveObjects.length))
6459
6651
  .replace('{total}', typeof total === 'number' ? String(total) : '')
6460
6652
  },
@@ -6468,11 +6660,12 @@ export default {
6468
6660
  // The default count line is the library's own string, so it
6469
6661
  // reads "14 van 14" in Dutch; a page's own template goes
6470
6662
  // through the app's label lookup.
6471
- if (!this.countText && !this.countSubtitle) {
6663
+ const countText = this.presented('countText')
6664
+ if (!countText && !this.countSubtitle) {
6472
6665
  return t('nextcloud-vue', '{shown} of {total}', { shown, total })
6473
6666
  }
6474
- if (this.countText) {
6475
- return this.cnTranslate(this.countText)
6667
+ if (countText) {
6668
+ return this.cnTranslate(countText)
6476
6669
  .replace('{shown}', String(shown))
6477
6670
  .replace('{total}', String(total))
6478
6671
  }
@@ -6486,8 +6679,10 @@ export default {
6486
6679
 
6487
6680
  /** Add button label — derived from schema.title if not explicitly set */
6488
6681
  resolvedAddLabel() {
6489
- if (this.addLabel) {
6490
- return this.cnTranslate(this.addLabel)
6682
+ const scoped = this.activeScopePresentation ? this.activeScopePresentation.addLabel : undefined
6683
+ const addLabel = scoped !== undefined ? scoped : this.addLabel
6684
+ if (addLabel) {
6685
+ return this.cnTranslate(addLabel)
6491
6686
  }
6492
6687
  // A named source names its own create action. Without this the button
6493
6688
  // falls back to a schema-derived noun, and a named source has no
@@ -6712,6 +6907,14 @@ export default {
6712
6907
  },
6713
6908
  },
6714
6909
 
6910
+ pageTabCountKey: {
6911
+ immediate: true,
6912
+ /** @spec openspec/changes/index-page-tabs-notice/specs/index-page/spec.md#requirement-an-index-page-can-link-its-sibling-pages-as-tabs */
6913
+ handler() {
6914
+ this.loadPageTabCounts()
6915
+ },
6916
+ },
6917
+
6715
6918
  filterStripCountKey: {
6716
6919
  immediate: true,
6717
6920
  /** @spec openspec/changes/screens-filter-strip/specs/index-filter-strip/spec.md#requirement-an-index-page-can-draw-a-second-filter-strip */
@@ -6916,6 +7119,7 @@ export default {
6916
7119
  this.maybeOpenCreateFromQuery()
6917
7120
  if (this.folderSidebar) {
6918
7121
  this.loadFolderRegister()
7122
+ this.loadFolderGroups()
6919
7123
  // Reflect a deep-link filter (e.g. ?caseType=<id>) as the active folder.
6920
7124
  const key = this.folderSidebar.filterField || this.folderSidebar.field
6921
7125
  const active = key && this.effectiveActiveFilters[key]
@@ -6979,6 +7183,36 @@ export default {
6979
7183
  },
6980
7184
 
6981
7185
  methods: {
7186
+ /**
7187
+ * A presentation value: the active scope's when it declares the key,
7188
+ * else this page's own prop (which a query preset may have set).
7189
+ *
7190
+ * @param {string} key One of the query preset keys.
7191
+ * @return {unknown} The value.
7192
+ * @spec openspec/changes/index-scope-presentation/specs/index-page/spec.md#requirement-a-preset-or-a-scope-sets-the-list-s-presentation
7193
+ */
7194
+ presented(key) {
7195
+ const scoped = this.activeScopePresentation
7196
+ return scoped[key] !== undefined ? scoped[key] : this.ownPresentation[key]
7197
+ },
7198
+
7199
+ /**
7200
+ * Whether a filter value is one of the query pairs that chose the
7201
+ * preset, so it shows no removable chip.
7202
+ *
7203
+ * @param {string} key The filter key.
7204
+ * @param {unknown} value One filter value.
7205
+ * @return {boolean}
7206
+ * @spec openspec/changes/index-scope-presentation/specs/index-page/spec.md#requirement-the-query-that-chose-a-preset-is-not-a-removable-filter
7207
+ */
7208
+ isFixedFilter(key, value) {
7209
+ const fixed = this.fixedFilters
7210
+ if (!fixed || !Object.hasOwn(fixed, key)) {
7211
+ return false
7212
+ }
7213
+ return String(fixed[key]) === String(value)
7214
+ },
7215
+
6982
7216
  /**
6983
7217
  * The manifest page that shows one object of a schema, for a linked
6984
7218
  * label. Null without a manifest or a matching detail page.
@@ -7092,6 +7326,32 @@ export default {
7092
7326
  ]
7093
7327
  },
7094
7328
 
7329
+ /**
7330
+ * Count the lists behind the manifest page tabs (`countFrom`), one list
7331
+ * request each. A failed count shows no number.
7332
+ *
7333
+ * @return {Promise<void>}
7334
+ * @spec openspec/changes/index-page-tabs-notice/specs/index-page/spec.md#requirement-an-index-page-can-link-its-sibling-pages-as-tabs
7335
+ */
7336
+ async loadPageTabCounts() {
7337
+ const key = this.pageTabCountKey
7338
+ if (key === '') {
7339
+ this.pageTabCounts = {}
7340
+ return
7341
+ }
7342
+ const ctx = typeof this.selfFetchTokenCtx === 'function' ? this.selfFetchTokenCtx() : {}
7343
+ const counts = {}
7344
+ await Promise.all(this.pageTabs.map(async (tab, index) => {
7345
+ const from = tab && tab.countFrom
7346
+ if (from && from.register && from.schema) {
7347
+ counts[index] = await fetchListTotal(from.register, from.schema, from.filter || {}, ctx).catch(() => null)
7348
+ }
7349
+ }))
7350
+ if (key === this.pageTabCountKey) {
7351
+ this.pageTabCounts = counts
7352
+ }
7353
+ },
7354
+
7095
7355
  /**
7096
7356
  * Read the counts of the `filterStrip` pills in one grouped request,
7097
7357
  * narrowed by the page filter and the active quick filter. A failed
@@ -8525,6 +8785,50 @@ export default {
8525
8785
  this.$emit('quick-edit-save', { id, patch })
8526
8786
  },
8527
8787
 
8788
+ /**
8789
+ * The buttons an `actions` column shows on a row: the row actions its
8790
+ * `widgetProps.actions` names (by id, else by label), in that order,
8791
+ * that are visible for the row.
8792
+ *
8793
+ * @param {object} col The column.
8794
+ * @param {object} row The row.
8795
+ * @return {Array<{key: string, label: string, variant: string, disabled: boolean}>}
8796
+ * @spec openspec/changes/table-actions-column/specs/index-page/spec.md#requirement-a-column-can-show-row-actions-as-buttons
8797
+ */
8798
+ actionColumnItems(col, row) {
8799
+ const wanted = Array.isArray(col.widgetProps && col.widgetProps.actions) ? col.widgetProps.actions : []
8800
+ const available = this.rowActionsFor(row)
8801
+ return wanted
8802
+ .map((ref) => available.find((action) => action.id === ref) || available.find((action) => action.label === ref))
8803
+ .filter((action) => action && isRowActionVisible(action, row))
8804
+ .map((action, index) => ({
8805
+ key: action.id || action.label,
8806
+ label: this.cnTranslate(action.label),
8807
+ variant: action.variant || (index === 0 ? 'secondary' : 'tertiary'),
8808
+ disabled: typeof action.disabled === 'function' ? !!action.disabled(row) : action.disabled === true,
8809
+ }))
8810
+ },
8811
+
8812
+ /**
8813
+ * Run one `actions` column button: its handler, then the same `action`
8814
+ * event a row menu entry emits.
8815
+ *
8816
+ * @param {string} key The action's id, else its label.
8817
+ * @param {object} row The row.
8818
+ * @return {void}
8819
+ * @spec openspec/changes/table-actions-column/specs/index-page/spec.md#requirement-a-column-can-show-row-actions-as-buttons
8820
+ */
8821
+ runColumnAction(key, row) {
8822
+ const action = this.rowActionsFor(row).find((entry) => (entry.id || entry.label) === key)
8823
+ if (!action) {
8824
+ return
8825
+ }
8826
+ if (typeof action.handler === 'function') {
8827
+ action.handler(row)
8828
+ }
8829
+ this.onRowAction(rowActionPayload(action, row))
8830
+ },
8831
+
8528
8832
  /**
8529
8833
  * Take the server's version of a row after a conflict. Nothing of this
8530
8834
  * person's edit is written, and the row in the list is refreshed to
@@ -8688,6 +8992,39 @@ export default {
8688
8992
  }
8689
8993
  },
8690
8994
 
8995
+ /**
8996
+ * Load the Nextcloud groups for `folderSidebar.source: 'groups'`, each
8997
+ * with its member count. `folderSidebar.groups` (`[{ id, parent? }]`)
8998
+ * picks and orders them and nests a group under its `parent`; without
8999
+ * it every group shows, by name. A refused request lists none.
9000
+ *
9001
+ * @return {Promise<void>}
9002
+ * @spec openspec/changes/index-groups-users-source/specs/index-page/spec.md#requirement-an-index-page-can-list-nextcloud-groups-and-their-members
9003
+ */
9004
+ async loadFolderGroups() {
9005
+ const cfg = this.folderSidebar
9006
+ if (!cfg || cfg.source !== 'groups') {
9007
+ return
9008
+ }
9009
+ try {
9010
+ const { default: axios } = await import('@nextcloud/axios')
9011
+ const groups = await fetchGroups(axios, { search: cfg.search || '', limit: cfg.limit || 200 })
9012
+ const declared = Array.isArray(cfg.groups) ? cfg.groups.filter((entry) => entry && entry.id) : null
9013
+ if (!declared) {
9014
+ this.folderGroupList = [...groups].sort((a, b) => a.name.localeCompare(b.name))
9015
+ return
9016
+ }
9017
+ const byId = new Map(groups.map((group) => [group.id, group]))
9018
+ this.folderGroupList = declared
9019
+ .filter((entry) => byId.has(String(entry.id)))
9020
+ .map((entry) => ({ ...byId.get(String(entry.id)), parentId: entry.parent ? String(entry.parent) : null }))
9021
+ } catch (e) {
9022
+ // eslint-disable-next-line no-console
9023
+ console.error('[CnIndexPage] failed to load the Nextcloud groups', e)
9024
+ this.folderGroupList = []
9025
+ }
9026
+ },
9027
+
8691
9028
  /**
8692
9029
  * Load the folder list from an OpenRegister register/schema for a
8693
9030
  * `folderSidebar.source === 'register'` config. Maps each object to
@@ -9377,6 +9714,9 @@ export default {
9377
9714
  * @return {boolean} True for the current page.
9378
9715
  */
9379
9716
  isActivePageTab(tab) {
9717
+ if (tab.to && typeof tab.to === 'object') {
9718
+ return !!this.$route && tab.to.name !== undefined && this.$route.name === tab.to.name
9719
+ }
9380
9720
  const path = this.$route && this.$route.path
9381
9721
  return !!path && String(path).replace(/\/+$/, '') === String(tab.to).replace(/\/+$/, '')
9382
9722
  },