@conduction/nextcloud-vue 0.1.0-beta.6 → 0.1.0-beta.7

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 (82) hide show
  1. package/dist/nextcloud-vue.cjs.js +13606 -1918
  2. package/dist/nextcloud-vue.cjs.js.map +1 -1
  3. package/dist/nextcloud-vue.css +1238 -270
  4. package/dist/nextcloud-vue.esm.js +13548 -1880
  5. package/dist/nextcloud-vue.esm.js.map +1 -1
  6. package/package.json +9 -4
  7. package/src/components/CnActionsBar/CnActionsBar.vue +6 -1
  8. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +1 -11
  9. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +5 -1
  10. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +1 -1
  11. package/src/components/CnCard/CnCard.vue +415 -0
  12. package/src/components/CnCard/index.js +1 -0
  13. package/src/components/CnCardGrid/CnCardGrid.vue +20 -20
  14. package/src/components/CnChartWidget/CnChartWidget.vue +3 -1
  15. package/src/components/CnCopyDialog/CnCopyDialog.vue +7 -1
  16. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +4 -0
  17. package/src/components/CnDashboardPage/CnDashboardPage.vue +2 -0
  18. package/src/components/CnDataTable/CnDataTable.vue +6 -2
  19. package/src/components/CnDeleteDialog/CnDeleteDialog.vue +7 -1
  20. package/src/components/CnDetailCard/CnDetailCard.vue +12 -1
  21. package/src/components/CnDetailGrid/CnDetailGrid.vue +254 -0
  22. package/src/components/CnDetailGrid/index.js +1 -0
  23. package/src/components/CnDetailPage/CnDetailPage.vue +157 -11
  24. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +3 -1
  25. package/src/components/CnFormDialog/CnFormDialog.vue +934 -920
  26. package/src/components/CnIcon/CnIcon.vue +1 -1
  27. package/src/components/CnIndexPage/CnIndexPage.vue +51 -9
  28. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +37 -9
  29. package/src/components/CnInfoWidget/CnInfoWidget.vue +219 -0
  30. package/src/components/CnInfoWidget/index.js +1 -0
  31. package/src/components/CnJsonViewer/CnJsonViewer.vue +283 -0
  32. package/src/components/CnJsonViewer/index.js +1 -0
  33. package/src/components/CnKpiGrid/CnKpiGrid.vue +5 -1
  34. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +7 -1
  35. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +7 -1
  36. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +1 -1
  37. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +1 -1
  38. package/src/components/CnObjectCard/CnObjectCard.vue +1 -1
  39. package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +368 -0
  40. package/src/components/CnObjectSidebar/CnFilesTab.vue +286 -0
  41. package/src/components/CnObjectSidebar/CnNotesTab.vue +249 -0
  42. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +45 -668
  43. package/src/components/CnObjectSidebar/CnTagsTab.vue +258 -0
  44. package/src/components/CnObjectSidebar/CnTasksTab.vue +482 -0
  45. package/src/components/CnObjectSidebar/index.js +5 -0
  46. package/src/components/CnProgressBar/CnProgressBar.vue +262 -0
  47. package/src/components/CnProgressBar/index.js +1 -0
  48. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +1 -1
  49. package/src/components/CnStatsBlock/CnStatsBlock.vue +27 -11
  50. package/src/components/CnStatsPanel/CnStatsPanel.vue +320 -0
  51. package/src/components/CnStatsPanel/index.js +1 -0
  52. package/src/components/CnStatusBadge/CnStatusBadge.vue +15 -2
  53. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +5 -1
  54. package/src/components/CnTableWidget/CnTableWidget.vue +332 -0
  55. package/src/components/CnTableWidget/index.js +1 -0
  56. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +36 -1
  57. package/src/components/index.js +11 -0
  58. package/src/composables/useDashboardView.js +58 -12
  59. package/src/composables/useDetailView.js +3 -2
  60. package/src/composables/useListView.js +7 -6
  61. package/src/composables/useSubResource.js +3 -3
  62. package/src/css/badge.css +32 -0
  63. package/src/css/card.css +1 -0
  64. package/src/css/detail-page.css +74 -7
  65. package/src/index.js +16 -0
  66. package/src/mixins/gridLayout.js +118 -0
  67. package/src/store/createCrudStore.js +360 -0
  68. package/src/store/createSubResourcePlugin.js +5 -15
  69. package/src/store/index.js +1 -0
  70. package/src/store/plugins/auditTrails.js +346 -6
  71. package/src/store/plugins/lifecycle.js +4 -4
  72. package/src/store/plugins/registerMapping.js +18 -8
  73. package/src/store/plugins/relations.js +1 -1
  74. package/src/store/plugins/search.js +21 -8
  75. package/src/store/useObjectStore.js +30 -36
  76. package/src/utils/getTheme.js +9 -0
  77. package/src/utils/headers.js +13 -3
  78. package/src/utils/index.js +1 -0
  79. package/src/utils/schema.js +3 -3
  80. package/src/utils/widgetVisibility.js +162 -0
  81. package/src/components/CnObjectCard/eslint-setup.md +0 -235
  82. 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>{{ label }}</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
- return this.colorMap[key] || this.variant
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 class="cn-widget-wrapper" :style="wrapperStyles">
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;
@@ -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, and edit mode. Apps provide their own widget
10
- * definitions and persist layouts however they choose (app config,
11
- * OpenRegister objects, etc.).
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=[]] Static widget definitions from the app
15
- * @param {Array} [options.defaultLayout=[]] Default layout if no saved layout exists
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=false] Whether to also load NC Dashboard API widgets
19
- * @param {number} [options.columns=12] Grid 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 [...appWidgets.value, ...ncWidgets.value]
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='title'] Field shown in error messages
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=20] Default `_limit` sent to the API
19
- * @param {number} [options.debounceMs=300] Search debounce in milliseconds
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=1] Page to fetch
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=300] Search debounce in milliseconds
260
- * @param {number} [options.pageSize=20] Default page size
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
  */