@conduction/nextcloud-vue 0.1.0-beta.3 → 0.1.0-beta.4

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 (152) hide show
  1. package/README.md +226 -226
  2. package/dist/nextcloud-vue.cjs.js +60455 -8755
  3. package/dist/nextcloud-vue.cjs.js.map +1 -1
  4. package/dist/nextcloud-vue.css +2062 -528
  5. package/dist/nextcloud-vue.esm.js +60411 -8731
  6. package/dist/nextcloud-vue.esm.js.map +1 -1
  7. package/package.json +75 -62
  8. package/src/components/CnActionsBar/CnActionsBar.vue +235 -225
  9. package/src/components/CnActionsBar/index.js +1 -1
  10. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +579 -0
  11. package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -0
  12. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -0
  13. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +418 -0
  14. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -0
  15. package/src/components/CnAdvancedFormDialog/index.js +1 -0
  16. package/src/components/CnCardGrid/CnCardGrid.vue +152 -152
  17. package/src/components/CnCardGrid/index.js +1 -1
  18. package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
  19. package/src/components/CnCellRenderer/index.js +1 -1
  20. package/src/components/CnChartWidget/CnChartWidget.vue +320 -0
  21. package/src/components/CnChartWidget/index.js +1 -0
  22. package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -77
  23. package/src/components/CnConfigurationCard/index.js +1 -1
  24. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +225 -0
  25. package/src/components/CnDashboardGrid/index.js +1 -0
  26. package/src/components/CnDashboardPage/CnDashboardPage.vue +390 -0
  27. package/src/components/CnDashboardPage/index.js +1 -0
  28. package/src/components/CnDataTable/CnDataTable.vue +349 -349
  29. package/src/components/CnDataTable/index.js +1 -1
  30. package/src/components/CnDetailCard/CnDetailCard.vue +214 -0
  31. package/src/components/CnDetailCard/index.js +1 -0
  32. package/src/components/CnDetailPage/CnDetailPage.vue +281 -0
  33. package/src/components/CnDetailPage/index.js +1 -0
  34. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +231 -223
  35. package/src/components/CnFacetSidebar/index.js +1 -1
  36. package/src/components/CnFilterBar/CnFilterBar.vue +152 -152
  37. package/src/components/CnFilterBar/index.js +1 -1
  38. package/src/components/CnIcon/CnIcon.vue +89 -89
  39. package/src/components/CnIcon/index.js +1 -1
  40. package/src/components/CnIndexPage/CnIndexPage.vue +874 -816
  41. package/src/components/CnIndexPage/index.js +1 -1
  42. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +503 -484
  43. package/src/components/CnIndexSidebar/index.js +1 -1
  44. package/src/components/CnItemCard/CnItemCard.vue +132 -0
  45. package/src/components/CnItemCard/index.js +1 -0
  46. package/src/components/CnKpiGrid/CnKpiGrid.vue +89 -89
  47. package/src/components/CnKpiGrid/index.js +1 -1
  48. package/src/components/CnMassActionBar/CnMassActionBar.vue +160 -160
  49. package/src/components/CnMassActionBar/index.js +1 -1
  50. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +320 -320
  51. package/src/components/CnMassCopyDialog/index.js +1 -1
  52. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +238 -238
  53. package/src/components/CnMassDeleteDialog/index.js +1 -1
  54. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +190 -190
  55. package/src/components/CnMassExportDialog/index.js +1 -1
  56. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +491 -491
  57. package/src/components/CnMassImportDialog/index.js +1 -1
  58. package/src/components/CnNoteCard/CnNoteCard.vue +149 -0
  59. package/src/components/CnNoteCard/index.js +1 -0
  60. package/src/components/CnNotesCard/CnNotesCard.vue +413 -0
  61. package/src/components/CnNotesCard/index.js +1 -0
  62. package/src/components/CnObjectCard/CnObjectCard.vue +292 -292
  63. package/src/components/CnObjectCard/index.js +1 -1
  64. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +876 -0
  65. package/src/components/CnObjectSidebar/index.js +1 -0
  66. package/src/components/CnPageHeader/CnPageHeader.vue +57 -57
  67. package/src/components/CnPageHeader/index.js +1 -1
  68. package/src/components/CnPagination/CnPagination.vue +252 -252
  69. package/src/components/CnPagination/index.js +1 -1
  70. package/src/components/CnRowActions/CnRowActions.vue +73 -73
  71. package/src/components/CnRowActions/index.js +1 -1
  72. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -0
  73. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -0
  74. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -0
  75. package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -0
  76. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -0
  77. package/src/components/CnSchemaFormDialog/index.js +1 -0
  78. package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
  79. package/src/components/CnSettingsCard/index.js +1 -1
  80. package/src/components/CnSettingsSection/CnSettingsSection.vue +266 -266
  81. package/src/components/CnSettingsSection/index.js +1 -1
  82. package/src/components/CnStatsBlock/CnStatsBlock.vue +420 -366
  83. package/src/components/CnStatsBlock/index.js +1 -1
  84. package/src/components/CnStatusBadge/CnStatusBadge.vue +77 -77
  85. package/src/components/CnStatusBadge/index.js +1 -1
  86. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +540 -0
  87. package/src/components/CnTabbedFormDialog/index.js +1 -0
  88. package/src/components/CnTasksCard/CnTasksCard.vue +373 -0
  89. package/src/components/CnTasksCard/index.js +1 -0
  90. package/src/components/CnTileWidget/CnTileWidget.vue +159 -0
  91. package/src/components/CnTileWidget/index.js +1 -0
  92. package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -0
  93. package/src/components/CnTimelineStages/index.js +1 -0
  94. package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -0
  95. package/src/components/CnUserActionMenu/index.js +1 -0
  96. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +312 -312
  97. package/src/components/CnVersionInfoCard/index.js +1 -1
  98. package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -0
  99. package/src/components/CnWidgetRenderer/index.js +1 -0
  100. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +211 -0
  101. package/src/components/CnWidgetWrapper/index.js +1 -0
  102. package/src/components/index.js +43 -29
  103. package/src/composables/index.js +4 -3
  104. package/src/composables/useDashboardView.js +240 -0
  105. package/src/composables/useDetailView.js +289 -132
  106. package/src/composables/useListView.js +363 -362
  107. package/src/composables/useSubResource.js +142 -142
  108. package/src/constants/metadata.js +30 -30
  109. package/src/css/CnSchemaFormDialog.css +546 -0
  110. package/src/css/__sample_nextcloud_tokens.css +110 -0
  111. package/src/css/actions-bar.css +48 -48
  112. package/src/css/badge.css +51 -51
  113. package/src/css/card.css +128 -128
  114. package/src/css/dashboard.css +70 -0
  115. package/src/css/detail-page.css +168 -0
  116. package/src/css/detail.css +68 -68
  117. package/src/css/index-page.css +44 -32
  118. package/src/css/index-sidebar.css +193 -187
  119. package/src/css/index.css +16 -12
  120. package/src/css/layout.css +90 -90
  121. package/src/css/page-header.css +33 -33
  122. package/src/css/pagination.css +72 -72
  123. package/src/css/table.css +142 -142
  124. package/src/css/timeline-stages.css +218 -0
  125. package/src/css/utilities.css +46 -46
  126. package/src/index.js +72 -53
  127. package/src/store/createSubResourcePlugin.js +135 -135
  128. package/src/store/index.js +3 -3
  129. package/src/store/plugins/auditTrails.js +17 -17
  130. package/src/store/plugins/files.js +250 -186
  131. package/src/store/plugins/index.js +7 -5
  132. package/src/store/plugins/lifecycle.js +180 -180
  133. package/src/store/plugins/relations.js +68 -68
  134. package/src/store/plugins/search.js +372 -0
  135. package/src/store/plugins/selection.js +104 -0
  136. package/src/store/useObjectStore.js +829 -686
  137. package/src/types/auditTrail.d.ts +32 -32
  138. package/src/types/file.d.ts +23 -23
  139. package/src/types/index.d.ts +35 -35
  140. package/src/types/notification.d.ts +36 -36
  141. package/src/types/object.d.ts +40 -40
  142. package/src/types/organisation.d.ts +41 -41
  143. package/src/types/register.d.ts +25 -25
  144. package/src/types/schema.d.ts +39 -39
  145. package/src/types/shared.d.ts +79 -79
  146. package/src/types/source.d.ts +14 -14
  147. package/src/types/task.d.ts +31 -31
  148. package/src/utils/errors.js +96 -96
  149. package/src/utils/headers.js +68 -50
  150. package/src/utils/id.js +13 -0
  151. package/src/utils/index.js +3 -3
  152. package/src/utils/schema.js +422 -419
@@ -1,484 +1,503 @@
1
- <template>
2
- <NcAppSidebar
3
- :name="resolvedName"
4
- :title="resolvedName"
5
- :subname="resolvedSubname"
6
- :open.sync="internalOpen"
7
- :active="internalActiveTab"
8
- :compact="!!resolvedIcon"
9
- @close="$emit('update:open', false)"
10
- @update:active="onTabChange">
11
- <!-- Schema icon in sidebar header -->
12
- <template v-if="resolvedIcon" #header>
13
- <div class="cn-index-sidebar__header-icon">
14
- <CnIcon :name="resolvedIcon" :size="32" />
15
- </div>
16
- </template>
17
-
18
- <!-- Search Tab -->
19
- <NcAppSidebarTab
20
- id="search-tab"
21
- :name="searchTabLabel"
22
- :order="1">
23
- <template #icon>
24
- <Magnify :size="20" />
25
- </template>
26
-
27
- <div class="cn-index-sidebar__section">
28
- <h3>{{ searchLabel }}</h3>
29
- <NcTextField
30
- :value="searchValue"
31
- :placeholder="searchPlaceholder"
32
- :label="searchLabel"
33
- @update:value="$emit('search', $event)" />
34
- </div>
35
-
36
- <div v-if="schemaFilters.length > 0" class="cn-index-sidebar__section">
37
- <h3>{{ filtersLabel }}</h3>
38
- <div
39
- v-for="filter in schemaFilters"
40
- :key="filter.key"
41
- class="cn-index-sidebar__filter-group">
42
- <div class="cn-index-sidebar__filter-header">
43
- <span class="cn-index-sidebar__filter-label">{{ filter.label }}</span>
44
- <NcPopover v-if="filter.description" popup-role="dialog">
45
- <template #trigger>
46
- <NcButton
47
- type="tertiary-no-background"
48
- :aria-label="filter.label + ' info'"
49
- class="cn-index-sidebar__info-btn">
50
- <template #icon>
51
- <InformationOutline :size="16" />
52
- </template>
53
- </NcButton>
54
- </template>
55
- <p class="cn-index-sidebar__filter-description">{{ filter.description }}</p>
56
- </NcPopover>
57
- </div>
58
- <NcSelect
59
- class="cn-index-sidebar__select"
60
- :value="getSelectedFilterOptions(filter)"
61
- :options="getFilterOptions(filter)"
62
- placeholder="Select..."
63
- :input-label="filter.label"
64
- :multiple="true"
65
- :clearable="true"
66
- @input="onFilterChange(filter.key, $event)" />
67
- </div>
68
- </div>
69
-
70
- <slot name="search-extra" />
71
- </NcAppSidebarTab>
72
-
73
- <!-- Columns Tab -->
74
- <NcAppSidebarTab
75
- id="columns-tab"
76
- :name="columnsTabLabel"
77
- :order="2">
78
- <template #icon>
79
- <FormatColumns :size="20" />
80
- </template>
81
-
82
- <div class="cn-sidebar-columns">
83
- <h3>{{ columnsHeading }}</h3>
84
- <p class="cn-sidebar-columns__description">{{ columnsDescription }}</p>
85
-
86
- <template v-if="allColumns.length > 0 || allGroups.length > 0">
87
- <!-- Schema properties group (collapsible) -->
88
- <div v-if="allColumns.length > 0" class="cn-sidebar-columns__group cn-sidebar-columns__group--collapsible">
89
- <div class="cn-sidebar-columns__group-header" @click="propertiesExpanded = !propertiesExpanded">
90
- <ChevronDown v-if="propertiesExpanded" :size="20" />
91
- <ChevronRight v-else :size="20" />
92
- <h4>{{ resolvedPropertiesLabel }}</h4>
93
- <NcCheckboxRadioSwitch
94
- :checked="isGroupAllVisible(allColumns)"
95
- class="cn-sidebar-columns__select-all"
96
- @click.native.stop
97
- @update:checked="toggleGroupAll(allColumns)">
98
- All
99
- </NcCheckboxRadioSwitch>
100
- </div>
101
- <div v-if="propertiesExpanded" class="cn-sidebar-columns__group-content">
102
- <NcCheckboxRadioSwitch
103
- v-for="col in allColumns"
104
- :key="col.key"
105
- :checked="isColumnVisible(col.key)"
106
- @update:checked="toggleColumn(col.key)">
107
- {{ col.label }}
108
- </NcCheckboxRadioSwitch>
109
- </div>
110
- </div>
111
-
112
- <!-- Extra column groups (built-in Metadata + external) -->
113
- <div
114
- v-for="group in allGroups"
115
- :key="group.id"
116
- class="cn-sidebar-columns__group cn-sidebar-columns__group--collapsible">
117
- <div class="cn-sidebar-columns__group-header" @click="toggleGroup(group.id)">
118
- <ChevronDown v-if="expandedGroups[group.id]" :size="20" />
119
- <ChevronRight v-else :size="20" />
120
- <h4>{{ group.label }}</h4>
121
- <NcCheckboxRadioSwitch
122
- :checked="isGroupAllVisible(group.columns)"
123
- class="cn-sidebar-columns__select-all"
124
- @click.native.stop
125
- @update:checked="toggleGroupAll(group.columns)">
126
- All
127
- </NcCheckboxRadioSwitch>
128
- </div>
129
- <div v-if="expandedGroups[group.id]" class="cn-sidebar-columns__group-content">
130
- <NcCheckboxRadioSwitch
131
- v-for="col in group.columns"
132
- :key="col.key"
133
- :checked="isColumnVisible(col.key)"
134
- @update:checked="toggleColumn(col.key)">
135
- {{ col.label }}
136
- </NcCheckboxRadioSwitch>
137
- </div>
138
- </div>
139
- </template>
140
-
141
- <p v-else class="cn-sidebar-columns__empty">
142
- No columns available. Provide a schema to auto-generate columns.
143
- </p>
144
- </div>
145
-
146
- <slot name="columns-extra" />
147
- </NcAppSidebarTab>
148
-
149
- <!-- Extra tabs injected by the consumer -->
150
- <slot name="tabs" />
151
- </NcAppSidebar>
152
- </template>
153
-
154
- <script>
155
- import { NcAppSidebar, NcAppSidebarTab, NcTextField, NcSelect, NcCheckboxRadioSwitch, NcPopover, NcButton } from '@nextcloud/vue'
156
- import Magnify from 'vue-material-design-icons/Magnify.vue'
157
- import FormatColumns from 'vue-material-design-icons/FormatColumns.vue'
158
- import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'
159
- import ChevronRight from 'vue-material-design-icons/ChevronRight.vue'
160
- import InformationOutline from 'vue-material-design-icons/InformationOutline.vue'
161
- import { CnIcon } from '../CnIcon/index.js'
162
- import { columnsFromSchema, filtersFromSchema } from '../../utils/schema.js'
163
- import { METADATA_COLUMNS } from '../../constants/metadata.js'
164
-
165
- /**
166
- * CnIndexSidebar — Reusable NcAppSidebar wrapper with Search + Columns tabs.
167
- *
168
- * Designed to be schema-driven: pass a schema and the sidebar auto-generates
169
- * search filters, column visibility controls, and the standard Metadata group.
170
- * Title and properties group label are derived from schema.title by default.
171
- *
172
- * Must be rendered at the App.vue level as a sibling of NcAppContent.
173
- * Use provide/inject to connect it to page components.
174
- *
175
- * @example
176
- * <!-- Minimal usage schema drives everything -->
177
- * <CnIndexSidebar
178
- * :schema="schema"
179
- * :visible-columns="visibleCols"
180
- * :search-value="search"
181
- * @search="onSearch"
182
- * @columns-change="onColumnsChange" />
183
- */
184
- export default {
185
- name: 'CnIndexSidebar',
186
-
187
- components: {
188
- NcAppSidebar,
189
- NcAppSidebarTab,
190
- NcTextField,
191
- NcSelect,
192
- NcCheckboxRadioSwitch,
193
- NcPopover,
194
- NcButton,
195
- CnIcon,
196
- Magnify,
197
- FormatColumns,
198
- ChevronDown,
199
- ChevronRight,
200
- InformationOutline,
201
- },
202
-
203
- props: {
204
- /** Sidebar title. Defaults to schema.title when not set. */
205
- title: {
206
- type: String,
207
- default: '',
208
- },
209
- /** MDI icon name or emoji. Defaults to schema.icon when not set. */
210
- icon: {
211
- type: String,
212
- default: '',
213
- },
214
- /** Schema object for auto-generating filters, columns, and labels */
215
- schema: {
216
- type: Object,
217
- default: null,
218
- },
219
- /** Array of currently visible column keys */
220
- visibleColumns: {
221
- type: Array,
222
- default: null,
223
- },
224
- /** Current search term */
225
- searchValue: {
226
- type: String,
227
- default: '',
228
- },
229
- /** Whether sidebar is open */
230
- open: {
231
- type: Boolean,
232
- default: true,
233
- },
234
- /** Current active facet filters: { fieldName: [values] } */
235
- activeFilters: {
236
- type: Object,
237
- default: () => ({}),
238
- },
239
- /** Live facet data from API: { fieldName: { values: [{value, count}] } } */
240
- facetData: {
241
- type: Object,
242
- default: () => ({}),
243
- },
244
- /**
245
- * Additional column groups beyond schema properties and the built-in Metadata.
246
- * Each group: { id: string, label: string, columns: Array<{key, label}>, expanded?: boolean }
247
- */
248
- columnGroups: {
249
- type: Array,
250
- default: () => [],
251
- },
252
- /** Whether to include the built-in Metadata column group */
253
- showMetadata: {
254
- type: Boolean,
255
- default: true,
256
- },
257
- /** Search input placeholder */
258
- searchPlaceholder: {
259
- type: String,
260
- default: 'Type to search...',
261
- },
262
- /** Search tab label */
263
- searchTabLabel: {
264
- type: String,
265
- default: 'Search',
266
- },
267
- /** Columns tab label */
268
- columnsTabLabel: {
269
- type: String,
270
- default: 'Columns',
271
- },
272
- /** Search section heading */
273
- searchLabel: {
274
- type: String,
275
- default: 'Search',
276
- },
277
- /** Filters section heading */
278
- filtersLabel: {
279
- type: String,
280
- default: 'Filters',
281
- },
282
- /** Columns section heading */
283
- columnsHeading: {
284
- type: String,
285
- default: 'Column Visibility',
286
- },
287
- /** Columns section description */
288
- columnsDescription: {
289
- type: String,
290
- default: 'Select which columns to display in the table',
291
- },
292
- /** Override label for the schema properties group. Defaults to schema.title. */
293
- propertiesGroupLabel: {
294
- type: String,
295
- default: '',
296
- },
297
- /**
298
- * ID of the tab that should be active when the sidebar opens.
299
- * Built-in IDs are 'search-tab' and 'columns-tab'.
300
- * Use the id you set on your custom NcAppSidebarTab for custom tabs.
301
- */
302
- defaultTab: {
303
- type: String,
304
- default: 'search-tab',
305
- },
306
- },
307
-
308
- data() {
309
- return {
310
- internalOpen: this.open,
311
- internalActiveTab: this.defaultTab,
312
- propertiesExpanded: true,
313
- expandedGroups: {},
314
- }
315
- },
316
-
317
- computed: {
318
- /** Resolved icon explicit prop overrides schema.icon */
319
- resolvedIcon() {
320
- return this.icon || this.schema?.icon || ''
321
- },
322
-
323
- /** Sidebar name — schema title, shown as the h2 header */
324
- resolvedName() {
325
- if (this.title) return this.title
326
- return this.schema?.title || 'Search'
327
- },
328
-
329
- /** Sidebar subname — schema description, shown below the name */
330
- resolvedSubname() {
331
- return this.schema?.description || ''
332
- },
333
-
334
- /** Properties group label — derived from schema.title if not explicitly set */
335
- resolvedPropertiesLabel() {
336
- if (this.propertiesGroupLabel) return this.propertiesGroupLabel
337
- return this.schema?.title || 'Properties'
338
- },
339
-
340
- /** All available columns from schema */
341
- allColumns() {
342
- if (!this.schema) return []
343
- return columnsFromSchema(this.schema, {})
344
- },
345
-
346
- /** Filter definitions from schema (facetable properties) */
347
- schemaFilters() {
348
- if (!this.schema) return []
349
- return filtersFromSchema(this.schema)
350
- },
351
-
352
- /** Combined column groups: built-in Metadata + external groups */
353
- allGroups() {
354
- const groups = []
355
- if (this.showMetadata && this.schema) {
356
- groups.push({
357
- id: 'metadata',
358
- label: 'Metadata',
359
- columns: METADATA_COLUMNS,
360
- expanded: true,
361
- })
362
- }
363
- return [...groups, ...this.columnGroups]
364
- },
365
-
366
- /** All column keys across schema properties and all groups */
367
- allColumnKeys() {
368
- return [
369
- ...this.allColumns.map((c) => c.key),
370
- ...this.allGroups.flatMap((g) => g.columns.map((c) => c.key)),
371
- ]
372
- },
373
- },
374
-
375
- watch: {
376
- open(val) {
377
- this.internalOpen = val
378
- },
379
- internalOpen(val) {
380
- this.$emit('update:open', val)
381
- },
382
- defaultTab(val) {
383
- this.internalActiveTab = val
384
- },
385
- allGroups: {
386
- immediate: true,
387
- handler(groups) {
388
- for (const group of groups) {
389
- if (!(group.id in this.expandedGroups)) {
390
- this.$set(this.expandedGroups, group.id, group.expanded !== false)
391
- }
392
- }
393
- },
394
- },
395
- },
396
-
397
- methods: {
398
- /** Handle tab change from NcAppSidebar */
399
- onTabChange(tabId) {
400
- this.internalActiveTab = tabId
401
- this.$emit('tab-change', tabId)
402
- },
403
-
404
- /** Check if a column is currently visible */
405
- isColumnVisible(key) {
406
- if (this.visibleColumns === null) return true
407
- return this.visibleColumns.includes(key)
408
- },
409
-
410
- /** Check if all columns in a group are visible */
411
- isGroupAllVisible(columns) {
412
- return columns.every((col) => this.isColumnVisible(col.key))
413
- },
414
-
415
- /** Toggle a single column's visibility */
416
- toggleColumn(key) {
417
- let newVisible
418
- if (this.visibleColumns === null) {
419
- newVisible = this.allColumnKeys.filter((k) => k !== key)
420
- } else if (this.isColumnVisible(key)) {
421
- newVisible = this.visibleColumns.filter((k) => k !== key)
422
- } else {
423
- newVisible = [...this.visibleColumns, key]
424
- }
425
- this.$emit('columns-change', newVisible)
426
- },
427
-
428
- /** Select or deselect all columns in a group */
429
- toggleGroupAll(columns) {
430
- const groupKeys = columns.map((c) => c.key)
431
- const allVisible = this.isGroupAllVisible(columns)
432
-
433
- let newVisible
434
- if (this.visibleColumns === null) {
435
- // Currently all visible — deselect this group
436
- newVisible = this.allColumnKeys.filter((k) => !groupKeys.includes(k))
437
- } else if (allVisible) {
438
- // All in group visible deselect them
439
- newVisible = this.visibleColumns.filter((k) => !groupKeys.includes(k))
440
- } else {
441
- // Not all visible — select them all
442
- const current = new Set(this.visibleColumns)
443
- groupKeys.forEach((k) => current.add(k))
444
- newVisible = [...current]
445
- }
446
- this.$emit('columns-change', newVisible)
447
- },
448
-
449
- /** Toggle a group's expanded state */
450
- toggleGroup(groupId) {
451
- this.$set(this.expandedGroups, groupId, !this.expandedGroups[groupId])
452
- },
453
-
454
- /** Get filter options for a filter definition */
455
- getFilterOptions(filter) {
456
- const facet = this.facetData[filter.key]
457
- if (facet?.values?.length > 0) {
458
- return facet.values.map((v) => ({
459
- id: v.value,
460
- label: v.count !== undefined ? `${v.value} (${v.count})` : String(v.value),
461
- }))
462
- }
463
- return filter.options || []
464
- },
465
-
466
- /** Get currently selected options for a filter */
467
- getSelectedFilterOptions(filter) {
468
- const value = this.activeFilters[filter.key]
469
- if (!value) return []
470
- const values = Array.isArray(value) ? value : [value]
471
- const options = this.getFilterOptions(filter)
472
- return values.map((v) => options.find((o) => o.id === v) || { id: v, label: String(v) })
473
- },
474
-
475
- /** Handle filter select change */
476
- onFilterChange(key, selected) {
477
- const values = selected ? selected.map((o) => o.id) : []
478
- this.$emit('filter-change', { key, values })
479
- },
480
- },
481
- }
482
- </script>
483
-
484
- <!-- Styles in css/index-sidebar.css -->
1
+ <template>
2
+ <NcAppSidebar
3
+ :name="resolvedName"
4
+ :title="resolvedName"
5
+ :subname="resolvedSubname"
6
+ :open.sync="internalOpen"
7
+ :active="internalActiveTab"
8
+ :compact="!!resolvedIcon"
9
+ @close="$emit('update:open', false)"
10
+ @update:active="onTabChange">
11
+ <!-- Schema icon in sidebar header -->
12
+ <template v-if="resolvedIcon" #header>
13
+ <div class="cn-index-sidebar__header-icon">
14
+ <CnIcon :name="resolvedIcon" :size="32" />
15
+ </div>
16
+ </template>
17
+
18
+ <!-- Search Tab -->
19
+ <NcAppSidebarTab
20
+ id="search-tab"
21
+ :name="searchTabLabel"
22
+ :order="1">
23
+ <template #icon>
24
+ <Magnify :size="20" />
25
+ </template>
26
+
27
+ <div class="cn-index-sidebar__tab-content">
28
+ <div v-if="$slots['search-above']" class="cn-index-sidebar__section">
29
+ <slot name="search-above" />
30
+ </div>
31
+
32
+ <div class="cn-index-sidebar__section">
33
+ <h3>{{ searchLabel }}</h3>
34
+ <NcTextField
35
+ :value="searchValue"
36
+ :placeholder="searchPlaceholder"
37
+ :label="searchLabel"
38
+ @update:value="$emit('search', $event)" />
39
+ </div>
40
+
41
+ <div v-if="schemaFilters.length > 0" class="cn-index-sidebar__section">
42
+ <h3>{{ filtersLabel }}</h3>
43
+ <div
44
+ v-for="filter in schemaFilters"
45
+ :key="filter.key"
46
+ class="cn-index-sidebar__filter-group">
47
+ <div class="cn-index-sidebar__filter-header">
48
+ <span class="cn-index-sidebar__filter-label">{{ filter.label }}</span>
49
+ <NcPopover v-if="filter.description" popup-role="dialog">
50
+ <template #trigger>
51
+ <NcButton
52
+ type="tertiary-no-background"
53
+ :aria-label="filter.label + ' info'"
54
+ class="cn-index-sidebar__info-btn">
55
+ <template #icon>
56
+ <InformationOutline :size="16" />
57
+ </template>
58
+ </NcButton>
59
+ </template>
60
+ <p class="cn-index-sidebar__filter-description">{{ filter.description }}</p>
61
+ </NcPopover>
62
+ </div>
63
+ <NcSelect
64
+ class="cn-index-sidebar__select"
65
+ :value="getSelectedFilterOptions(filter)"
66
+ :options="getFilterOptions(filter)"
67
+ placeholder="Select..."
68
+ :input-label="filter.label"
69
+ :multiple="true"
70
+ :clearable="true"
71
+ @input="onFilterChange(filter.key, $event)" />
72
+ </div>
73
+ </div>
74
+
75
+ <slot name="search-extra" />
76
+ </div>
77
+ </NcAppSidebarTab>
78
+
79
+ <!-- Columns Tab -->
80
+ <NcAppSidebarTab
81
+ id="columns-tab"
82
+ :name="columnsTabLabel"
83
+ :order="2">
84
+ <template #icon>
85
+ <FormatColumns :size="20" />
86
+ </template>
87
+
88
+ <div class="cn-index-sidebar__tab-content">
89
+ <div class="cn-sidebar-columns">
90
+ <h3>{{ columnsHeading }}</h3>
91
+ <p class="cn-sidebar-columns__description">{{ columnsDescription }}</p>
92
+
93
+ <template v-if="allColumns.length > 0 || allGroups.length > 0">
94
+ <!-- Schema properties group (collapsible) -->
95
+ <div v-if="allColumns.length > 0" class="cn-sidebar-columns__group cn-sidebar-columns__group--collapsible">
96
+ <div class="cn-sidebar-columns__group-header" @click="propertiesExpanded = !propertiesExpanded">
97
+ <ChevronDown v-if="propertiesExpanded" :size="20" />
98
+ <ChevronRight v-else :size="20" />
99
+ <h4>{{ resolvedPropertiesLabel }}</h4>
100
+ <NcCheckboxRadioSwitch
101
+ :checked="isGroupAllVisible(allColumns)"
102
+ class="cn-sidebar-columns__select-all"
103
+ @click.native.stop
104
+ @update:checked="toggleGroupAll(allColumns)">
105
+ All
106
+ </NcCheckboxRadioSwitch>
107
+ </div>
108
+ <div v-if="propertiesExpanded" class="cn-sidebar-columns__group-content">
109
+ <NcCheckboxRadioSwitch
110
+ v-for="col in allColumns"
111
+ :key="col.key"
112
+ :checked="isColumnVisible(col.key)"
113
+ @update:checked="toggleColumn(col.key)">
114
+ {{ col.label }}
115
+ </NcCheckboxRadioSwitch>
116
+ </div>
117
+ </div>
118
+
119
+ <!-- Extra column groups (built-in Metadata + external) -->
120
+ <div
121
+ v-for="group in allGroups"
122
+ :key="group.id"
123
+ class="cn-sidebar-columns__group cn-sidebar-columns__group--collapsible">
124
+ <div class="cn-sidebar-columns__group-header" @click="toggleGroup(group.id)">
125
+ <ChevronDown v-if="expandedGroups[group.id]" :size="20" />
126
+ <ChevronRight v-else :size="20" />
127
+ <h4>{{ group.label }}</h4>
128
+ <NcCheckboxRadioSwitch
129
+ :checked="isGroupAllVisible(group.columns)"
130
+ class="cn-sidebar-columns__select-all"
131
+ @click.native.stop
132
+ @update:checked="toggleGroupAll(group.columns)">
133
+ All
134
+ </NcCheckboxRadioSwitch>
135
+ </div>
136
+ <div v-if="expandedGroups[group.id]" class="cn-sidebar-columns__group-content">
137
+ <NcCheckboxRadioSwitch
138
+ v-for="col in group.columns"
139
+ :key="col.key"
140
+ :checked="isColumnVisible(col.key)"
141
+ @update:checked="toggleColumn(col.key)">
142
+ {{ col.label }}
143
+ </NcCheckboxRadioSwitch>
144
+ </div>
145
+ </div>
146
+ </template>
147
+
148
+ <p v-else class="cn-sidebar-columns__empty">
149
+ No columns available. Provide a schema to auto-generate columns.
150
+ </p>
151
+ </div>
152
+
153
+ <slot name="columns-extra" />
154
+ </div>
155
+ </NcAppSidebarTab>
156
+
157
+ <!-- Extra tabs injected by the consumer -->
158
+ <slot name="tabs" />
159
+ </NcAppSidebar>
160
+ </template>
161
+
162
+ <script>
163
+ import { NcAppSidebar, NcAppSidebarTab, NcTextField, NcSelect, NcCheckboxRadioSwitch, NcPopover, NcButton } from '@nextcloud/vue'
164
+ import Magnify from 'vue-material-design-icons/Magnify.vue'
165
+ import FormatColumns from 'vue-material-design-icons/FormatColumns.vue'
166
+ import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'
167
+ import ChevronRight from 'vue-material-design-icons/ChevronRight.vue'
168
+ import InformationOutline from 'vue-material-design-icons/InformationOutline.vue'
169
+ import { CnIcon } from '../CnIcon/index.js'
170
+ import { columnsFromSchema, filtersFromSchema } from '../../utils/schema.js'
171
+ import { METADATA_COLUMNS } from '../../constants/metadata.js'
172
+
173
+ /**
174
+ * CnIndexSidebar — Reusable NcAppSidebar wrapper with Search + Columns tabs.
175
+ *
176
+ * Designed to be schema-driven: pass a schema and the sidebar auto-generates
177
+ * search filters, column visibility controls, and the standard Metadata group.
178
+ * Title and properties group label are derived from schema.title by default.
179
+ *
180
+ * Must be rendered at the App.vue level as a sibling of NcAppContent.
181
+ * Use provide/inject to connect it to page components.
182
+ *
183
+ * @example
184
+ * <!-- Minimal usage — schema drives everything -->
185
+ * <CnIndexSidebar
186
+ * :schema="schema"
187
+ * :visible-columns="visibleCols"
188
+ * :search-value="search"
189
+ * @search="onSearch"
190
+ * @columns-change="onColumnsChange" />
191
+ *
192
+ * @slot search-above - Content rendered above the search field in the Search tab (e.g. hints, quick actions).
193
+ * @slot search-extra - Content rendered below the search field and filters in the Search tab (e.g. saved searches).
194
+ */
195
+ export default {
196
+ name: 'CnIndexSidebar',
197
+
198
+ components: {
199
+ NcAppSidebar,
200
+ NcAppSidebarTab,
201
+ NcTextField,
202
+ NcSelect,
203
+ NcCheckboxRadioSwitch,
204
+ NcPopover,
205
+ NcButton,
206
+ CnIcon,
207
+ Magnify,
208
+ FormatColumns,
209
+ ChevronDown,
210
+ ChevronRight,
211
+ InformationOutline,
212
+ },
213
+
214
+ props: {
215
+ /** Sidebar title. Defaults to schema.title when not set. */
216
+ title: {
217
+ type: String,
218
+ default: '',
219
+ },
220
+ /** MDI icon name or emoji. Defaults to schema.icon when not set. */
221
+ icon: {
222
+ type: String,
223
+ default: '',
224
+ },
225
+ /** Schema object for auto-generating filters, columns, and labels */
226
+ schema: {
227
+ type: Object,
228
+ default: null,
229
+ },
230
+ /** Array of currently visible column keys */
231
+ visibleColumns: {
232
+ type: Array,
233
+ default: null,
234
+ },
235
+ /** Current search term */
236
+ searchValue: {
237
+ type: String,
238
+ default: '',
239
+ },
240
+ /** Whether sidebar is open */
241
+ open: {
242
+ type: Boolean,
243
+ default: true,
244
+ },
245
+ /** Current active facet filters: { fieldName: [values] } */
246
+ activeFilters: {
247
+ type: Object,
248
+ default: () => ({}),
249
+ },
250
+ /** Live facet data from API: { fieldName: { values: [{value, count}] } } */
251
+ facetData: {
252
+ type: Object,
253
+ default: () => ({}),
254
+ },
255
+ /**
256
+ * Additional column groups beyond schema properties and the built-in Metadata.
257
+ * Each group: { id: string, label: string, columns: Array<{key, label}>, expanded?: boolean }
258
+ */
259
+ columnGroups: {
260
+ type: Array,
261
+ default: () => [],
262
+ },
263
+ /** Whether to include the built-in Metadata column group */
264
+ showMetadata: {
265
+ type: Boolean,
266
+ default: true,
267
+ },
268
+ /** Search input placeholder */
269
+ searchPlaceholder: {
270
+ type: String,
271
+ default: 'Type to search...',
272
+ },
273
+ /** Search tab label */
274
+ searchTabLabel: {
275
+ type: String,
276
+ default: 'Search',
277
+ },
278
+ /** Columns tab label */
279
+ columnsTabLabel: {
280
+ type: String,
281
+ default: 'Columns',
282
+ },
283
+ /** Search section heading */
284
+ searchLabel: {
285
+ type: String,
286
+ default: 'Search',
287
+ },
288
+ /** Filters section heading */
289
+ filtersLabel: {
290
+ type: String,
291
+ default: 'Filters',
292
+ },
293
+ /** Columns section heading */
294
+ columnsHeading: {
295
+ type: String,
296
+ default: 'Column Visibility',
297
+ },
298
+ /** Columns section description */
299
+ columnsDescription: {
300
+ type: String,
301
+ default: 'Select which columns to display in the table',
302
+ },
303
+ /** Override label for the schema properties group. Defaults to schema.title. */
304
+ propertiesGroupLabel: {
305
+ type: String,
306
+ default: '',
307
+ },
308
+ /**
309
+ * ID of the tab that should be active when the sidebar opens.
310
+ * Built-in IDs are 'search-tab' and 'columns-tab'.
311
+ * Use the id you set on your custom NcAppSidebarTab for custom tabs.
312
+ */
313
+ defaultTab: {
314
+ type: String,
315
+ default: 'search-tab',
316
+ },
317
+ /**
318
+ * Whether the current user is an admin.
319
+ * When false, schema properties with `adminOnly: true` are hidden from filters.
320
+ */
321
+ userIsAdmin: {
322
+ type: Boolean,
323
+ default: true,
324
+ },
325
+ },
326
+
327
+ data() {
328
+ return {
329
+ internalOpen: this.open,
330
+ internalActiveTab: this.defaultTab,
331
+ propertiesExpanded: true,
332
+ expandedGroups: {},
333
+ }
334
+ },
335
+
336
+ computed: {
337
+ /** Resolved icon — explicit prop overrides schema.icon */
338
+ resolvedIcon() {
339
+ return this.icon || this.schema?.icon || ''
340
+ },
341
+
342
+ /** Sidebar name — schema title, shown as the h2 header */
343
+ resolvedName() {
344
+ if (this.title) return this.title
345
+ return this.schema?.title || 'Search'
346
+ },
347
+
348
+ /** Sidebar subname — schema description, shown below the name */
349
+ resolvedSubname() {
350
+ return this.schema?.description || ''
351
+ },
352
+
353
+ /** Properties group label — derived from schema.title if not explicitly set */
354
+ resolvedPropertiesLabel() {
355
+ if (this.propertiesGroupLabel) return this.propertiesGroupLabel
356
+ return this.schema?.title || 'Properties'
357
+ },
358
+
359
+ /** All available columns from schema */
360
+ allColumns() {
361
+ if (!this.schema) return []
362
+ return columnsFromSchema(this.schema, {})
363
+ },
364
+
365
+ /** Filter definitions from schema (facetable properties, respecting RBAC) */
366
+ schemaFilters() {
367
+ if (!this.schema) return []
368
+ return filtersFromSchema(this.schema, { isAdmin: this.userIsAdmin })
369
+ },
370
+
371
+ /** Combined column groups: built-in Metadata + external groups */
372
+ allGroups() {
373
+ const groups = []
374
+ if (this.showMetadata && this.schema) {
375
+ groups.push({
376
+ id: 'metadata',
377
+ label: 'Metadata',
378
+ columns: METADATA_COLUMNS,
379
+ expanded: true,
380
+ })
381
+ }
382
+ return [...groups, ...this.columnGroups]
383
+ },
384
+
385
+ /** All column keys across schema properties and all groups */
386
+ allColumnKeys() {
387
+ return [
388
+ ...this.allColumns.map((c) => c.key),
389
+ ...this.allGroups.flatMap((g) => g.columns.map((c) => c.key)),
390
+ ]
391
+ },
392
+ },
393
+
394
+ watch: {
395
+ open(val) {
396
+ this.internalOpen = val
397
+ },
398
+ internalOpen(val) {
399
+ this.$emit('update:open', val)
400
+ },
401
+ defaultTab(val) {
402
+ this.internalActiveTab = val
403
+ },
404
+ allGroups: {
405
+ immediate: true,
406
+ handler(groups) {
407
+ for (const group of groups) {
408
+ if (!(group.id in this.expandedGroups)) {
409
+ this.$set(this.expandedGroups, group.id, group.expanded !== false)
410
+ }
411
+ }
412
+ },
413
+ },
414
+ },
415
+
416
+ methods: {
417
+ /** Handle tab change from NcAppSidebar */
418
+ onTabChange(tabId) {
419
+ this.internalActiveTab = tabId
420
+ this.$emit('tab-change', tabId)
421
+ },
422
+
423
+ /** Check if a column is currently visible */
424
+ isColumnVisible(key) {
425
+ if (this.visibleColumns === null) return true
426
+ return this.visibleColumns.includes(key)
427
+ },
428
+
429
+ /** Check if all columns in a group are visible */
430
+ isGroupAllVisible(columns) {
431
+ return columns.every((col) => this.isColumnVisible(col.key))
432
+ },
433
+
434
+ /** Toggle a single column's visibility */
435
+ toggleColumn(key) {
436
+ let newVisible
437
+ if (this.visibleColumns === null) {
438
+ newVisible = this.allColumnKeys.filter((k) => k !== key)
439
+ } else if (this.isColumnVisible(key)) {
440
+ newVisible = this.visibleColumns.filter((k) => k !== key)
441
+ } else {
442
+ newVisible = [...this.visibleColumns, key]
443
+ }
444
+ this.$emit('columns-change', newVisible)
445
+ },
446
+
447
+ /** Select or deselect all columns in a group */
448
+ toggleGroupAll(columns) {
449
+ const groupKeys = columns.map((c) => c.key)
450
+ const allVisible = this.isGroupAllVisible(columns)
451
+
452
+ let newVisible
453
+ if (this.visibleColumns === null) {
454
+ // Currently all visible deselect this group
455
+ newVisible = this.allColumnKeys.filter((k) => !groupKeys.includes(k))
456
+ } else if (allVisible) {
457
+ // All in group visible — deselect them
458
+ newVisible = this.visibleColumns.filter((k) => !groupKeys.includes(k))
459
+ } else {
460
+ // Not all visible select them all
461
+ const current = new Set(this.visibleColumns)
462
+ groupKeys.forEach((k) => current.add(k))
463
+ newVisible = [...current]
464
+ }
465
+ this.$emit('columns-change', newVisible)
466
+ },
467
+
468
+ /** Toggle a group's expanded state */
469
+ toggleGroup(groupId) {
470
+ this.$set(this.expandedGroups, groupId, !this.expandedGroups[groupId])
471
+ },
472
+
473
+ /** Get filter options for a filter definition */
474
+ getFilterOptions(filter) {
475
+ const facet = this.facetData[filter.key]
476
+ if (facet?.values?.length > 0) {
477
+ return facet.values.map((v) => ({
478
+ id: v.value,
479
+ label: v.count !== undefined ? `${v.value} (${v.count})` : String(v.value),
480
+ }))
481
+ }
482
+ return filter.options || []
483
+ },
484
+
485
+ /** Get currently selected options for a filter */
486
+ getSelectedFilterOptions(filter) {
487
+ const value = this.activeFilters[filter.key]
488
+ if (!value) return []
489
+ const values = Array.isArray(value) ? value : [value]
490
+ const options = this.getFilterOptions(filter)
491
+ return values.map((v) => options.find((o) => o.id === v) || { id: v, label: String(v) })
492
+ },
493
+
494
+ /** Handle filter select change */
495
+ onFilterChange(key, selected) {
496
+ const values = selected ? selected.map((o) => o.id) : []
497
+ this.$emit('filter-change', { key, values })
498
+ },
499
+ },
500
+ }
501
+ </script>
502
+
503
+ <!-- Styles in css/index-sidebar.css -->