@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,432 +1,432 @@
1
- <!--
2
- CnDetailPage — Generic detail/overview page.
3
-
4
- The detail page equivalent of CnDashboardPage. Assembles a complete entity detail
5
- view from card-based sections, matching the dashboard visual style (rounded cards
6
- with headers). Uses a fixed declarative layout (no drag-and-drop).
7
-
8
- Features:
9
- - Header with back button, title, subtitle, and action buttons
10
- - Card-based content area (via default slot with CnDetailCard components)
11
- - Optional 12-column CSS grid layout mode (via layout + widgets props)
12
- - Optional right sidebar (CnObjectSidebar) for files, notes, tags, tasks, audit trail
13
- - Loading and error states
14
- - Edit mode toggle
15
-
16
- A simpler alternative to CnIndexPage for detail, stats, and overview pages.
17
- No multi-object table, no CRUD dialogs — just a clean layout with:
18
- - Header (title, description, icon, action buttons)
19
- - Loading / error / empty states
20
- - Statistics table section
21
- - Content sections via slots
22
- -->
23
- <template>
24
- <div class="cn-detail-page" :style="{ maxWidth: maxWidth }">
25
- <!-- Header -->
26
- <div class="cn-detail-page__header">
27
- <div class="cn-detail-page__header-left">
28
- <slot name="icon">
29
- <CnIcon
30
- v-if="icon"
31
- :name="icon"
32
- :size="iconSize"
33
- class="cn-detail-page__icon" />
34
- </slot>
35
- <div class="cn-detail-page__header-text">
36
- <h2 v-if="title" class="cn-detail-page__title">
37
- {{ title }}
38
- </h2>
39
- <p v-if="description" class="cn-detail-page__description">
40
- {{ description }}
41
- </p>
42
- </div>
43
- </div>
44
- <div class="cn-detail-page__header-actions">
45
- <slot name="actions" />
46
- </div>
47
- </div>
48
-
49
- <!-- Loading state -->
50
- <div v-if="loading" class="cn-detail-page__loading">
51
- <NcLoadingIcon :size="32" />
52
- <span>{{ loadingLabel }}</span>
53
- </div>
54
-
55
- <!-- Error state -->
56
- <div v-else-if="error" class="cn-detail-page__error">
57
- <slot name="error">
58
- <NcEmptyContent :name="errorMessage">
59
- <template #icon>
60
- <AlertCircleOutline :size="48" />
61
- </template>
62
- <template #action>
63
- <NcButton v-if="onRetry" type="primary" @click="onRetry">
64
- <template #icon>
65
- <Refresh :size="20" />
66
- </template>
67
- {{ retryLabel }}
68
- </NcButton>
69
- <slot name="error-actions" />
70
- </template>
71
- </NcEmptyContent>
72
- </slot>
73
- </div>
74
-
75
- <!-- Empty state -->
76
- <div v-else-if="empty" class="cn-detail-page__empty">
77
- <slot name="empty">
78
- <NcEmptyContent :name="emptyLabel">
79
- <template #icon>
80
- <InformationOutline :size="48" />
81
- </template>
82
- <template #action>
83
- <slot name="empty-actions" />
84
- </template>
85
- </NcEmptyContent>
86
- </slot>
87
- </div>
88
-
89
- <!-- Main content -->
90
- <div v-else class="cn-detail-page__body">
91
- <!-- Grid layout mode -->
92
- <div v-if="hasGridLayout" class="cn-detail-page__content cn-detail-page__content--grid">
93
- <section
94
- v-for="item in sortedLayout"
95
- :key="item.id"
96
- :style="widgetGridStyle(item)"
97
- class="cn-detail-page__grid-item"
98
- :aria-labelledby="item.showTitle !== false && findWidget(item) ? `widget-title-${item.id}` : undefined">
99
- <h3
100
- v-if="item.showTitle !== false && findWidget(item)"
101
- :id="`widget-title-${item.id}`"
102
- class="cn-detail-page__widget-title">
103
- {{ findWidget(item).title }}
104
- </h3>
105
- <slot
106
- :name="`widget-${item.widgetId}`"
107
- :item="item"
108
- :widget="findWidget(item)" />
109
- </section>
110
- </div>
111
-
112
- <!-- Statistics table -->
113
- <div v-if="hasStats" class="cn-detail-page__stats">
114
- <slot name="stats-header">
115
- <h3 v-if="statsTitle" class="cn-detail-page__section-title">
116
- {{ statsTitle }}
117
- </h3>
118
- </slot>
119
- <table class="cn-detail-page__stats-table">
120
- <thead v-if="statsColumns.length > 0">
121
- <tr>
122
- <th v-for="col in statsColumns" :key="col.key" :class="col.align ? 'cn-detail-page__stats-cell--' + col.align : ''">
123
- {{ col.label }}
124
- </th>
125
- </tr>
126
- </thead>
127
- <tbody>
128
- <slot name="stats-rows">
129
- <tr v-for="(row, index) in statsRows" :key="index" :class="{ 'cn-detail-page__stats-row--sub': row.indent }">
130
- <td v-for="col in statsColumns" :key="col.key" :class="[row.indent ? 'cn-detail-page__stats-cell--indented' : '', col.align ? 'cn-detail-page__stats-cell--' + col.align : '']">
131
- {{ row[col.key] !== undefined ? row[col.key] : '-' }}
132
- </td>
133
- </tr>
134
- </slot>
135
- </tbody>
136
- </table>
137
- </div>
138
-
139
- <!-- Default vertical stacking mode -->
140
- <div v-else class="cn-detail-page__content">
141
- <!-- Default content -->
142
- <div class="cn-detail-page__content">
143
- <slot />
144
- </div>
145
-
146
- <!-- Sections slot — additional content below stats -->
147
- <div v-if="$slots.sections" class="cn-detail-page__sections">
148
- <slot name="sections" />
149
- </div>
150
- </div>
151
-
152
- <!-- Footer -->
153
- <div v-if="$slots.footer" class="cn-detail-page__footer">
154
- <slot name="footer" />
155
- </div>
156
- </div>
157
- </div>
158
- </template>
159
-
160
- <script>
161
- import { translate as t } from '@nextcloud/l10n'
162
- import { NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'
163
- import { CnIcon } from '../CnIcon/index.js'
164
- import AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'
165
- import InformationOutline from 'vue-material-design-icons/InformationOutline.vue'
166
- import { gridLayout } from '../../mixins/gridLayout.js'
167
- import Refresh from 'vue-material-design-icons/Refresh.vue'
168
-
169
- /**
170
- * CnDetailPage — Generic detail/overview page.
171
- *
172
- * Supports two layout modes:
173
- * 1. **Default (vertical stacking):** Content provided via default slot, cards stack vertically.
174
- * 2. **Grid layout:** When `layout` and `widgets` props are provided, content renders in a
175
- * 12-column CSS grid with `#widget-{widgetId}` scoped slots. Same API as CnDashboardPage.
176
- *
177
- * @example Basic usage (vertical stacking)
178
- *
179
- * A simpler alternative to CnIndexPage for pages that display detail info,
180
- * statistics, charts, or card grids — without multi-object tables or CRUD
181
- * dialogs. Provides a consistent layout with header, loading/error/empty
182
- * states, a statistics table, and flexible content slots.
183
- *
184
- * @example Basic usage with stats table and content
185
- * <CnDetailPage
186
- * title="Register Overview"
187
- * description="Statistics and schema details"
188
- * icon="DatabaseOutline"
189
- * :stats-title="'Register Statistics'"
190
- * :stats-columns="[
191
- * { key: 'type', label: 'Type' },
192
- * { key: 'total', label: 'Total' },
193
- * { key: 'size', label: 'Size' },
194
- * ]"
195
- * :stats-rows="[
196
- * { type: 'Objects', total: 150, size: '2.4 MB' },
197
- * { type: 'Files', total: 42, size: '1.1 MB' },
198
- * ]"
199
- * :loading="isLoading">
200
- * <ChartGrid :data="chartData" />
201
- * <SchemaCards :schemas="schemas" />
202
- * </CnDetailPage>
203
- *
204
- * @example Grid layout mode
205
- * <CnDetailPage
206
- * title="Character Detail"
207
- * :layout="[
208
- * { id: 1, widgetId: 'info', gridX: 0, gridY: 0, gridWidth: 8 },
209
- * { id: 2, widgetId: 'stats', gridX: 8, gridY: 0, gridWidth: 4 },
210
- * ]"
211
- * :widgets="[
212
- * { id: 'info', title: 'Character Info' },
213
- * { id: 'stats', title: 'Statistics' },
214
- * ]">
215
- * <template #widget-info="{ item, widget }">
216
- * <CharacterInfoCard :character="character" />
217
- * </template>
218
- * <template #widget-stats="{ item, widget }">
219
- * <StatsCard :stats="character.stats" />
220
- * </template>
221
- *
222
- * @example With header actions and error handling
223
- * <CnDetailPage
224
- * title="Schema Details"
225
- * :error="hasError"
226
- * error-message="Failed to load schema"
227
- * :on-retry="loadSchema">
228
- * <template #actions>
229
- * <NcButton @click="editSchema">Edit</NcButton>
230
- * </template>
231
- * <DetailContent :schema="schema" />
232
- * </CnDetailPage>
233
- */
234
- export default {
235
- name: 'CnDetailPage',
236
-
237
- components: {
238
- NcButton,
239
- NcEmptyContent,
240
- NcLoadingIcon,
241
- CnIcon,
242
- AlertCircleOutline,
243
- InformationOutline,
244
- Refresh,
245
- },
246
-
247
- mixins: [gridLayout],
248
-
249
- inject: {
250
- objectSidebarState: { default: null },
251
- },
252
-
253
- props: {
254
- /** Page title */
255
- title: {
256
- type: String,
257
- default: '',
258
- },
259
- /** Page description (shown below title) */
260
- description: {
261
- type: String,
262
- default: '',
263
- },
264
- /** Optional MDI icon name (rendered via CnIcon) */
265
- icon: {
266
- type: String,
267
- default: '',
268
- },
269
- /** Icon size in pixels */
270
- iconSize: {
271
- type: Number,
272
- default: 28,
273
- },
274
- /** Whether the page is in a loading state */
275
- loading: {
276
- type: Boolean,
277
- default: false,
278
- },
279
- /** Message shown during loading */
280
- loadingLabel: {
281
- type: String,
282
- default: () => t('nextcloud-vue', 'Loading...'),
283
- },
284
- /** Whether to activate the external sidebar (via objectSidebarState inject) */
285
- sidebar: {
286
- type: Boolean,
287
- default: false,
288
- },
289
- /** Whether the sidebar is open (expanded) */
290
- sidebarOpen: {
291
- type: Boolean,
292
- default: true,
293
- },
294
- /** The registered object type slug for the sidebar */
295
- objectType: {
296
- type: String,
297
- default: '',
298
- },
299
- /** The object ID to display in the sidebar */
300
- objectId: {
301
- type: [String, Number],
302
- default: '',
303
- },
304
- /** Subtitle shown in the sidebar header */
305
- subtitle: {
306
- type: String,
307
- default: '',
308
- },
309
- /** Additional sidebar configuration (register, schema, hiddenTabs, title, subtitle) */
310
- sidebarProps: {
311
- type: Object,
312
- default: () => ({}),
313
- },
314
- /** Whether the page is in an error state */
315
- error: {
316
- type: Boolean,
317
- default: false,
318
- },
319
- /** Error message shown in error state */
320
- errorMessage: {
321
- type: String,
322
- default: () => t('nextcloud-vue', 'An error occurred'),
323
- },
324
- /** Callback for retry button in error state. If null, no retry button is shown. */
325
- onRetry: {
326
- type: Function,
327
- default: null,
328
- },
329
- /** Label for the retry button */
330
- retryLabel: {
331
- type: String,
332
- default: () => t('nextcloud-vue', 'Retry'),
333
- },
334
- /** Whether the page has no data to show */
335
- empty: {
336
- type: Boolean,
337
- default: false,
338
- },
339
- /** Message shown when page is empty */
340
- emptyLabel: {
341
- type: String,
342
- default: () => t('nextcloud-vue', 'No data available'),
343
- },
344
- /** Title shown above the statistics table */
345
- statsTitle: {
346
- type: String,
347
- default: '',
348
- },
349
- /**
350
- * Column definitions for the statistics table.
351
- * Each column: `{ key: string, label: string, align?: 'left'|'center'|'right' }`
352
- *
353
- * @type {Array<{ key: string, label: string, align?: string }>}
354
- */
355
- statsColumns: {
356
- type: Array,
357
- default: () => [],
358
- },
359
- /**
360
- * Row data for the statistics table. Each row is an object keyed by
361
- * column keys. Set `indent: true` on a row for sub-row styling.
362
- *
363
- * @type {Array<object>}
364
- */
365
- statsRows: {
366
- type: Array,
367
- default: () => [],
368
- },
369
- /** Maximum width of the page content */
370
- maxWidth: {
371
- type: String,
372
- default: '1200px',
373
- },
374
- },
375
-
376
- computed: {
377
- /**
378
- * Whether the sidebar is rendered externally (via objectSidebarState inject)
379
- * rather than inline. When external, CnDetailPage only manages state —
380
- * the parent App renders the actual NcAppSidebar.
381
- */
382
- hasExternalSidebar() {
383
- return !!this.objectSidebarState
384
- },
385
- hasStats() {
386
- return this.statsColumns.length > 0 && (this.statsRows.length > 0 || !!this.$slots['stats-rows'])
387
- },
388
- },
389
-
390
- watch: {
391
- sidebar: {
392
- immediate: true,
393
- handler() { this.syncSidebarState() },
394
- },
395
- title() { this.syncSidebarState() },
396
- subtitle() { this.syncSidebarState() },
397
- objectType() { this.syncSidebarState() },
398
- objectId() { this.syncSidebarState() },
399
- sidebarProps: {
400
- deep: true,
401
- handler() { this.syncSidebarState() },
402
- },
403
- },
404
-
405
- beforeDestroy() {
406
- if (this.hasExternalSidebar) {
407
- this.objectSidebarState.active = false
408
- }
409
- },
410
-
411
- methods: {
412
- syncSidebarState() {
413
- if (!this.hasExternalSidebar) return
414
- if (this.sidebar && this.objectType && this.objectId) {
415
- this.objectSidebarState.active = true
416
- this.objectSidebarState.open = this.sidebarOpen
417
- this.objectSidebarState.objectType = this.objectType
418
- this.objectSidebarState.objectId = this.objectId
419
- this.objectSidebarState.title = this.sidebarProps.title || this.title || ''
420
- this.objectSidebarState.subtitle = this.sidebarProps.subtitle || this.subtitle || ''
421
- this.objectSidebarState.register = this.sidebarProps.register || ''
422
- this.objectSidebarState.schema = this.sidebarProps.schema || ''
423
- this.objectSidebarState.hiddenTabs = this.sidebarProps.hiddenTabs || []
424
- } else {
425
- this.objectSidebarState.active = false
426
- }
427
- },
428
- },
429
- }
430
- </script>
431
-
432
- <!-- Styles in css/detail-page.css -->
1
+ <!--
2
+ CnDetailPage — Generic detail/overview page.
3
+
4
+ The detail page equivalent of CnDashboardPage. Assembles a complete entity detail
5
+ view from card-based sections, matching the dashboard visual style (rounded cards
6
+ with headers). Uses a fixed declarative layout (no drag-and-drop).
7
+
8
+ Features:
9
+ - Header with back button, title, subtitle, and action buttons
10
+ - Card-based content area (via default slot with CnDetailCard components)
11
+ - Optional 12-column CSS grid layout mode (via layout + widgets props)
12
+ - Optional right sidebar (CnObjectSidebar) for files, notes, tags, tasks, audit trail
13
+ - Loading and error states
14
+ - Edit mode toggle
15
+
16
+ A simpler alternative to CnIndexPage for detail, stats, and overview pages.
17
+ No multi-object table, no CRUD dialogs — just a clean layout with:
18
+ - Header (title, description, icon, action buttons)
19
+ - Loading / error / empty states
20
+ - Statistics table section
21
+ - Content sections via slots
22
+ -->
23
+ <template>
24
+ <div class="cn-detail-page" :style="{ maxWidth: maxWidth }">
25
+ <!-- Header -->
26
+ <div class="cn-detail-page__header">
27
+ <div class="cn-detail-page__header-left">
28
+ <slot name="icon">
29
+ <CnIcon
30
+ v-if="icon"
31
+ :name="icon"
32
+ :size="iconSize"
33
+ class="cn-detail-page__icon" />
34
+ </slot>
35
+ <div class="cn-detail-page__header-text">
36
+ <h2 v-if="title" class="cn-detail-page__title">
37
+ {{ title }}
38
+ </h2>
39
+ <p v-if="description" class="cn-detail-page__description">
40
+ {{ description }}
41
+ </p>
42
+ </div>
43
+ </div>
44
+ <div class="cn-detail-page__header-actions">
45
+ <slot name="actions" />
46
+ </div>
47
+ </div>
48
+
49
+ <!-- Loading state -->
50
+ <div v-if="loading" class="cn-detail-page__loading">
51
+ <NcLoadingIcon :size="32" />
52
+ <span>{{ loadingLabel }}</span>
53
+ </div>
54
+
55
+ <!-- Error state -->
56
+ <div v-else-if="error" class="cn-detail-page__error">
57
+ <slot name="error">
58
+ <NcEmptyContent :name="errorMessage">
59
+ <template #icon>
60
+ <AlertCircleOutline :size="48" />
61
+ </template>
62
+ <template #action>
63
+ <NcButton v-if="onRetry" type="primary" @click="onRetry">
64
+ <template #icon>
65
+ <Refresh :size="20" />
66
+ </template>
67
+ {{ retryLabel }}
68
+ </NcButton>
69
+ <slot name="error-actions" />
70
+ </template>
71
+ </NcEmptyContent>
72
+ </slot>
73
+ </div>
74
+
75
+ <!-- Empty state -->
76
+ <div v-else-if="empty" class="cn-detail-page__empty">
77
+ <slot name="empty">
78
+ <NcEmptyContent :name="emptyLabel">
79
+ <template #icon>
80
+ <InformationOutline :size="48" />
81
+ </template>
82
+ <template #action>
83
+ <slot name="empty-actions" />
84
+ </template>
85
+ </NcEmptyContent>
86
+ </slot>
87
+ </div>
88
+
89
+ <!-- Main content -->
90
+ <div v-else class="cn-detail-page__body">
91
+ <!-- Grid layout mode -->
92
+ <div v-if="hasGridLayout" class="cn-detail-page__content cn-detail-page__content--grid">
93
+ <section
94
+ v-for="item in sortedLayout"
95
+ :key="item.id"
96
+ :style="widgetGridStyle(item)"
97
+ class="cn-detail-page__grid-item"
98
+ :aria-labelledby="item.showTitle !== false && findWidget(item) ? `widget-title-${item.id}` : undefined">
99
+ <h3
100
+ v-if="item.showTitle !== false && findWidget(item)"
101
+ :id="`widget-title-${item.id}`"
102
+ class="cn-detail-page__widget-title">
103
+ {{ findWidget(item).title }}
104
+ </h3>
105
+ <slot
106
+ :name="`widget-${item.widgetId}`"
107
+ :item="item"
108
+ :widget="findWidget(item)" />
109
+ </section>
110
+ </div>
111
+
112
+ <!-- Statistics table -->
113
+ <div v-if="hasStats" class="cn-detail-page__stats">
114
+ <slot name="stats-header">
115
+ <h3 v-if="statsTitle" class="cn-detail-page__section-title">
116
+ {{ statsTitle }}
117
+ </h3>
118
+ </slot>
119
+ <table class="cn-detail-page__stats-table">
120
+ <thead v-if="statsColumns.length > 0">
121
+ <tr>
122
+ <th v-for="col in statsColumns" :key="col.key" :class="col.align ? 'cn-detail-page__stats-cell--' + col.align : ''">
123
+ {{ col.label }}
124
+ </th>
125
+ </tr>
126
+ </thead>
127
+ <tbody>
128
+ <slot name="stats-rows">
129
+ <tr v-for="(row, index) in statsRows" :key="index" :class="{ 'cn-detail-page__stats-row--sub': row.indent }">
130
+ <td v-for="col in statsColumns" :key="col.key" :class="[row.indent ? 'cn-detail-page__stats-cell--indented' : '', col.align ? 'cn-detail-page__stats-cell--' + col.align : '']">
131
+ {{ row[col.key] !== undefined ? row[col.key] : '-' }}
132
+ </td>
133
+ </tr>
134
+ </slot>
135
+ </tbody>
136
+ </table>
137
+ </div>
138
+
139
+ <!-- Default vertical stacking mode -->
140
+ <div v-else class="cn-detail-page__content">
141
+ <!-- Default content -->
142
+ <div class="cn-detail-page__content">
143
+ <slot />
144
+ </div>
145
+
146
+ <!-- Sections slot — additional content below stats -->
147
+ <div v-if="$slots.sections" class="cn-detail-page__sections">
148
+ <slot name="sections" />
149
+ </div>
150
+ </div>
151
+
152
+ <!-- Footer -->
153
+ <div v-if="$slots.footer" class="cn-detail-page__footer">
154
+ <slot name="footer" />
155
+ </div>
156
+ </div>
157
+ </div>
158
+ </template>
159
+
160
+ <script>
161
+ import { translate as t } from '@nextcloud/l10n'
162
+ import { NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'
163
+ import { CnIcon } from '../CnIcon/index.js'
164
+ import AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'
165
+ import InformationOutline from 'vue-material-design-icons/InformationOutline.vue'
166
+ import { gridLayout } from '../../mixins/gridLayout.js'
167
+ import Refresh from 'vue-material-design-icons/Refresh.vue'
168
+
169
+ /**
170
+ * CnDetailPage — Generic detail/overview page.
171
+ *
172
+ * Supports two layout modes:
173
+ * 1. **Default (vertical stacking):** Content provided via default slot, cards stack vertically.
174
+ * 2. **Grid layout:** When `layout` and `widgets` props are provided, content renders in a
175
+ * 12-column CSS grid with `#widget-{widgetId}` scoped slots. Same API as CnDashboardPage.
176
+ *
177
+ * @example Basic usage (vertical stacking)
178
+ *
179
+ * A simpler alternative to CnIndexPage for pages that display detail info,
180
+ * statistics, charts, or card grids — without multi-object tables or CRUD
181
+ * dialogs. Provides a consistent layout with header, loading/error/empty
182
+ * states, a statistics table, and flexible content slots.
183
+ *
184
+ * @example Basic usage with stats table and content
185
+ * <CnDetailPage
186
+ * title="Register Overview"
187
+ * description="Statistics and schema details"
188
+ * icon="DatabaseOutline"
189
+ * :stats-title="'Register Statistics'"
190
+ * :stats-columns="[
191
+ * { key: 'type', label: 'Type' },
192
+ * { key: 'total', label: 'Total' },
193
+ * { key: 'size', label: 'Size' },
194
+ * ]"
195
+ * :stats-rows="[
196
+ * { type: 'Objects', total: 150, size: '2.4 MB' },
197
+ * { type: 'Files', total: 42, size: '1.1 MB' },
198
+ * ]"
199
+ * :loading="isLoading">
200
+ * <ChartGrid :data="chartData" />
201
+ * <SchemaCards :schemas="schemas" />
202
+ * </CnDetailPage>
203
+ *
204
+ * @example Grid layout mode
205
+ * <CnDetailPage
206
+ * title="Character Detail"
207
+ * :layout="[
208
+ * { id: 1, widgetId: 'info', gridX: 0, gridY: 0, gridWidth: 8 },
209
+ * { id: 2, widgetId: 'stats', gridX: 8, gridY: 0, gridWidth: 4 },
210
+ * ]"
211
+ * :widgets="[
212
+ * { id: 'info', title: 'Character Info' },
213
+ * { id: 'stats', title: 'Statistics' },
214
+ * ]">
215
+ * <template #widget-info="{ item, widget }">
216
+ * <CharacterInfoCard :character="character" />
217
+ * </template>
218
+ * <template #widget-stats="{ item, widget }">
219
+ * <StatsCard :stats="character.stats" />
220
+ * </template>
221
+ *
222
+ * @example With header actions and error handling
223
+ * <CnDetailPage
224
+ * title="Schema Details"
225
+ * :error="hasError"
226
+ * error-message="Failed to load schema"
227
+ * :on-retry="loadSchema">
228
+ * <template #actions>
229
+ * <NcButton @click="editSchema">Edit</NcButton>
230
+ * </template>
231
+ * <DetailContent :schema="schema" />
232
+ * </CnDetailPage>
233
+ */
234
+ export default {
235
+ name: 'CnDetailPage',
236
+
237
+ components: {
238
+ NcButton,
239
+ NcEmptyContent,
240
+ NcLoadingIcon,
241
+ CnIcon,
242
+ AlertCircleOutline,
243
+ InformationOutline,
244
+ Refresh,
245
+ },
246
+
247
+ mixins: [gridLayout],
248
+
249
+ inject: {
250
+ objectSidebarState: { default: null },
251
+ },
252
+
253
+ props: {
254
+ /** Page title */
255
+ title: {
256
+ type: String,
257
+ default: '',
258
+ },
259
+ /** Page description (shown below title) */
260
+ description: {
261
+ type: String,
262
+ default: '',
263
+ },
264
+ /** Optional MDI icon name (rendered via CnIcon) */
265
+ icon: {
266
+ type: String,
267
+ default: '',
268
+ },
269
+ /** Icon size in pixels */
270
+ iconSize: {
271
+ type: Number,
272
+ default: 28,
273
+ },
274
+ /** Whether the page is in a loading state */
275
+ loading: {
276
+ type: Boolean,
277
+ default: false,
278
+ },
279
+ /** Message shown during loading */
280
+ loadingLabel: {
281
+ type: String,
282
+ default: () => t('nextcloud-vue', 'Loading...'),
283
+ },
284
+ /** Whether to activate the external sidebar (via objectSidebarState inject) */
285
+ sidebar: {
286
+ type: Boolean,
287
+ default: false,
288
+ },
289
+ /** Whether the sidebar is open (expanded) */
290
+ sidebarOpen: {
291
+ type: Boolean,
292
+ default: true,
293
+ },
294
+ /** The registered object type slug for the sidebar */
295
+ objectType: {
296
+ type: String,
297
+ default: '',
298
+ },
299
+ /** The object ID to display in the sidebar */
300
+ objectId: {
301
+ type: [String, Number],
302
+ default: '',
303
+ },
304
+ /** Subtitle shown in the sidebar header */
305
+ subtitle: {
306
+ type: String,
307
+ default: '',
308
+ },
309
+ /** Additional sidebar configuration (register, schema, hiddenTabs, title, subtitle) */
310
+ sidebarProps: {
311
+ type: Object,
312
+ default: () => ({}),
313
+ },
314
+ /** Whether the page is in an error state */
315
+ error: {
316
+ type: Boolean,
317
+ default: false,
318
+ },
319
+ /** Error message shown in error state */
320
+ errorMessage: {
321
+ type: String,
322
+ default: () => t('nextcloud-vue', 'An error occurred'),
323
+ },
324
+ /** Callback for retry button in error state. If null, no retry button is shown. */
325
+ onRetry: {
326
+ type: Function,
327
+ default: null,
328
+ },
329
+ /** Label for the retry button */
330
+ retryLabel: {
331
+ type: String,
332
+ default: () => t('nextcloud-vue', 'Retry'),
333
+ },
334
+ /** Whether the page has no data to show */
335
+ empty: {
336
+ type: Boolean,
337
+ default: false,
338
+ },
339
+ /** Message shown when page is empty */
340
+ emptyLabel: {
341
+ type: String,
342
+ default: () => t('nextcloud-vue', 'No data available'),
343
+ },
344
+ /** Title shown above the statistics table */
345
+ statsTitle: {
346
+ type: String,
347
+ default: '',
348
+ },
349
+ /**
350
+ * Column definitions for the statistics table.
351
+ * Each column: `{ key: string, label: string, align?: 'left'|'center'|'right' }`
352
+ *
353
+ * @type {Array<{ key: string, label: string, align?: string }>}
354
+ */
355
+ statsColumns: {
356
+ type: Array,
357
+ default: () => [],
358
+ },
359
+ /**
360
+ * Row data for the statistics table. Each row is an object keyed by
361
+ * column keys. Set `indent: true` on a row for sub-row styling.
362
+ *
363
+ * @type {Array<object>}
364
+ */
365
+ statsRows: {
366
+ type: Array,
367
+ default: () => [],
368
+ },
369
+ /** Maximum width of the page content */
370
+ maxWidth: {
371
+ type: String,
372
+ default: '1200px',
373
+ },
374
+ },
375
+
376
+ computed: {
377
+ /**
378
+ * Whether the sidebar is rendered externally (via objectSidebarState inject)
379
+ * rather than inline. When external, CnDetailPage only manages state —
380
+ * the parent App renders the actual NcAppSidebar.
381
+ */
382
+ hasExternalSidebar() {
383
+ return !!this.objectSidebarState
384
+ },
385
+ hasStats() {
386
+ return this.statsColumns.length > 0 && (this.statsRows.length > 0 || !!this.$slots['stats-rows'])
387
+ },
388
+ },
389
+
390
+ watch: {
391
+ sidebar: {
392
+ immediate: true,
393
+ handler() { this.syncSidebarState() },
394
+ },
395
+ title() { this.syncSidebarState() },
396
+ subtitle() { this.syncSidebarState() },
397
+ objectType() { this.syncSidebarState() },
398
+ objectId() { this.syncSidebarState() },
399
+ sidebarProps: {
400
+ deep: true,
401
+ handler() { this.syncSidebarState() },
402
+ },
403
+ },
404
+
405
+ beforeDestroy() {
406
+ if (this.hasExternalSidebar) {
407
+ this.objectSidebarState.active = false
408
+ }
409
+ },
410
+
411
+ methods: {
412
+ syncSidebarState() {
413
+ if (!this.hasExternalSidebar) return
414
+ if (this.sidebar && this.objectType && this.objectId) {
415
+ this.objectSidebarState.active = true
416
+ this.objectSidebarState.open = this.sidebarOpen
417
+ this.objectSidebarState.objectType = this.objectType
418
+ this.objectSidebarState.objectId = this.objectId
419
+ this.objectSidebarState.title = this.sidebarProps.title || this.title || ''
420
+ this.objectSidebarState.subtitle = this.sidebarProps.subtitle || this.subtitle || ''
421
+ this.objectSidebarState.register = this.sidebarProps.register || ''
422
+ this.objectSidebarState.schema = this.sidebarProps.schema || ''
423
+ this.objectSidebarState.hiddenTabs = this.sidebarProps.hiddenTabs || []
424
+ } else {
425
+ this.objectSidebarState.active = false
426
+ }
427
+ },
428
+ },
429
+ }
430
+ </script>
431
+
432
+ <!-- Styles in css/detail-page.css -->