@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
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
<span
|
|
3
3
|
class="cn-status-badge"
|
|
4
4
|
:class="badgeClasses">
|
|
5
|
-
<slot>
|
|
5
|
+
<slot>
|
|
6
|
+
<slot name="icon" />
|
|
7
|
+
{{ label }}
|
|
8
|
+
</slot>
|
|
6
9
|
</span>
|
|
7
10
|
</template>
|
|
8
11
|
|
|
@@ -46,6 +49,14 @@ export default {
|
|
|
46
49
|
default: 'medium',
|
|
47
50
|
validator: (v) => ['small', 'medium'].includes(v),
|
|
48
51
|
},
|
|
52
|
+
/**
|
|
53
|
+
* Use solid background with white text instead of light background with colored text.
|
|
54
|
+
* Useful when the badge is placed on a colored background (e.g., an active card).
|
|
55
|
+
*/
|
|
56
|
+
solid: {
|
|
57
|
+
type: Boolean,
|
|
58
|
+
default: false,
|
|
59
|
+
},
|
|
49
60
|
/**
|
|
50
61
|
* Map of label values to variants. When provided, the variant is resolved
|
|
51
62
|
* from this map using the label (case-insensitive). Falls back to the variant prop.
|
|
@@ -61,7 +72,8 @@ export default {
|
|
|
61
72
|
resolvedVariant() {
|
|
62
73
|
if (this.colorMap && this.label) {
|
|
63
74
|
const key = this.label.toLowerCase()
|
|
64
|
-
|
|
75
|
+
const normalizedColorMap = Object.fromEntries(Object.entries(this.colorMap).map(([k, v]) => [k.toLowerCase(), v]))
|
|
76
|
+
return normalizedColorMap[key] || this.variant
|
|
65
77
|
}
|
|
66
78
|
return this.variant
|
|
67
79
|
},
|
|
@@ -70,6 +82,7 @@ export default {
|
|
|
70
82
|
return {
|
|
71
83
|
['cn-status-badge--' + this.resolvedVariant]: true,
|
|
72
84
|
'cn-status-badge--small': this.size === 'small',
|
|
85
|
+
'cn-status-badge--solid': this.solid,
|
|
73
86
|
}
|
|
74
87
|
},
|
|
75
88
|
},
|
|
@@ -48,6 +48,9 @@
|
|
|
48
48
|
</BTab>
|
|
49
49
|
</BTabs>
|
|
50
50
|
</div>
|
|
51
|
+
|
|
52
|
+
<!-- Optional content below tabs (e.g. shared settings across all tabs) -->
|
|
53
|
+
<slot name="below-tabs" :loading="loading" />
|
|
51
54
|
</div>
|
|
52
55
|
|
|
53
56
|
<template #actions>
|
|
@@ -88,6 +91,7 @@
|
|
|
88
91
|
@click="executeConfirm">
|
|
89
92
|
<template #icon>
|
|
90
93
|
<NcLoadingIcon v-if="loading" :size="20" />
|
|
94
|
+
<slot v-else-if="$slots['confirm-icon']" name="confirm-icon" />
|
|
91
95
|
<Plus v-else-if="isCreateMode" :size="20" />
|
|
92
96
|
<ContentSaveOutline v-else :size="20" />
|
|
93
97
|
</template>
|
|
@@ -328,7 +332,7 @@ export default {
|
|
|
328
332
|
* the success message shows inline for 2 seconds, then clears and
|
|
329
333
|
* emits the `reset` event.
|
|
330
334
|
*
|
|
331
|
-
* @param {{ success?: boolean, error?: string }} resultData
|
|
335
|
+
* @param {{ success?: boolean, error?: string }} resultData - Result data to pass to the dialog
|
|
332
336
|
* @public
|
|
333
337
|
*/
|
|
334
338
|
setResult(resultData) {
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
CnTableWidget — Data table widget with card wrapper and dual data sourcing.
|
|
3
|
+
|
|
4
|
+
Wraps CnDataTable in a card container with a title header, optional "View all"
|
|
5
|
+
footer link, and loading/empty states. Supports two data modes:
|
|
6
|
+
1. External: `rows` prop provided (no API calls)
|
|
7
|
+
2. Self-fetch: `register` + `schemaId` provided (fetches from OpenRegister API)
|
|
8
|
+
|
|
9
|
+
Used in dashboard and detail page grid layouts for displaying related data tables.
|
|
10
|
+
-->
|
|
11
|
+
<template>
|
|
12
|
+
<div class="cn-table-widget">
|
|
13
|
+
<!-- Header -->
|
|
14
|
+
<div v-if="title" class="cn-table-widget__header">
|
|
15
|
+
<h3 class="cn-table-widget__title">
|
|
16
|
+
{{ title }}
|
|
17
|
+
</h3>
|
|
18
|
+
<span v-if="totalCount > 0" class="cn-table-widget__count">
|
|
19
|
+
{{ totalCount }}
|
|
20
|
+
</span>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<!-- Loading state -->
|
|
24
|
+
<NcLoadingIcon v-if="isLoading" class="cn-table-widget__loading" :size="32" />
|
|
25
|
+
|
|
26
|
+
<!-- Empty state -->
|
|
27
|
+
<p v-else-if="displayRows.length === 0" class="cn-table-widget__empty">
|
|
28
|
+
{{ emptyText }}
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+
<!-- Data table -->
|
|
32
|
+
<CnDataTable
|
|
33
|
+
v-else
|
|
34
|
+
:rows="displayRows"
|
|
35
|
+
:columns="columns"
|
|
36
|
+
:loading="false"
|
|
37
|
+
:selectable="false"
|
|
38
|
+
@row-click="onRowClick" />
|
|
39
|
+
|
|
40
|
+
<!-- Footer with view all link -->
|
|
41
|
+
<div
|
|
42
|
+
v-if="viewAllRoute && totalCount > limitedCount"
|
|
43
|
+
class="cn-table-widget__footer">
|
|
44
|
+
<a
|
|
45
|
+
class="cn-table-widget__view-all"
|
|
46
|
+
@click.prevent="$router.push(viewAllRoute)">
|
|
47
|
+
{{ viewAllLabel }}
|
|
48
|
+
</a>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<script>
|
|
54
|
+
import { NcLoadingIcon } from '@nextcloud/vue'
|
|
55
|
+
import CnDataTable from '../CnDataTable/CnDataTable.vue'
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* CnTableWidget — Data table widget with card wrapper and dual data sourcing.
|
|
59
|
+
*
|
|
60
|
+
* @example External data mode
|
|
61
|
+
* <CnTableWidget
|
|
62
|
+
* title="Related Skills"
|
|
63
|
+
* :rows="skillRows"
|
|
64
|
+
* :columns="skillColumns"
|
|
65
|
+
* :view-all-route="{ name: 'Skills' }" />
|
|
66
|
+
*
|
|
67
|
+
* @example Self-fetch mode
|
|
68
|
+
* <CnTableWidget
|
|
69
|
+
* title="Documents"
|
|
70
|
+
* register="9"
|
|
71
|
+
* schema-id="42"
|
|
72
|
+
* :limit="5" />
|
|
73
|
+
*/
|
|
74
|
+
export default {
|
|
75
|
+
name: 'CnTableWidget',
|
|
76
|
+
|
|
77
|
+
components: {
|
|
78
|
+
NcLoadingIcon,
|
|
79
|
+
CnDataTable,
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
props: {
|
|
83
|
+
/** Widget title shown in the header. */
|
|
84
|
+
title: {
|
|
85
|
+
type: String,
|
|
86
|
+
default: '',
|
|
87
|
+
},
|
|
88
|
+
/**
|
|
89
|
+
* External row data. When provided, no API calls are made.
|
|
90
|
+
*
|
|
91
|
+
* @type {object[]}
|
|
92
|
+
*/
|
|
93
|
+
rows: {
|
|
94
|
+
type: Array,
|
|
95
|
+
default: null,
|
|
96
|
+
},
|
|
97
|
+
/**
|
|
98
|
+
* Column definitions for CnDataTable.
|
|
99
|
+
*
|
|
100
|
+
* @type {{ key: string, label: string, sortable?: boolean }[]}
|
|
101
|
+
*/
|
|
102
|
+
columns: {
|
|
103
|
+
type: Array,
|
|
104
|
+
default: () => [],
|
|
105
|
+
},
|
|
106
|
+
/**
|
|
107
|
+
* OpenRegister register ID for self-fetch mode.
|
|
108
|
+
*
|
|
109
|
+
* @type {string|number}
|
|
110
|
+
*/
|
|
111
|
+
register: {
|
|
112
|
+
type: [String, Number],
|
|
113
|
+
default: null,
|
|
114
|
+
},
|
|
115
|
+
/**
|
|
116
|
+
* OpenRegister schema ID for self-fetch mode.
|
|
117
|
+
*
|
|
118
|
+
* @type {string|number}
|
|
119
|
+
*/
|
|
120
|
+
schemaId: {
|
|
121
|
+
type: [String, Number],
|
|
122
|
+
default: null,
|
|
123
|
+
},
|
|
124
|
+
/**
|
|
125
|
+
* Maximum number of rows to display. When total exceeds this,
|
|
126
|
+
* a "View all" link appears.
|
|
127
|
+
*
|
|
128
|
+
* @type {number}
|
|
129
|
+
*/
|
|
130
|
+
limit: {
|
|
131
|
+
type: Number,
|
|
132
|
+
default: 0,
|
|
133
|
+
},
|
|
134
|
+
/**
|
|
135
|
+
* Vue Router route for the "View all" link.
|
|
136
|
+
*
|
|
137
|
+
* @type {object}
|
|
138
|
+
*/
|
|
139
|
+
viewAllRoute: {
|
|
140
|
+
type: Object,
|
|
141
|
+
default: null,
|
|
142
|
+
},
|
|
143
|
+
/**
|
|
144
|
+
* Function that returns a route object for row click navigation.
|
|
145
|
+
* Receives the row data as argument.
|
|
146
|
+
*
|
|
147
|
+
* @type {Function}
|
|
148
|
+
*/
|
|
149
|
+
rowClickRoute: {
|
|
150
|
+
type: Function,
|
|
151
|
+
default: null,
|
|
152
|
+
},
|
|
153
|
+
/** Pre-translated "View all" label. */
|
|
154
|
+
viewAllLabel: {
|
|
155
|
+
type: String,
|
|
156
|
+
default: 'View all',
|
|
157
|
+
},
|
|
158
|
+
/** Pre-translated empty state text. */
|
|
159
|
+
emptyText: {
|
|
160
|
+
type: String,
|
|
161
|
+
default: 'No data available',
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
data() {
|
|
166
|
+
return {
|
|
167
|
+
fetchedRows: [],
|
|
168
|
+
loading: false,
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
computed: {
|
|
173
|
+
/**
|
|
174
|
+
* Whether data is currently loading.
|
|
175
|
+
*
|
|
176
|
+
* @return {boolean}
|
|
177
|
+
*/
|
|
178
|
+
isLoading() {
|
|
179
|
+
return this.loading
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* All rows (external or fetched).
|
|
184
|
+
*
|
|
185
|
+
* @return {object[]}
|
|
186
|
+
*/
|
|
187
|
+
allRows() {
|
|
188
|
+
return this.rows || this.fetchedRows
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Rows limited to the configured limit.
|
|
193
|
+
*
|
|
194
|
+
* @return {object[]}
|
|
195
|
+
*/
|
|
196
|
+
displayRows() {
|
|
197
|
+
if (this.limit > 0) {
|
|
198
|
+
return this.allRows.slice(0, this.limit)
|
|
199
|
+
}
|
|
200
|
+
return this.allRows
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Total count of all rows (before limiting).
|
|
205
|
+
*
|
|
206
|
+
* @return {number}
|
|
207
|
+
*/
|
|
208
|
+
totalCount() {
|
|
209
|
+
return this.allRows.length
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Count of displayed rows (after limiting).
|
|
214
|
+
*
|
|
215
|
+
* @return {number}
|
|
216
|
+
*/
|
|
217
|
+
limitedCount() {
|
|
218
|
+
return this.displayRows.length
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
mounted() {
|
|
223
|
+
if (this.rows === null && this.register && this.schemaId) {
|
|
224
|
+
this.fetchData()
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
methods: {
|
|
229
|
+
/**
|
|
230
|
+
* Fetch data from the OpenRegister API.
|
|
231
|
+
*
|
|
232
|
+
* @return {Promise<void>}
|
|
233
|
+
*/
|
|
234
|
+
async fetchData() {
|
|
235
|
+
this.loading = true
|
|
236
|
+
try {
|
|
237
|
+
const url = `/index.php/apps/openregister/api/objects/${this.register}/${this.schemaId}`
|
|
238
|
+
const response = await fetch(url, {
|
|
239
|
+
headers: {
|
|
240
|
+
'Content-Type': 'application/json',
|
|
241
|
+
'OCS-APIREQUEST': 'true',
|
|
242
|
+
},
|
|
243
|
+
})
|
|
244
|
+
if (response.ok) {
|
|
245
|
+
const data = await response.json()
|
|
246
|
+
this.fetchedRows = data.results || data || []
|
|
247
|
+
}
|
|
248
|
+
} catch (error) {
|
|
249
|
+
console.error('CnTableWidget: Failed to fetch data', error)
|
|
250
|
+
} finally {
|
|
251
|
+
this.loading = false
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Handle row click events. Navigates if rowClickRoute is configured.
|
|
257
|
+
*
|
|
258
|
+
* @param {object} row - The clicked row data.
|
|
259
|
+
*/
|
|
260
|
+
onRowClick(row) {
|
|
261
|
+
if (this.rowClickRoute) {
|
|
262
|
+
const route = this.rowClickRoute(row)
|
|
263
|
+
if (route) {
|
|
264
|
+
this.$router.push(route)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
</script>
|
|
271
|
+
|
|
272
|
+
<style scoped>
|
|
273
|
+
.cn-table-widget {
|
|
274
|
+
background: var(--color-main-background);
|
|
275
|
+
border: 1px solid var(--color-border);
|
|
276
|
+
border-radius: var(--border-radius-large, 16px);
|
|
277
|
+
overflow: hidden;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.cn-table-widget__header {
|
|
281
|
+
display: flex;
|
|
282
|
+
align-items: center;
|
|
283
|
+
justify-content: space-between;
|
|
284
|
+
padding: 12px 16px;
|
|
285
|
+
border-bottom: 1px solid var(--color-border);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.cn-table-widget__title {
|
|
289
|
+
margin: 0;
|
|
290
|
+
font-size: 14px;
|
|
291
|
+
font-weight: 600;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.cn-table-widget__count {
|
|
295
|
+
font-size: 12px;
|
|
296
|
+
color: var(--color-text-maxcontrast);
|
|
297
|
+
background: var(--color-background-dark);
|
|
298
|
+
padding: 2px 8px;
|
|
299
|
+
border-radius: 10px;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.cn-table-widget__loading {
|
|
303
|
+
padding: 32px 0;
|
|
304
|
+
display: flex;
|
|
305
|
+
justify-content: center;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.cn-table-widget__empty {
|
|
309
|
+
padding: 24px 16px;
|
|
310
|
+
text-align: center;
|
|
311
|
+
color: var(--color-text-maxcontrast);
|
|
312
|
+
font-size: 14px;
|
|
313
|
+
margin: 0;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.cn-table-widget__footer {
|
|
317
|
+
padding: 8px 16px;
|
|
318
|
+
border-top: 1px solid var(--color-border);
|
|
319
|
+
text-align: center;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.cn-table-widget__view-all {
|
|
323
|
+
font-size: 13px;
|
|
324
|
+
color: var(--color-primary-element);
|
|
325
|
+
cursor: pointer;
|
|
326
|
+
text-decoration: none;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.cn-table-widget__view-all:hover {
|
|
330
|
+
text-decoration: underline;
|
|
331
|
+
}
|
|
332
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as CnTableWidget } from './CnTableWidget.vue'
|
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
backgrounds, and padding.
|
|
7
7
|
-->
|
|
8
8
|
<template>
|
|
9
|
-
<div
|
|
9
|
+
<div
|
|
10
|
+
class="cn-widget-wrapper"
|
|
11
|
+
:class="{
|
|
12
|
+
'cn-widget-wrapper--borderless': borderless,
|
|
13
|
+
'cn-widget-wrapper--flush': flush,
|
|
14
|
+
}"
|
|
15
|
+
:style="wrapperStyles">
|
|
10
16
|
<!-- Header -->
|
|
11
17
|
<div v-if="showTitle" class="cn-widget-wrapper__header">
|
|
12
18
|
<div class="cn-widget-wrapper__header-left">
|
|
@@ -80,6 +86,22 @@ export default {
|
|
|
80
86
|
type: Boolean,
|
|
81
87
|
default: true,
|
|
82
88
|
},
|
|
89
|
+
/**
|
|
90
|
+
* Remove border and background — makes the wrapper transparent.
|
|
91
|
+
* Useful for widgets that are self-contained cards (e.g. CnStatsBlock).
|
|
92
|
+
*/
|
|
93
|
+
borderless: {
|
|
94
|
+
type: Boolean,
|
|
95
|
+
default: false,
|
|
96
|
+
},
|
|
97
|
+
/**
|
|
98
|
+
* Remove content padding — allows content to go edge-to-edge.
|
|
99
|
+
* Useful for list-style widgets where items should span the full width.
|
|
100
|
+
*/
|
|
101
|
+
flush: {
|
|
102
|
+
type: Boolean,
|
|
103
|
+
default: false,
|
|
104
|
+
},
|
|
83
105
|
/** Icon URL (image) */
|
|
84
106
|
iconUrl: {
|
|
85
107
|
type: String,
|
|
@@ -146,6 +168,19 @@ export default {
|
|
|
146
168
|
overflow: hidden;
|
|
147
169
|
}
|
|
148
170
|
|
|
171
|
+
.cn-widget-wrapper--borderless {
|
|
172
|
+
border: none;
|
|
173
|
+
background: transparent;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.cn-widget-wrapper--borderless .cn-widget-wrapper__content {
|
|
177
|
+
padding: 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.cn-widget-wrapper--flush .cn-widget-wrapper__content {
|
|
181
|
+
padding: 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
149
184
|
.cn-widget-wrapper__header {
|
|
150
185
|
display: flex;
|
|
151
186
|
align-items: center;
|
package/src/components/index.js
CHANGED
|
@@ -41,3 +41,14 @@ export { CnTimelineStages } from './CnTimelineStages/index.js'
|
|
|
41
41
|
export { CnUserActionMenu } from './CnUserActionMenu/index.js'
|
|
42
42
|
export { CnNotesCard } from './CnNotesCard/index.js'
|
|
43
43
|
export { CnTasksCard } from './CnTasksCard/index.js'
|
|
44
|
+
export { CnDetailCard } from './CnDetailCard/index.js'
|
|
45
|
+
export { CnCard } from './CnCard/index.js'
|
|
46
|
+
export { CnStatsPanel } from './CnStatsPanel/index.js'
|
|
47
|
+
export { CnJsonViewer } from './CnJsonViewer/index.js'
|
|
48
|
+
export { CnDetailGrid } from './CnDetailGrid/index.js'
|
|
49
|
+
export { CnProgressBar } from './CnProgressBar/index.js'
|
|
50
|
+
export { CnChartWidget } from './CnChartWidget/index.js'
|
|
51
|
+
export { CnObjectSidebar } from './CnObjectSidebar/index.js'
|
|
52
|
+
export { CnInfoWidget } from './CnInfoWidget/index.js'
|
|
53
|
+
export { CnTableWidget } from './CnTableWidget/index.js'
|
|
54
|
+
export { CnNoteCard } from './CnNoteCard/index.js'
|
|
@@ -1,22 +1,35 @@
|
|
|
1
1
|
import { ref, computed, onMounted } from 'vue'
|
|
2
2
|
import axios from '@nextcloud/axios'
|
|
3
3
|
import { generateOcsUrl } from '@nextcloud/router'
|
|
4
|
+
import { filterWidgetsByVisibility } from '../utils/widgetVisibility.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Composable for managing dashboard view state.
|
|
7
8
|
*
|
|
8
9
|
* Handles widget definition loading (including NC Dashboard API widgets),
|
|
9
|
-
* layout management,
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* layout management, edit mode, and role-based widget visibility filtering.
|
|
11
|
+
*
|
|
12
|
+
* Widgets can specify a `visibility` property to control which users see them:
|
|
13
|
+
* ```js
|
|
14
|
+
* {
|
|
15
|
+
* id: 'kcc-search',
|
|
16
|
+
* type: 'custom',
|
|
17
|
+
* title: 'Quick Search',
|
|
18
|
+
* visibility: {
|
|
19
|
+
* users: ['admin'], // specific user IDs (optional)
|
|
20
|
+
* groups: ['KCC', 'Admins'], // Nextcloud group names (optional)
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
* If `visibility` is not set or both arrays are empty, the widget is visible to everyone.
|
|
12
25
|
*
|
|
13
26
|
* @param {object} [options] Configuration options
|
|
14
|
-
* @param {Array} [options.widgets
|
|
15
|
-
* @param {Array} [options.defaultLayout
|
|
27
|
+
* @param {Array} [options.widgets] Static widget definitions from the app
|
|
28
|
+
* @param {Array} [options.defaultLayout] Default layout if no saved layout exists
|
|
16
29
|
* @param {Function} [options.loadLayout] Async function that returns saved layout array, or null
|
|
17
30
|
* @param {Function} [options.saveLayout] Async function that persists layout: (layout) => Promise
|
|
18
|
-
* @param {boolean} [options.includeNcWidgets
|
|
19
|
-
* @param {number} [options.columns
|
|
31
|
+
* @param {boolean} [options.includeNcWidgets] Whether to also load NC Dashboard API widgets
|
|
32
|
+
* @param {number} [options.columns] Grid columns
|
|
20
33
|
* @return {object} Reactive state and methods for CnDashboardPage
|
|
21
34
|
*
|
|
22
35
|
* @example Basic usage with static widgets
|
|
@@ -39,6 +52,16 @@ import { generateOcsUrl } from '@nextcloud/router'
|
|
|
39
52
|
* saveLayout: (layout) => fetch('/api/dashboard-layout', { method: 'PUT', body: JSON.stringify(layout) }),
|
|
40
53
|
* includeNcWidgets: true,
|
|
41
54
|
* })
|
|
55
|
+
*
|
|
56
|
+
* @example With role-based visibility
|
|
57
|
+
* const dashboard = useDashboardView({
|
|
58
|
+
* widgets: [
|
|
59
|
+
* { id: 'admin-panel', title: 'Admin Panel', type: 'custom', visibility: { groups: ['admin'] } },
|
|
60
|
+
* { id: 'kcc-search', title: 'KCC Search', type: 'custom', visibility: { groups: ['KCC'] } },
|
|
61
|
+
* { id: 'public-info', title: 'Info', type: 'custom' }, // visible to everyone
|
|
62
|
+
* ],
|
|
63
|
+
* defaultLayout: [...],
|
|
64
|
+
* })
|
|
42
65
|
*/
|
|
43
66
|
export function useDashboardView(options = {}) {
|
|
44
67
|
const opts = {
|
|
@@ -54,6 +77,8 @@ export function useDashboardView(options = {}) {
|
|
|
54
77
|
// ── State ────────────────────────────────────────────────────────────
|
|
55
78
|
const appWidgets = ref(opts.widgets)
|
|
56
79
|
const ncWidgets = ref([])
|
|
80
|
+
const visibleAppWidgets = ref([])
|
|
81
|
+
const visibleNcWidgets = ref([])
|
|
57
82
|
const layout = ref([])
|
|
58
83
|
const loading = ref(false)
|
|
59
84
|
const saving = ref(false)
|
|
@@ -61,9 +86,9 @@ export function useDashboardView(options = {}) {
|
|
|
61
86
|
|
|
62
87
|
// ── Computed ─────────────────────────────────────────────────────────
|
|
63
88
|
|
|
64
|
-
/** All available widgets (app + NC Dashboard API) */
|
|
89
|
+
/** All available widgets (app + NC Dashboard API), filtered by visibility */
|
|
65
90
|
const widgets = computed(() => {
|
|
66
|
-
return [...
|
|
91
|
+
return [...visibleAppWidgets.value, ...visibleNcWidgets.value]
|
|
67
92
|
})
|
|
68
93
|
|
|
69
94
|
/** Widget IDs currently on the dashboard */
|
|
@@ -78,6 +103,22 @@ export function useDashboardView(options = {}) {
|
|
|
78
103
|
|
|
79
104
|
// ── Methods ──────────────────────────────────────────────────────────
|
|
80
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Apply visibility filtering to the current widget sets and update
|
|
108
|
+
* the layout to remove items whose widgets are no longer visible.
|
|
109
|
+
*/
|
|
110
|
+
async function applyVisibilityFilter() {
|
|
111
|
+
visibleAppWidgets.value = await filterWidgetsByVisibility(appWidgets.value)
|
|
112
|
+
visibleNcWidgets.value = await filterWidgetsByVisibility(ncWidgets.value)
|
|
113
|
+
|
|
114
|
+
// Remove layout items that reference widgets the user cannot see
|
|
115
|
+
const visibleIds = new Set(widgets.value.map(w => w.id))
|
|
116
|
+
const filteredLayout = layout.value.filter(item => visibleIds.has(item.widgetId))
|
|
117
|
+
if (filteredLayout.length !== layout.value.length) {
|
|
118
|
+
layout.value = filteredLayout
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
81
122
|
/**
|
|
82
123
|
* Load NC Dashboard API widgets from the OCS endpoint.
|
|
83
124
|
*/
|
|
@@ -132,6 +173,9 @@ export function useDashboardView(options = {}) {
|
|
|
132
173
|
}
|
|
133
174
|
|
|
134
175
|
await Promise.all(tasks)
|
|
176
|
+
|
|
177
|
+
// Apply visibility filtering after all data is loaded
|
|
178
|
+
await applyVisibilityFilter()
|
|
135
179
|
} catch (error) {
|
|
136
180
|
console.error('[useDashboardView] Init failed:', error)
|
|
137
181
|
layout.value = [...opts.defaultLayout]
|
|
@@ -202,17 +246,19 @@ export function useDashboardView(options = {}) {
|
|
|
202
246
|
|
|
203
247
|
/**
|
|
204
248
|
* Update app widget definitions (e.g., when data changes).
|
|
249
|
+
* Re-applies visibility filtering after update.
|
|
205
250
|
*
|
|
206
251
|
* @param {Array} newWidgets Updated widget definitions
|
|
207
252
|
*/
|
|
208
|
-
function setWidgets(newWidgets) {
|
|
253
|
+
async function setWidgets(newWidgets) {
|
|
209
254
|
appWidgets.value = newWidgets
|
|
255
|
+
await applyVisibilityFilter()
|
|
210
256
|
}
|
|
211
257
|
|
|
212
258
|
// ── Lifecycle ────────────────────────────────────────────────────────
|
|
213
259
|
|
|
214
|
-
onMounted(() => {
|
|
215
|
-
init()
|
|
260
|
+
onMounted(async () => {
|
|
261
|
+
await init()
|
|
216
262
|
})
|
|
217
263
|
|
|
218
264
|
// ── Return ───────────────────────────────────────────────────────────
|
|
@@ -14,10 +14,11 @@ import { useObjectStore } from '../store/index.js'
|
|
|
14
14
|
* @param {string|object} [objectTypeOrOptions] Object type slug (new API) or legacy options object
|
|
15
15
|
* @param {string|import('vue').Ref<string>} [id] Object ID or `'new'` for a new object
|
|
16
16
|
* @param {object} [options] Options (new API only)
|
|
17
|
+
* @param {Function} [options.objectStore] Custom object store instance (from createObjectStore)
|
|
17
18
|
* @param {object|null} [options.router] Vue Router instance — enables post-save/delete navigation
|
|
18
19
|
* @param {string|null} [options.listRouteName] Route name to navigate to after successful delete
|
|
19
20
|
* @param {string|null} [options.detailRouteName] Route name to navigate to after successful create
|
|
20
|
-
* @param {string} [options.nameField
|
|
21
|
+
* @param {string} [options.nameField] Field shown in error messages
|
|
21
22
|
* @return {object} Reactive state and operation functions
|
|
22
23
|
*
|
|
23
24
|
* @example
|
|
@@ -54,7 +55,7 @@ export function useDetailView(objectTypeOrOptions, id, options) {
|
|
|
54
55
|
// Normalise `id` to a ref so we can watch it
|
|
55
56
|
const idRef = isRef(id) ? id : ref(id)
|
|
56
57
|
|
|
57
|
-
const objectStore = useObjectStore()
|
|
58
|
+
const objectStore = opts.objectStore ? opts.objectStore() : useObjectStore()
|
|
58
59
|
|
|
59
60
|
// ── State refs ───────────────────────────────────────────────────────
|
|
60
61
|
const editing = ref(false)
|
|
@@ -14,9 +14,10 @@ import { useObjectStore } from '../store/index.js'
|
|
|
14
14
|
*
|
|
15
15
|
* @param {string|object} [objectTypeOrOptions] Object type slug (new API) or legacy options object
|
|
16
16
|
* @param {object} [options] Options (new API only)
|
|
17
|
+
* @param {object|null} [options.objectStore] Custom object store instance (from createObjectStore). When provided, uses this store instead of the default useObjectStore(). Required when the app uses createObjectStore with a custom store ID.
|
|
17
18
|
* @param {object|null} [options.sidebarState] Sidebar state object from `inject('sidebarState')`. When provided, the composable wires and unwires the sidebar automatically on mount/unmount.
|
|
18
|
-
* @param {number} [options.defaultPageSize
|
|
19
|
-
* @param {number} [options.debounceMs
|
|
19
|
+
* @param {number} [options.defaultPageSize] Default `_limit` sent to the API
|
|
20
|
+
* @param {number} [options.debounceMs] Search debounce in milliseconds
|
|
20
21
|
* @param {object} [options.defaultSort] Default sort applied on mount e.g. `{ key: 'createdAt', order: 'desc' }`
|
|
21
22
|
* @return {object} Reactive state and event handlers
|
|
22
23
|
*
|
|
@@ -49,7 +50,7 @@ export function useListView(objectTypeOrOptions, options) {
|
|
|
49
50
|
const opts = options || {}
|
|
50
51
|
const sidebarState = opts.sidebarState || null
|
|
51
52
|
|
|
52
|
-
const objectStore = useObjectStore()
|
|
53
|
+
const objectStore = opts.objectStore || useObjectStore()
|
|
53
54
|
|
|
54
55
|
// ── State refs ───────────────────────────────────────────────────────
|
|
55
56
|
const schema = ref(null)
|
|
@@ -103,7 +104,7 @@ export function useListView(objectTypeOrOptions, options) {
|
|
|
103
104
|
/**
|
|
104
105
|
* Fetch the collection using current state params and update sidebar facet data.
|
|
105
106
|
*
|
|
106
|
-
* @param {number} [page
|
|
107
|
+
* @param {number} [page] Page to fetch
|
|
107
108
|
* @return {Promise<void>}
|
|
108
109
|
*/
|
|
109
110
|
async function refresh(page = 1) {
|
|
@@ -256,8 +257,8 @@ export function useListView(objectTypeOrOptions, options) {
|
|
|
256
257
|
* @param {object} options Legacy options object
|
|
257
258
|
* @param {string} [options.objectType] The registered object type slug
|
|
258
259
|
* @param {Function} [options.fetchFn] Function to call: (type, params) => Promise<Array>
|
|
259
|
-
* @param {number} [options.debounceMs
|
|
260
|
-
* @param {number} [options.pageSize
|
|
260
|
+
* @param {number} [options.debounceMs] Search debounce in milliseconds
|
|
261
|
+
* @param {number} [options.pageSize] Default page size
|
|
261
262
|
* @param {object} [options.defaultSort] Default sort: { key: string, order: 'asc'|'desc' }
|
|
262
263
|
* @return {object} Reactive state and methods
|
|
263
264
|
*/
|