@conduction/nextcloud-vue 0.1.0-beta.11 → 0.1.0-beta.12

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