@conduction/nextcloud-vue 0.1.0-beta.6 → 0.1.0-beta.8
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.
- package/dist/nextcloud-vue.cjs.js +13575 -2374
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.css +1238 -270
- package/dist/nextcloud-vue.esm.js +13517 -2336
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/package.json +11 -7
- package/src/components/CnActionsBar/CnActionsBar.vue +20 -2
- package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +1 -11
- package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +5 -1
- package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +1 -1
- package/src/components/CnCard/CnCard.vue +415 -0
- package/src/components/CnCard/index.js +1 -0
- package/src/components/CnCardGrid/CnCardGrid.vue +20 -20
- package/src/components/CnChartWidget/CnChartWidget.vue +3 -1
- package/src/components/CnCopyDialog/CnCopyDialog.vue +7 -1
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +4 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +2 -0
- package/src/components/CnDataTable/CnDataTable.vue +6 -2
- package/src/components/CnDeleteDialog/CnDeleteDialog.vue +7 -1
- package/src/components/CnDetailCard/CnDetailCard.vue +12 -1
- package/src/components/CnDetailGrid/CnDetailGrid.vue +254 -0
- package/src/components/CnDetailGrid/index.js +1 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +157 -11
- package/src/components/CnFacetSidebar/CnFacetSidebar.vue +3 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +934 -920
- package/src/components/CnIcon/CnIcon.vue +1 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +63 -9
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +37 -9
- package/src/components/CnInfoWidget/CnInfoWidget.vue +219 -0
- package/src/components/CnInfoWidget/index.js +1 -0
- package/src/components/CnJsonViewer/CnJsonViewer.vue +283 -0
- package/src/components/CnJsonViewer/index.js +1 -0
- package/src/components/CnKpiGrid/CnKpiGrid.vue +5 -1
- package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +7 -1
- package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +7 -1
- package/src/components/CnMassExportDialog/CnMassExportDialog.vue +1 -1
- package/src/components/CnMassImportDialog/CnMassImportDialog.vue +1 -1
- package/src/components/CnObjectCard/CnObjectCard.vue +1 -1
- package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +368 -0
- package/src/components/CnObjectSidebar/CnFilesTab.vue +286 -0
- package/src/components/CnObjectSidebar/CnNotesTab.vue +249 -0
- package/src/components/CnObjectSidebar/CnObjectSidebar.vue +45 -668
- package/src/components/CnObjectSidebar/CnTagsTab.vue +258 -0
- package/src/components/CnObjectSidebar/CnTasksTab.vue +482 -0
- package/src/components/CnObjectSidebar/index.js +5 -0
- package/src/components/CnProgressBar/CnProgressBar.vue +262 -0
- package/src/components/CnProgressBar/index.js +1 -0
- package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +1 -1
- package/src/components/CnStatsBlock/CnStatsBlock.vue +27 -11
- package/src/components/CnStatsPanel/CnStatsPanel.vue +320 -0
- package/src/components/CnStatsPanel/index.js +1 -0
- package/src/components/CnStatusBadge/CnStatusBadge.vue +15 -2
- package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +5 -1
- package/src/components/CnTableWidget/CnTableWidget.vue +332 -0
- package/src/components/CnTableWidget/index.js +1 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +36 -1
- package/src/components/index.js +11 -0
- package/src/composables/useDashboardView.js +58 -12
- package/src/composables/useDetailView.js +3 -2
- package/src/composables/useListView.js +7 -6
- package/src/composables/useSubResource.js +3 -3
- package/src/css/badge.css +32 -0
- package/src/css/card.css +1 -0
- package/src/css/detail-page.css +74 -7
- package/src/index.js +16 -0
- package/src/mixins/gridLayout.js +118 -0
- package/src/store/createCrudStore.js +360 -0
- package/src/store/createSubResourcePlugin.js +5 -15
- package/src/store/index.js +1 -0
- package/src/store/plugins/auditTrails.js +346 -6
- package/src/store/plugins/lifecycle.js +4 -4
- package/src/store/plugins/registerMapping.js +18 -8
- package/src/store/plugins/relations.js +1 -1
- package/src/store/plugins/search.js +21 -8
- package/src/store/useObjectStore.js +30 -36
- package/src/utils/getTheme.js +9 -0
- package/src/utils/headers.js +13 -3
- package/src/utils/index.js +1 -0
- package/src/utils/schema.js +3 -3
- package/src/utils/widgetVisibility.js +162 -0
- package/src/components/CnObjectCard/eslint-setup.md +0 -235
- package/src/components/CnObjectCard/package.json-or.json +0 -132
|
@@ -337,9 +337,13 @@ export default {
|
|
|
337
337
|
|
|
338
338
|
toggleSelectAll() {
|
|
339
339
|
if (this.allSelected) {
|
|
340
|
-
|
|
340
|
+
// Remove only current page IDs, preserving cross-page selections
|
|
341
|
+
const currentPageIds = new Set(this.rows.map((row) => row[this.rowKey]))
|
|
342
|
+
this.$emit('select', this.selectedIds.filter((id) => !currentPageIds.has(id)))
|
|
341
343
|
} else {
|
|
342
|
-
|
|
344
|
+
// Add current page IDs to existing selections
|
|
345
|
+
const merged = new Set([...this.selectedIds, ...this.rows.map((row) => row[this.rowKey])])
|
|
346
|
+
this.$emit('select', [...merged])
|
|
343
347
|
}
|
|
344
348
|
/** @event select-all Emitted when select-all checkbox is toggled. */
|
|
345
349
|
this.$emit('select-all', !this.allSelected)
|
|
@@ -91,6 +91,11 @@ export default {
|
|
|
91
91
|
type: String,
|
|
92
92
|
default: 'title',
|
|
93
93
|
},
|
|
94
|
+
/** Optional function to format the item name. Receives the item, returns a string. Overrides nameField when provided. */
|
|
95
|
+
nameFormatter: {
|
|
96
|
+
type: Function,
|
|
97
|
+
default: null,
|
|
98
|
+
},
|
|
94
99
|
/** Dialog title */
|
|
95
100
|
dialogTitle: {
|
|
96
101
|
type: String,
|
|
@@ -121,6 +126,7 @@ export default {
|
|
|
121
126
|
|
|
122
127
|
computed: {
|
|
123
128
|
itemName() {
|
|
129
|
+
if (this.nameFormatter) return this.nameFormatter(this.item)
|
|
124
130
|
return this.item[this.nameField] || this.item.name || this.item.title || this.item.id
|
|
125
131
|
},
|
|
126
132
|
resolvedWarningText() {
|
|
@@ -147,7 +153,7 @@ export default {
|
|
|
147
153
|
* Set the result of the delete operation. Call this from the parent
|
|
148
154
|
* after the API call completes.
|
|
149
155
|
*
|
|
150
|
-
* @param {{ success?: boolean, error?: string }} resultData
|
|
156
|
+
* @param {{ success?: boolean, error?: string }} resultData - Result data to pass to the dialog
|
|
151
157
|
* @public
|
|
152
158
|
*/
|
|
153
159
|
setResult(resultData) {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
</div>
|
|
39
39
|
|
|
40
40
|
<!-- Content -->
|
|
41
|
-
<div v-show="!isCollapsed" class="cn-detail-card__content">
|
|
41
|
+
<div v-show="!isCollapsed" class="cn-detail-card__content" :class="{ 'cn-detail-card__content--flush': flush }">
|
|
42
42
|
<slot />
|
|
43
43
|
</div>
|
|
44
44
|
|
|
@@ -101,6 +101,13 @@ export default {
|
|
|
101
101
|
type: Boolean,
|
|
102
102
|
default: false,
|
|
103
103
|
},
|
|
104
|
+
/**
|
|
105
|
+
* Remove content padding — allows tables and lists to go edge-to-edge.
|
|
106
|
+
*/
|
|
107
|
+
flush: {
|
|
108
|
+
type: Boolean,
|
|
109
|
+
default: false,
|
|
110
|
+
},
|
|
104
111
|
},
|
|
105
112
|
|
|
106
113
|
data() {
|
|
@@ -207,6 +214,10 @@ export default {
|
|
|
207
214
|
padding: 16px;
|
|
208
215
|
}
|
|
209
216
|
|
|
217
|
+
.cn-detail-card__content--flush {
|
|
218
|
+
padding: 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
210
221
|
.cn-detail-card__footer {
|
|
211
222
|
padding: 8px 16px;
|
|
212
223
|
border-top: 1px solid var(--color-border);
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
CnDetailGrid — Data-driven label-value grid for detail/info sections.
|
|
3
|
+
|
|
4
|
+
Supports two layout modes:
|
|
5
|
+
- grid (default): Responsive card grid with label stacked above value
|
|
6
|
+
- horizontal: Vertical list of rows with label on left, value on right
|
|
7
|
+
|
|
8
|
+
Items can be data-driven via the `items` prop, or customized per-item
|
|
9
|
+
via named scoped slots (#item-{index}, #label-{index}, #item-actions-{index}).
|
|
10
|
+
-->
|
|
11
|
+
<template>
|
|
12
|
+
<div
|
|
13
|
+
class="cn-detail-grid"
|
|
14
|
+
:class="rootClasses"
|
|
15
|
+
:style="rootStyles">
|
|
16
|
+
<!-- Empty state -->
|
|
17
|
+
<div v-if="!items.length && !$scopedSlots.default" class="cn-detail-grid__empty">
|
|
18
|
+
<slot name="empty">
|
|
19
|
+
{{ emptyLabel }}
|
|
20
|
+
</slot>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<!-- Data-driven items -->
|
|
24
|
+
<div
|
|
25
|
+
v-for="(item, index) in items"
|
|
26
|
+
:key="index"
|
|
27
|
+
class="cn-detail-grid__item"
|
|
28
|
+
:class="itemClasses">
|
|
29
|
+
<!-- Label -->
|
|
30
|
+
<div class="cn-detail-grid__label">
|
|
31
|
+
<slot :name="'label-' + index" :item="item" :index="index">
|
|
32
|
+
{{ item.label }}
|
|
33
|
+
</slot>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<!-- Value -->
|
|
37
|
+
<div class="cn-detail-grid__value">
|
|
38
|
+
<slot :name="'item-' + index" :item="item" :index="index">
|
|
39
|
+
{{ item.value !== undefined && item.value !== null ? item.value : '-' }}
|
|
40
|
+
</slot>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<!-- Optional per-item actions -->
|
|
44
|
+
<div v-if="$scopedSlots['item-actions-' + index]" class="cn-detail-grid__actions">
|
|
45
|
+
<slot :name="'item-actions-' + index" :item="item" :index="index" />
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<!-- Append slot for manual items -->
|
|
50
|
+
<slot />
|
|
51
|
+
</div>
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<script>
|
|
55
|
+
/**
|
|
56
|
+
* CnDetailGrid — Data-driven label-value grid for detail/info sections.
|
|
57
|
+
*
|
|
58
|
+
* @example Simple data-driven grid
|
|
59
|
+
* <CnDetailGrid :items="[
|
|
60
|
+
* { label: 'ID', value: '12345' },
|
|
61
|
+
* { label: 'Status', value: 'Active' },
|
|
62
|
+
* { label: 'Created', value: '2024-01-15' },
|
|
63
|
+
* ]" />
|
|
64
|
+
*
|
|
65
|
+
* @example Grid with custom slot content
|
|
66
|
+
* <CnDetailGrid :items="[
|
|
67
|
+
* { label: 'ID', value: item.id },
|
|
68
|
+
* { label: 'Action' },
|
|
69
|
+
* ]">
|
|
70
|
+
* <template #item-1>
|
|
71
|
+
* <CnStatusBadge :label="item.action" />
|
|
72
|
+
* </template>
|
|
73
|
+
* </CnDetailGrid>
|
|
74
|
+
*
|
|
75
|
+
* @example Horizontal row layout
|
|
76
|
+
* <CnDetailGrid layout="horizontal" :items="fields" />
|
|
77
|
+
*/
|
|
78
|
+
export default {
|
|
79
|
+
name: 'CnDetailGrid',
|
|
80
|
+
|
|
81
|
+
props: {
|
|
82
|
+
/**
|
|
83
|
+
* Array of detail items to render.
|
|
84
|
+
* @type {Array<{ label: string, value?: string|number }>}
|
|
85
|
+
*/
|
|
86
|
+
items: {
|
|
87
|
+
type: Array,
|
|
88
|
+
default: () => [],
|
|
89
|
+
},
|
|
90
|
+
/**
|
|
91
|
+
* Layout mode.
|
|
92
|
+
* - 'grid': Responsive card grid, label stacked above value
|
|
93
|
+
* - 'horizontal': Vertical list of rows, label on left, value on right
|
|
94
|
+
*/
|
|
95
|
+
layout: {
|
|
96
|
+
type: String,
|
|
97
|
+
default: 'grid',
|
|
98
|
+
validator: (v) => ['grid', 'horizontal'].includes(v),
|
|
99
|
+
},
|
|
100
|
+
/**
|
|
101
|
+
* Number of fixed grid columns. Set to 0 (default) for responsive auto-fit.
|
|
102
|
+
* Only applies to layout="grid".
|
|
103
|
+
*/
|
|
104
|
+
columns: {
|
|
105
|
+
type: Number,
|
|
106
|
+
default: 0,
|
|
107
|
+
},
|
|
108
|
+
/**
|
|
109
|
+
* Minimum width (px) for auto-fit grid items.
|
|
110
|
+
* Only applies when columns is 0 and layout is 'grid'.
|
|
111
|
+
*/
|
|
112
|
+
minItemWidth: {
|
|
113
|
+
type: Number,
|
|
114
|
+
default: 250,
|
|
115
|
+
},
|
|
116
|
+
/**
|
|
117
|
+
* Minimum width (px) for labels in horizontal mode.
|
|
118
|
+
*/
|
|
119
|
+
labelWidth: {
|
|
120
|
+
type: Number,
|
|
121
|
+
default: 150,
|
|
122
|
+
},
|
|
123
|
+
/**
|
|
124
|
+
* Whether to show the left accent border on items.
|
|
125
|
+
*/
|
|
126
|
+
accent: {
|
|
127
|
+
type: Boolean,
|
|
128
|
+
default: true,
|
|
129
|
+
},
|
|
130
|
+
/**
|
|
131
|
+
* Text shown when the items array is empty.
|
|
132
|
+
*/
|
|
133
|
+
emptyLabel: {
|
|
134
|
+
type: String,
|
|
135
|
+
default: 'No details available',
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
computed: {
|
|
140
|
+
rootClasses() {
|
|
141
|
+
return {
|
|
142
|
+
'cn-detail-grid--grid': this.layout === 'grid',
|
|
143
|
+
'cn-detail-grid--horizontal': this.layout === 'horizontal',
|
|
144
|
+
'cn-detail-grid--accent': this.accent,
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
rootStyles() {
|
|
148
|
+
if (this.layout === 'grid') {
|
|
149
|
+
if (this.columns > 0) {
|
|
150
|
+
return { 'grid-template-columns': `repeat(${this.columns}, 1fr)` }
|
|
151
|
+
}
|
|
152
|
+
return { 'grid-template-columns': `repeat(auto-fit, minmax(${this.minItemWidth}px, 1fr))` }
|
|
153
|
+
}
|
|
154
|
+
if (this.layout === 'horizontal') {
|
|
155
|
+
return { '--cn-detail-grid-label-width': this.labelWidth + 'px' }
|
|
156
|
+
}
|
|
157
|
+
return {}
|
|
158
|
+
},
|
|
159
|
+
itemClasses() {
|
|
160
|
+
return {
|
|
161
|
+
'cn-detail-grid__item--horizontal': this.layout === 'horizontal',
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
</script>
|
|
167
|
+
|
|
168
|
+
<style scoped>
|
|
169
|
+
/* ===== Grid layout (default) ===== */
|
|
170
|
+
.cn-detail-grid--grid {
|
|
171
|
+
display: grid;
|
|
172
|
+
gap: calc(4 * var(--default-grid-baseline, 4px));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* ===== Horizontal layout ===== */
|
|
176
|
+
.cn-detail-grid--horizontal {
|
|
177
|
+
display: flex;
|
|
178
|
+
flex-direction: column;
|
|
179
|
+
gap: calc(3 * var(--default-grid-baseline, 4px));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* ===== Item (card style) ===== */
|
|
183
|
+
.cn-detail-grid__item {
|
|
184
|
+
display: flex;
|
|
185
|
+
flex-direction: column;
|
|
186
|
+
gap: var(--default-grid-baseline, 4px);
|
|
187
|
+
padding: calc(2 * var(--default-grid-baseline, 4px)) calc(3 * var(--default-grid-baseline, 4px));
|
|
188
|
+
background: var(--color-background-hover);
|
|
189
|
+
border-radius: 0 var(--border-radius) var(--border-radius) 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* Accent border */
|
|
193
|
+
.cn-detail-grid--accent .cn-detail-grid__item {
|
|
194
|
+
border-left: 3px solid var(--color-primary-element);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Horizontal item: row direction */
|
|
198
|
+
.cn-detail-grid__item--horizontal {
|
|
199
|
+
flex-direction: row;
|
|
200
|
+
align-items: center;
|
|
201
|
+
gap: calc(4 * var(--default-grid-baseline, 4px));
|
|
202
|
+
border-radius: var(--border-radius);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* ===== Label ===== */
|
|
206
|
+
.cn-detail-grid__label {
|
|
207
|
+
font-size: 0.85em;
|
|
208
|
+
color: var(--color-text-maxcontrast);
|
|
209
|
+
font-weight: 500;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.cn-detail-grid--horizontal .cn-detail-grid__label {
|
|
213
|
+
min-width: var(--cn-detail-grid-label-width, 150px);
|
|
214
|
+
flex-shrink: 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* ===== Value ===== */
|
|
218
|
+
.cn-detail-grid__value {
|
|
219
|
+
font-size: 1em;
|
|
220
|
+
color: var(--color-main-text);
|
|
221
|
+
word-break: break-word;
|
|
222
|
+
margin: 0.5rem;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.cn-detail-grid--horizontal .cn-detail-grid__value {
|
|
226
|
+
flex: 1;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* ===== Actions ===== */
|
|
230
|
+
.cn-detail-grid__actions {
|
|
231
|
+
flex-shrink: 0;
|
|
232
|
+
display: flex;
|
|
233
|
+
align-items: center;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* ===== Empty state ===== */
|
|
237
|
+
.cn-detail-grid__empty {
|
|
238
|
+
color: var(--color-text-maxcontrast);
|
|
239
|
+
font-style: italic;
|
|
240
|
+
padding: calc(2 * var(--default-grid-baseline, 4px));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* ===== Responsive ===== */
|
|
244
|
+
@media (max-width: 600px) {
|
|
245
|
+
.cn-detail-grid--grid {
|
|
246
|
+
grid-template-columns: 1fr !important;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.cn-detail-grid__item--horizontal {
|
|
250
|
+
flex-direction: column;
|
|
251
|
+
align-items: flex-start;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as CnDetailGrid } from './CnDetailGrid.vue'
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
CnDetailPage — Generic detail/overview page.
|
|
3
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
|
+
|
|
4
16
|
A simpler alternative to CnIndexPage for detail, stats, and overview pages.
|
|
5
17
|
No multi-object table, no CRUD dialogs — just a clean layout with:
|
|
6
18
|
- Header (title, description, icon, action buttons)
|
|
@@ -76,6 +88,27 @@
|
|
|
76
88
|
|
|
77
89
|
<!-- Main content -->
|
|
78
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
|
+
|
|
79
112
|
<!-- Statistics table -->
|
|
80
113
|
<div v-if="hasStats" class="cn-detail-page__stats">
|
|
81
114
|
<slot name="stats-header">
|
|
@@ -103,20 +136,23 @@
|
|
|
103
136
|
</table>
|
|
104
137
|
</div>
|
|
105
138
|
|
|
106
|
-
<!-- Default
|
|
107
|
-
<div class="cn-detail-page__content">
|
|
108
|
-
|
|
109
|
-
|
|
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>
|
|
110
145
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
146
|
+
<!-- Sections slot — additional content below stats -->
|
|
147
|
+
<div v-if="$slots.sections" class="cn-detail-page__sections">
|
|
148
|
+
<slot name="sections" />
|
|
149
|
+
</div>
|
|
114
150
|
</div>
|
|
115
|
-
</div>
|
|
116
151
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
152
|
+
<!-- Footer -->
|
|
153
|
+
<div v-if="$slots.footer" class="cn-detail-page__footer">
|
|
154
|
+
<slot name="footer" />
|
|
155
|
+
</div>
|
|
120
156
|
</div>
|
|
121
157
|
</div>
|
|
122
158
|
</template>
|
|
@@ -126,11 +162,19 @@ import { NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'
|
|
|
126
162
|
import { CnIcon } from '../CnIcon/index.js'
|
|
127
163
|
import AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'
|
|
128
164
|
import InformationOutline from 'vue-material-design-icons/InformationOutline.vue'
|
|
165
|
+
import { gridLayout } from '../../mixins/gridLayout.js'
|
|
129
166
|
import Refresh from 'vue-material-design-icons/Refresh.vue'
|
|
130
167
|
|
|
131
168
|
/**
|
|
132
169
|
* CnDetailPage — Generic detail/overview page.
|
|
133
170
|
*
|
|
171
|
+
* Supports two layout modes:
|
|
172
|
+
* 1. **Default (vertical stacking):** Content provided via default slot, cards stack vertically.
|
|
173
|
+
* 2. **Grid layout:** When `layout` and `widgets` props are provided, content renders in a
|
|
174
|
+
* 12-column CSS grid with `#widget-{widgetId}` scoped slots. Same API as CnDashboardPage.
|
|
175
|
+
*
|
|
176
|
+
* @example Basic usage (vertical stacking)
|
|
177
|
+
*
|
|
134
178
|
* A simpler alternative to CnIndexPage for pages that display detail info,
|
|
135
179
|
* statistics, charts, or card grids — without multi-object tables or CRUD
|
|
136
180
|
* dialogs. Provides a consistent layout with header, loading/error/empty
|
|
@@ -156,6 +200,24 @@ import Refresh from 'vue-material-design-icons/Refresh.vue'
|
|
|
156
200
|
* <SchemaCards :schemas="schemas" />
|
|
157
201
|
* </CnDetailPage>
|
|
158
202
|
*
|
|
203
|
+
* @example Grid layout mode
|
|
204
|
+
* <CnDetailPage
|
|
205
|
+
* title="Character Detail"
|
|
206
|
+
* :layout="[
|
|
207
|
+
* { id: 1, widgetId: 'info', gridX: 0, gridY: 0, gridWidth: 8 },
|
|
208
|
+
* { id: 2, widgetId: 'stats', gridX: 8, gridY: 0, gridWidth: 4 },
|
|
209
|
+
* ]"
|
|
210
|
+
* :widgets="[
|
|
211
|
+
* { id: 'info', title: 'Character Info' },
|
|
212
|
+
* { id: 'stats', title: 'Statistics' },
|
|
213
|
+
* ]">
|
|
214
|
+
* <template #widget-info="{ item, widget }">
|
|
215
|
+
* <CharacterInfoCard :character="character" />
|
|
216
|
+
* </template>
|
|
217
|
+
* <template #widget-stats="{ item, widget }">
|
|
218
|
+
* <StatsCard :stats="character.stats" />
|
|
219
|
+
* </template>
|
|
220
|
+
*
|
|
159
221
|
* @example With header actions and error handling
|
|
160
222
|
* <CnDetailPage
|
|
161
223
|
* title="Schema Details"
|
|
@@ -181,6 +243,12 @@ export default {
|
|
|
181
243
|
Refresh,
|
|
182
244
|
},
|
|
183
245
|
|
|
246
|
+
mixins: [gridLayout],
|
|
247
|
+
|
|
248
|
+
inject: {
|
|
249
|
+
objectSidebarState: { default: null },
|
|
250
|
+
},
|
|
251
|
+
|
|
184
252
|
props: {
|
|
185
253
|
/** Page title */
|
|
186
254
|
title: {
|
|
@@ -212,6 +280,36 @@ export default {
|
|
|
212
280
|
type: String,
|
|
213
281
|
default: 'Loading...',
|
|
214
282
|
},
|
|
283
|
+
/** Whether to activate the external sidebar (via objectSidebarState inject) */
|
|
284
|
+
sidebar: {
|
|
285
|
+
type: Boolean,
|
|
286
|
+
default: false,
|
|
287
|
+
},
|
|
288
|
+
/** Whether the sidebar is open (expanded) */
|
|
289
|
+
sidebarOpen: {
|
|
290
|
+
type: Boolean,
|
|
291
|
+
default: true,
|
|
292
|
+
},
|
|
293
|
+
/** The registered object type slug for the sidebar */
|
|
294
|
+
objectType: {
|
|
295
|
+
type: String,
|
|
296
|
+
default: '',
|
|
297
|
+
},
|
|
298
|
+
/** The object ID to display in the sidebar */
|
|
299
|
+
objectId: {
|
|
300
|
+
type: [String, Number],
|
|
301
|
+
default: '',
|
|
302
|
+
},
|
|
303
|
+
/** Subtitle shown in the sidebar header */
|
|
304
|
+
subtitle: {
|
|
305
|
+
type: String,
|
|
306
|
+
default: '',
|
|
307
|
+
},
|
|
308
|
+
/** Additional sidebar configuration (register, schema, hiddenTabs, title, subtitle) */
|
|
309
|
+
sidebarProps: {
|
|
310
|
+
type: Object,
|
|
311
|
+
default: () => ({}),
|
|
312
|
+
},
|
|
215
313
|
/** Whether the page is in an error state */
|
|
216
314
|
error: {
|
|
217
315
|
type: Boolean,
|
|
@@ -275,10 +373,58 @@ export default {
|
|
|
275
373
|
},
|
|
276
374
|
|
|
277
375
|
computed: {
|
|
376
|
+
/**
|
|
377
|
+
* Whether the sidebar is rendered externally (via objectSidebarState inject)
|
|
378
|
+
* rather than inline. When external, CnDetailPage only manages state —
|
|
379
|
+
* the parent App renders the actual NcAppSidebar.
|
|
380
|
+
*/
|
|
381
|
+
hasExternalSidebar() {
|
|
382
|
+
return !!this.objectSidebarState
|
|
383
|
+
},
|
|
278
384
|
hasStats() {
|
|
279
385
|
return this.statsColumns.length > 0 && (this.statsRows.length > 0 || !!this.$slots['stats-rows'])
|
|
280
386
|
},
|
|
281
387
|
},
|
|
388
|
+
|
|
389
|
+
watch: {
|
|
390
|
+
sidebar: {
|
|
391
|
+
immediate: true,
|
|
392
|
+
handler() { this.syncSidebarState() },
|
|
393
|
+
},
|
|
394
|
+
title() { this.syncSidebarState() },
|
|
395
|
+
subtitle() { this.syncSidebarState() },
|
|
396
|
+
objectType() { this.syncSidebarState() },
|
|
397
|
+
objectId() { this.syncSidebarState() },
|
|
398
|
+
sidebarProps: {
|
|
399
|
+
deep: true,
|
|
400
|
+
handler() { this.syncSidebarState() },
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
beforeDestroy() {
|
|
405
|
+
if (this.hasExternalSidebar) {
|
|
406
|
+
this.objectSidebarState.active = false
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
|
|
410
|
+
methods: {
|
|
411
|
+
syncSidebarState() {
|
|
412
|
+
if (!this.hasExternalSidebar) return
|
|
413
|
+
if (this.sidebar && this.objectType && this.objectId) {
|
|
414
|
+
this.objectSidebarState.active = true
|
|
415
|
+
this.objectSidebarState.open = this.sidebarOpen
|
|
416
|
+
this.objectSidebarState.objectType = this.objectType
|
|
417
|
+
this.objectSidebarState.objectId = this.objectId
|
|
418
|
+
this.objectSidebarState.title = this.sidebarProps.title || this.title || ''
|
|
419
|
+
this.objectSidebarState.subtitle = this.sidebarProps.subtitle || this.subtitle || ''
|
|
420
|
+
this.objectSidebarState.register = this.sidebarProps.register || ''
|
|
421
|
+
this.objectSidebarState.schema = this.sidebarProps.schema || ''
|
|
422
|
+
this.objectSidebarState.hiddenTabs = this.sidebarProps.hiddenTabs || []
|
|
423
|
+
} else {
|
|
424
|
+
this.objectSidebarState.active = false
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
},
|
|
282
428
|
}
|
|
283
429
|
</script>
|
|
284
430
|
|