@conduction/nextcloud-vue 0.1.0-beta.3 → 0.1.0-beta.5

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 (142) hide show
  1. package/README.md +226 -226
  2. package/dist/nextcloud-vue.cjs +67614 -0
  3. package/dist/nextcloud-vue.cjs.js +58386 -6112
  4. package/dist/nextcloud-vue.cjs.js.map +1 -1
  5. package/dist/nextcloud-vue.cjs.map +1 -0
  6. package/dist/nextcloud-vue.css +1819 -285
  7. package/dist/nextcloud-vue.esm.js +58342 -6088
  8. package/dist/nextcloud-vue.esm.js.map +1 -1
  9. package/package.json +82 -62
  10. package/src/components/CnActionsBar/CnActionsBar.vue +17 -7
  11. package/src/components/CnActionsBar/index.js +1 -1
  12. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +579 -0
  13. package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -0
  14. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -0
  15. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +418 -0
  16. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -0
  17. package/src/components/CnAdvancedFormDialog/index.js +1 -0
  18. package/src/components/CnCardGrid/CnCardGrid.vue +1 -1
  19. package/src/components/CnCardGrid/index.js +1 -1
  20. package/src/components/CnCellRenderer/index.js +1 -1
  21. package/src/components/CnChartWidget/CnChartWidget.vue +320 -0
  22. package/src/components/CnChartWidget/index.js +1 -0
  23. package/src/components/CnConfigurationCard/index.js +1 -1
  24. package/src/components/CnCopyDialog/CnCopyDialog.vue +250 -250
  25. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +225 -0
  26. package/src/components/CnDashboardGrid/index.js +1 -0
  27. package/src/components/CnDashboardPage/CnDashboardPage.vue +390 -0
  28. package/src/components/CnDashboardPage/index.js +1 -0
  29. package/src/components/CnDataTable/CnDataTable.vue +1 -1
  30. package/src/components/CnDataTable/index.js +1 -1
  31. package/src/components/CnDeleteDialog/CnDeleteDialog.vue +170 -170
  32. package/src/components/CnDetailCard/CnDetailCard.vue +214 -0
  33. package/src/components/CnDetailCard/index.js +1 -0
  34. package/src/components/CnDetailPage/CnDetailPage.vue +285 -0
  35. package/src/components/CnDetailPage/index.js +1 -0
  36. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +9 -1
  37. package/src/components/CnFacetSidebar/index.js +1 -1
  38. package/src/components/CnFilterBar/index.js +1 -1
  39. package/src/components/CnFormDialog/CnFormDialog.vue +302 -11
  40. package/src/components/CnIcon/index.js +1 -1
  41. package/src/components/CnIndexPage/CnIndexPage.vue +71 -3
  42. package/src/components/CnIndexPage/index.js +1 -1
  43. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +121 -102
  44. package/src/components/CnIndexSidebar/index.js +1 -1
  45. package/src/components/CnItemCard/CnItemCard.vue +132 -0
  46. package/src/components/CnItemCard/index.js +1 -0
  47. package/src/components/CnKpiGrid/index.js +1 -1
  48. package/src/components/CnMassActionBar/index.js +1 -1
  49. package/src/components/CnMassCopyDialog/index.js +1 -1
  50. package/src/components/CnMassDeleteDialog/index.js +1 -1
  51. package/src/components/CnMassExportDialog/index.js +1 -1
  52. package/src/components/CnMassImportDialog/index.js +1 -1
  53. package/src/components/CnNoteCard/CnNoteCard.vue +149 -0
  54. package/src/components/CnNoteCard/index.js +1 -0
  55. package/src/components/CnNotesCard/CnNotesCard.vue +413 -0
  56. package/src/components/CnNotesCard/index.js +1 -0
  57. package/src/components/CnObjectCard/CnObjectCard.vue +1 -1
  58. package/src/components/CnObjectCard/index.js +1 -1
  59. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +876 -0
  60. package/src/components/CnObjectSidebar/index.js +1 -0
  61. package/src/components/CnPageHeader/index.js +1 -1
  62. package/src/components/CnPagination/index.js +1 -1
  63. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +792 -792
  64. package/src/components/CnRowActions/CnRowActions.vue +25 -3
  65. package/src/components/CnRowActions/index.js +1 -1
  66. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -0
  67. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -0
  68. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -0
  69. package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -0
  70. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -0
  71. package/src/components/CnSchemaFormDialog/index.js +1 -0
  72. package/src/components/CnSettingsCard/index.js +1 -1
  73. package/src/components/CnSettingsSection/index.js +1 -1
  74. package/src/components/CnStatsBlock/CnStatsBlock.vue +62 -8
  75. package/src/components/CnStatsBlock/index.js +1 -1
  76. package/src/components/CnStatusBadge/index.js +1 -1
  77. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +540 -0
  78. package/src/components/CnTabbedFormDialog/index.js +1 -0
  79. package/src/components/CnTasksCard/CnTasksCard.vue +373 -0
  80. package/src/components/CnTasksCard/index.js +1 -0
  81. package/src/components/CnTileWidget/CnTileWidget.vue +159 -0
  82. package/src/components/CnTileWidget/index.js +1 -0
  83. package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -0
  84. package/src/components/CnTimelineStages/index.js +1 -0
  85. package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -0
  86. package/src/components/CnUserActionMenu/index.js +1 -0
  87. package/src/components/CnVersionInfoCard/index.js +1 -1
  88. package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -0
  89. package/src/components/CnWidgetRenderer/index.js +1 -0
  90. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +211 -0
  91. package/src/components/CnWidgetWrapper/index.js +1 -0
  92. package/src/components/index.js +43 -29
  93. package/src/composables/index.js +4 -3
  94. package/src/composables/useDashboardView.js +240 -0
  95. package/src/composables/useDetailView.js +289 -132
  96. package/src/composables/useListView.js +363 -362
  97. package/src/composables/useSubResource.js +142 -142
  98. package/src/constants/metadata.js +30 -30
  99. package/src/css/CnSchemaFormDialog.css +546 -0
  100. package/src/css/__sample_nextcloud_tokens.css +110 -0
  101. package/src/css/actions-bar.css +48 -48
  102. package/src/css/badge.css +51 -51
  103. package/src/css/card.css +128 -128
  104. package/src/css/dashboard.css +70 -0
  105. package/src/css/detail-page.css +168 -0
  106. package/src/css/detail.css +68 -68
  107. package/src/css/index-page.css +44 -32
  108. package/src/css/index-sidebar.css +193 -187
  109. package/src/css/index.css +16 -12
  110. package/src/css/layout.css +90 -90
  111. package/src/css/page-header.css +33 -33
  112. package/src/css/pagination.css +72 -72
  113. package/src/css/table.css +142 -142
  114. package/src/css/timeline-stages.css +218 -0
  115. package/src/css/utilities.css +46 -46
  116. package/src/index.js +72 -53
  117. package/src/store/createSubResourcePlugin.js +135 -135
  118. package/src/store/index.js +3 -3
  119. package/src/store/plugins/auditTrails.js +17 -17
  120. package/src/store/plugins/files.js +250 -186
  121. package/src/store/plugins/index.js +7 -5
  122. package/src/store/plugins/lifecycle.js +180 -180
  123. package/src/store/plugins/relations.js +68 -68
  124. package/src/store/plugins/search.js +372 -0
  125. package/src/store/plugins/selection.js +104 -0
  126. package/src/store/useObjectStore.js +829 -686
  127. package/src/types/auditTrail.d.ts +32 -32
  128. package/src/types/file.d.ts +23 -23
  129. package/src/types/index.d.ts +35 -35
  130. package/src/types/notification.d.ts +36 -36
  131. package/src/types/object.d.ts +40 -40
  132. package/src/types/organisation.d.ts +41 -41
  133. package/src/types/register.d.ts +25 -25
  134. package/src/types/schema.d.ts +39 -39
  135. package/src/types/shared.d.ts +79 -79
  136. package/src/types/source.d.ts +14 -14
  137. package/src/types/task.d.ts +31 -31
  138. package/src/utils/errors.js +96 -96
  139. package/src/utils/headers.js +68 -50
  140. package/src/utils/id.js +13 -0
  141. package/src/utils/index.js +3 -3
  142. package/src/utils/schema.js +422 -419
@@ -1,362 +1,363 @@
1
- import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
2
- import { useObjectStore } from '../store/index.js'
3
-
4
- /**
5
- * Composable for managing list view state with full objectStore integration.
6
- *
7
- * When called with an `objectType` string, connects to the objectStore and handles
8
- * schema loading, collection fetching, sidebar wiring, and all event handlers
9
- * automatically. Everything a `CnIndexPage`-based list view needs is returned
10
- * directly — no additional computed properties or methods required in the component.
11
- *
12
- * Backward-compatible: existing `useListView(options)` and `useListView()` calls
13
- * continue to work without modification.
14
- *
15
- * @param {string|object} [objectTypeOrOptions] Object type slug (new API) or legacy options object
16
- * @param {object} [options] Options (new API only)
17
- * @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
20
- * @return {object} Reactive state and event handlers
21
- *
22
- * @example
23
- * // New API — minimal
24
- * const { schema, objects, loading, pagination,
25
- * onSearch, onSort, onFilterChange, onPageChange, refresh } = useListView('client')
26
- *
27
- * @example
28
- * // New API — with sidebar wiring
29
- * const list = useListView('client', {
30
- * sidebarState: inject('sidebarState', null),
31
- * })
32
- *
33
- * @example
34
- * // Legacy API — still works
35
- * const { searchTerm, filters, onSearchInput, toggleSort } = useListView({
36
- * objectType: 'client',
37
- * fetchFn: (type, params) => objectStore.fetchCollection(type, params),
38
- * })
39
- */
40
- export function useListView(objectTypeOrOptions, options) {
41
- // Backward compat: if first arg is an object or absent, delegate to legacy implementation
42
- if (!objectTypeOrOptions || typeof objectTypeOrOptions === 'object') {
43
- return useLegacyListView(objectTypeOrOptions || {})
44
- }
45
-
46
- // ── New API ──────────────────────────────────────────────────────────
47
- const objectType = objectTypeOrOptions
48
- const opts = options || {}
49
- const sidebarState = opts.sidebarState || null
50
-
51
- const objectStore = useObjectStore()
52
-
53
- // ── State refs ───────────────────────────────────────────────────────
54
- const schema = ref(null)
55
- const searchTerm = ref('')
56
- const sortKey = ref(null)
57
- const sortOrder = ref('asc')
58
- const activeFilters = ref({})
59
- const visibleColumns = ref(null)
60
- const pageSize = ref(opts.defaultPageSize || 20)
61
-
62
- // ── Computed refs from the store ─────────────────────────────────────
63
- const objects = computed(() => objectStore.collections[objectType] || [])
64
- const loading = computed(() => objectStore.loading[objectType] || false)
65
- const pagination = computed(
66
- () => objectStore.pagination[objectType] || { total: 0, page: 1, pages: 1, limit: 20 },
67
- )
68
-
69
- let searchTimeout = null
70
-
71
- // ── Param construction ───────────────────────────────────────────────
72
-
73
- /**
74
- * Build API fetch params from current reactive state.
75
- *
76
- * @param {number} page Page number to request
77
- * @return {object} Params object ready to pass to fetchCollection
78
- */
79
- function buildParams(page) {
80
- const params = { _limit: pageSize.value, _page: page }
81
-
82
- if (searchTerm.value) {
83
- params._search = searchTerm.value
84
- }
85
-
86
- if (sortKey.value) {
87
- params._order = { [sortKey.value]: sortOrder.value }
88
- }
89
-
90
- for (const [key, values] of Object.entries(activeFilters.value)) {
91
- if (values && values.length > 0) {
92
- // Single-value arrays are unwrapped to scalar params
93
- params[key] = values.length === 1 ? values[0] : values
94
- }
95
- }
96
-
97
- return params
98
- }
99
-
100
- // ── Fetch ────────────────────────────────────────────────────────────
101
-
102
- /**
103
- * Fetch the collection using current state params and update sidebar facet data.
104
- *
105
- * @param {number} [page=1] Page to fetch
106
- * @return {Promise<void>}
107
- */
108
- async function refresh(page = 1) {
109
- await objectStore.fetchCollection(objectType, buildParams(page))
110
- }
111
-
112
- // ── Event handlers ───────────────────────────────────────────────────
113
-
114
- /**
115
- * Handle search input. Debounced by `options.debounceMs` (default 300 ms).
116
- *
117
- * @param {string} value New search string
118
- */
119
- function onSearch(value) {
120
- searchTerm.value = value
121
- clearTimeout(searchTimeout)
122
- searchTimeout = setTimeout(() => refresh(1), opts.debounceMs || 300)
123
- }
124
-
125
- /**
126
- * Handle sort change. Updates sort state and triggers refresh.
127
- *
128
- * @param {{key: string, order: string}} sort New sort definition
129
- */
130
- function onSort({ key, order }) {
131
- sortKey.value = key
132
- sortOrder.value = order || 'asc'
133
- refresh(1)
134
- }
135
-
136
- /**
137
- * Handle filter change for a single key. Empty arrays remove the key.
138
- *
139
- * @param {string} key Filter key (maps to API param name)
140
- * @param {Array} values Selected filter values
141
- */
142
- function onFilterChange(key, values) {
143
- if (!values || values.length === 0) {
144
- const updated = { ...activeFilters.value }
145
- delete updated[key]
146
- activeFilters.value = updated
147
- } else {
148
- activeFilters.value = { ...activeFilters.value, [key]: values }
149
- }
150
- refresh(1)
151
- }
152
-
153
- /**
154
- * Handle page navigation.
155
- *
156
- * @param {number} page Page number to navigate to
157
- */
158
- function onPageChange(page) {
159
- refresh(page)
160
- }
161
-
162
- /**
163
- * Handle page-size change. Resets to page 1.
164
- *
165
- * @param {number} size New page size
166
- */
167
- function onPageSizeChange(size) {
168
- pageSize.value = size
169
- refresh(1)
170
- }
171
-
172
- // ── Sidebar wiring ───────────────────────────────────────────────────
173
-
174
- function setupSidebar() {
175
- if (!sidebarState) return
176
- sidebarState.active = true
177
- sidebarState.schema = schema.value
178
- sidebarState.searchValue = searchTerm.value
179
- sidebarState.activeFilters = {}
180
- sidebarState.onSearch = onSearch
181
- sidebarState.onColumnsChange = (cols) => {
182
- visibleColumns.value = cols
183
- }
184
- sidebarState.onFilterChange = ({ key, values }) => onFilterChange(key, values)
185
- }
186
-
187
- function teardownSidebar() {
188
- if (!sidebarState) return
189
- sidebarState.active = false
190
- sidebarState.schema = null
191
- sidebarState.activeFilters = {}
192
- sidebarState.facetData = {}
193
- sidebarState.onSearch = null
194
- sidebarState.onColumnsChange = null
195
- sidebarState.onFilterChange = null
196
- }
197
-
198
- // Push facet data to sidebar after each store update
199
- if (sidebarState) {
200
- watch(
201
- () => objectStore.facets[objectType],
202
- (facets) => {
203
- sidebarState.facetData = facets || {}
204
- },
205
- )
206
- }
207
-
208
- // ── Lifecycle ────────────────────────────────────────────────────────
209
-
210
- onMounted(async () => {
211
- schema.value = await objectStore.fetchSchema(objectType)
212
- if (sidebarState) {
213
- setupSidebar()
214
- }
215
- await refresh(1)
216
- })
217
-
218
- onBeforeUnmount(() => {
219
- clearTimeout(searchTimeout)
220
- teardownSidebar()
221
- })
222
-
223
- // ── Return value ─────────────────────────────────────────────────────
224
-
225
- return {
226
- // Store-derived
227
- schema,
228
- objects,
229
- loading,
230
- pagination,
231
- // Local state
232
- searchTerm,
233
- sortKey,
234
- sortOrder,
235
- activeFilters,
236
- visibleColumns,
237
- pageSize,
238
- // Event handlers
239
- onSearch,
240
- onSort,
241
- onFilterChange,
242
- onPageChange,
243
- onPageSizeChange,
244
- // Explicit fetch
245
- refresh,
246
- }
247
- }
248
-
249
- // ── Legacy implementation ─────────────────────────────────────────────────────
250
-
251
- /**
252
- * Legacy `useListView(options)` implementation.
253
- * Preserved verbatim for backward compatibility.
254
- *
255
- * @param {object} options Legacy options object
256
- * @param {string} [options.objectType] The registered object type slug
257
- * @param {Function} [options.fetchFn] Function to call: (type, params) => Promise<Array>
258
- * @param {number} [options.debounceMs=300] Search debounce in milliseconds
259
- * @param {number} [options.pageSize=20] Default page size
260
- * @param {object} [options.defaultSort] Default sort: { key: string, order: 'asc'|'desc' }
261
- * @return {object} Reactive state and methods
262
- */
263
- function useLegacyListView(options) {
264
- const searchTerm = ref('')
265
- const filters = ref({})
266
- const sortKey = ref(options.defaultSort?.key || null)
267
- const sortOrder = ref(options.defaultSort?.order || 'asc')
268
- const currentPage = ref(1)
269
- const pageSize = ref(options.pageSize || 20)
270
-
271
- let searchTimeout = null
272
-
273
- function buildFetchParams() {
274
- const params = {
275
- _limit: pageSize.value,
276
- _page: currentPage.value,
277
- }
278
-
279
- if (searchTerm.value) {
280
- params._search = searchTerm.value
281
- }
282
-
283
- if (sortKey.value) {
284
- params._order = { [sortKey.value]: sortOrder.value }
285
- }
286
-
287
- for (const [key, value] of Object.entries(filters.value)) {
288
- if (value !== null && value !== '' && value !== false) {
289
- params[key] = value
290
- }
291
- }
292
-
293
- return params
294
- }
295
-
296
- async function fetchData(page) {
297
- if (page !== undefined) {
298
- currentPage.value = page
299
- }
300
- const params = buildFetchParams()
301
- if (options.fetchFn) {
302
- return options.fetchFn(options.objectType, params)
303
- }
304
- }
305
-
306
- function onSearchInput(value) {
307
- searchTerm.value = value
308
- clearTimeout(searchTimeout)
309
- searchTimeout = setTimeout(() => fetchData(1), options.debounceMs || 300)
310
- }
311
-
312
- function toggleSort(key) {
313
- if (sortKey.value === key) {
314
- if (sortOrder.value === 'asc') {
315
- sortOrder.value = 'desc'
316
- } else {
317
- sortKey.value = null
318
- sortOrder.value = 'asc'
319
- }
320
- } else {
321
- sortKey.value = key
322
- sortOrder.value = 'asc'
323
- }
324
- fetchData(1)
325
- }
326
-
327
- function setFilter(key, value) {
328
- filters.value = { ...filters.value, [key]: value }
329
- fetchData(1)
330
- }
331
-
332
- function clearAllFilters() {
333
- searchTerm.value = ''
334
- filters.value = {}
335
- sortKey.value = options.defaultSort?.key || null
336
- sortOrder.value = options.defaultSort?.order || 'asc'
337
- fetchData(1)
338
- }
339
-
340
- function goToPage(page) {
341
- currentPage.value = page
342
- fetchData()
343
- }
344
-
345
- onBeforeUnmount(() => clearTimeout(searchTimeout))
346
-
347
- return {
348
- searchTerm,
349
- filters,
350
- sortKey,
351
- sortOrder,
352
- currentPage,
353
- pageSize,
354
- onSearchInput,
355
- toggleSort,
356
- setFilter,
357
- clearAllFilters,
358
- goToPage,
359
- fetch: fetchData,
360
- buildFetchParams,
361
- }
362
- }
1
+ import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
2
+ import { useObjectStore } from '../store/index.js'
3
+
4
+ /**
5
+ * Composable for managing list view state with full objectStore integration.
6
+ *
7
+ * When called with an `objectType` string, connects to the objectStore and handles
8
+ * schema loading, collection fetching, sidebar wiring, and all event handlers
9
+ * automatically. Everything a `CnIndexPage`-based list view needs is returned
10
+ * directly — no additional computed properties or methods required in the component.
11
+ *
12
+ * Backward-compatible: existing `useListView(options)` and `useListView()` calls
13
+ * continue to work without modification.
14
+ *
15
+ * @param {string|object} [objectTypeOrOptions] Object type slug (new API) or legacy options object
16
+ * @param {object} [options] Options (new API only)
17
+ * @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
20
+ * @param {object} [options.defaultSort] Default sort applied on mount e.g. `{ key: 'createdAt', order: 'desc' }`
21
+ * @return {object} Reactive state and event handlers
22
+ *
23
+ * @example
24
+ * // New API minimal
25
+ * const { schema, objects, loading, pagination,
26
+ * onSearch, onSort, onFilterChange, onPageChange, refresh } = useListView('client')
27
+ *
28
+ * @example
29
+ * // New API with sidebar wiring
30
+ * const list = useListView('client', {
31
+ * sidebarState: inject('sidebarState', null),
32
+ * })
33
+ *
34
+ * @example
35
+ * // Legacy API still works
36
+ * const { searchTerm, filters, onSearchInput, toggleSort } = useListView({
37
+ * objectType: 'client',
38
+ * fetchFn: (type, params) => objectStore.fetchCollection(type, params),
39
+ * })
40
+ */
41
+ export function useListView(objectTypeOrOptions, options) {
42
+ // Backward compat: if first arg is an object or absent, delegate to legacy implementation
43
+ if (!objectTypeOrOptions || typeof objectTypeOrOptions === 'object') {
44
+ return useLegacyListView(objectTypeOrOptions || {})
45
+ }
46
+
47
+ // ── New API ──────────────────────────────────────────────────────────
48
+ const objectType = objectTypeOrOptions
49
+ const opts = options || {}
50
+ const sidebarState = opts.sidebarState || null
51
+
52
+ const objectStore = useObjectStore()
53
+
54
+ // ── State refs ───────────────────────────────────────────────────────
55
+ const schema = ref(null)
56
+ const searchTerm = ref('')
57
+ const sortKey = ref(opts.defaultSort?.key || null)
58
+ const sortOrder = ref(opts.defaultSort?.order || 'asc')
59
+ const activeFilters = ref({})
60
+ const visibleColumns = ref(null)
61
+ const pageSize = ref(opts.defaultPageSize || 20)
62
+
63
+ // ── Computed refs from the store ─────────────────────────────────────
64
+ const objects = computed(() => objectStore.collections[objectType] || [])
65
+ const loading = computed(() => objectStore.loading[objectType] || false)
66
+ const pagination = computed(
67
+ () => objectStore.pagination[objectType] || { total: 0, page: 1, pages: 1, limit: 20 },
68
+ )
69
+
70
+ let searchTimeout = null
71
+
72
+ // ── Param construction ───────────────────────────────────────────────
73
+
74
+ /**
75
+ * Build API fetch params from current reactive state.
76
+ *
77
+ * @param {number} page Page number to request
78
+ * @return {object} Params object ready to pass to fetchCollection
79
+ */
80
+ function buildParams(page) {
81
+ const params = { _limit: pageSize.value, _page: page }
82
+
83
+ if (searchTerm.value) {
84
+ params._search = searchTerm.value
85
+ }
86
+
87
+ if (sortKey.value) {
88
+ params._order = { [sortKey.value]: sortOrder.value }
89
+ }
90
+
91
+ for (const [key, values] of Object.entries(activeFilters.value)) {
92
+ if (values && values.length > 0) {
93
+ // Single-value arrays are unwrapped to scalar params
94
+ params[key] = values.length === 1 ? values[0] : values
95
+ }
96
+ }
97
+
98
+ return params
99
+ }
100
+
101
+ // ── Fetch ────────────────────────────────────────────────────────────
102
+
103
+ /**
104
+ * Fetch the collection using current state params and update sidebar facet data.
105
+ *
106
+ * @param {number} [page=1] Page to fetch
107
+ * @return {Promise<void>}
108
+ */
109
+ async function refresh(page = 1) {
110
+ await objectStore.fetchCollection(objectType, buildParams(page))
111
+ }
112
+
113
+ // ── Event handlers ───────────────────────────────────────────────────
114
+
115
+ /**
116
+ * Handle search input. Debounced by `options.debounceMs` (default 300 ms).
117
+ *
118
+ * @param {string} value New search string
119
+ */
120
+ function onSearch(value) {
121
+ searchTerm.value = value
122
+ clearTimeout(searchTimeout)
123
+ searchTimeout = setTimeout(() => refresh(1), opts.debounceMs || 300)
124
+ }
125
+
126
+ /**
127
+ * Handle sort change. Updates sort state and triggers refresh.
128
+ *
129
+ * @param {{key: string, order: string}} sort New sort definition
130
+ */
131
+ function onSort({ key, order }) {
132
+ sortKey.value = key
133
+ sortOrder.value = order || 'asc'
134
+ refresh(1)
135
+ }
136
+
137
+ /**
138
+ * Handle filter change for a single key. Empty arrays remove the key.
139
+ *
140
+ * @param {string} key Filter key (maps to API param name)
141
+ * @param {Array} values Selected filter values
142
+ */
143
+ function onFilterChange(key, values) {
144
+ if (!values || values.length === 0) {
145
+ const updated = { ...activeFilters.value }
146
+ delete updated[key]
147
+ activeFilters.value = updated
148
+ } else {
149
+ activeFilters.value = { ...activeFilters.value, [key]: values }
150
+ }
151
+ refresh(1)
152
+ }
153
+
154
+ /**
155
+ * Handle page navigation.
156
+ *
157
+ * @param {number} page Page number to navigate to
158
+ */
159
+ function onPageChange(page) {
160
+ refresh(page)
161
+ }
162
+
163
+ /**
164
+ * Handle page-size change. Resets to page 1.
165
+ *
166
+ * @param {number} size New page size
167
+ */
168
+ function onPageSizeChange(size) {
169
+ pageSize.value = size
170
+ refresh(1)
171
+ }
172
+
173
+ // ── Sidebar wiring ───────────────────────────────────────────────────
174
+
175
+ function setupSidebar() {
176
+ if (!sidebarState) return
177
+ sidebarState.active = true
178
+ sidebarState.schema = schema.value
179
+ sidebarState.searchValue = searchTerm.value
180
+ sidebarState.activeFilters = {}
181
+ sidebarState.onSearch = onSearch
182
+ sidebarState.onColumnsChange = (cols) => {
183
+ visibleColumns.value = cols
184
+ }
185
+ sidebarState.onFilterChange = ({ key, values }) => onFilterChange(key, values)
186
+ }
187
+
188
+ function teardownSidebar() {
189
+ if (!sidebarState) return
190
+ sidebarState.active = false
191
+ sidebarState.schema = null
192
+ sidebarState.activeFilters = {}
193
+ sidebarState.facetData = {}
194
+ sidebarState.onSearch = null
195
+ sidebarState.onColumnsChange = null
196
+ sidebarState.onFilterChange = null
197
+ }
198
+
199
+ // Push facet data to sidebar after each store update
200
+ if (sidebarState) {
201
+ watch(
202
+ () => objectStore.facets[objectType],
203
+ (facets) => {
204
+ sidebarState.facetData = facets || {}
205
+ },
206
+ )
207
+ }
208
+
209
+ // ── Lifecycle ────────────────────────────────────────────────────────
210
+
211
+ onMounted(async () => {
212
+ schema.value = await objectStore.fetchSchema(objectType)
213
+ if (sidebarState) {
214
+ setupSidebar()
215
+ }
216
+ await refresh(1)
217
+ })
218
+
219
+ onBeforeUnmount(() => {
220
+ clearTimeout(searchTimeout)
221
+ teardownSidebar()
222
+ })
223
+
224
+ // ── Return value ─────────────────────────────────────────────────────
225
+
226
+ return {
227
+ // Store-derived
228
+ schema,
229
+ objects,
230
+ loading,
231
+ pagination,
232
+ // Local state
233
+ searchTerm,
234
+ sortKey,
235
+ sortOrder,
236
+ activeFilters,
237
+ visibleColumns,
238
+ pageSize,
239
+ // Event handlers
240
+ onSearch,
241
+ onSort,
242
+ onFilterChange,
243
+ onPageChange,
244
+ onPageSizeChange,
245
+ // Explicit fetch
246
+ refresh,
247
+ }
248
+ }
249
+
250
+ // ── Legacy implementation ─────────────────────────────────────────────────────
251
+
252
+ /**
253
+ * Legacy `useListView(options)` implementation.
254
+ * Preserved verbatim for backward compatibility.
255
+ *
256
+ * @param {object} options Legacy options object
257
+ * @param {string} [options.objectType] The registered object type slug
258
+ * @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
261
+ * @param {object} [options.defaultSort] Default sort: { key: string, order: 'asc'|'desc' }
262
+ * @return {object} Reactive state and methods
263
+ */
264
+ function useLegacyListView(options) {
265
+ const searchTerm = ref('')
266
+ const filters = ref({})
267
+ const sortKey = ref(options.defaultSort?.key || null)
268
+ const sortOrder = ref(options.defaultSort?.order || 'asc')
269
+ const currentPage = ref(1)
270
+ const pageSize = ref(options.pageSize || 20)
271
+
272
+ let searchTimeout = null
273
+
274
+ function buildFetchParams() {
275
+ const params = {
276
+ _limit: pageSize.value,
277
+ _page: currentPage.value,
278
+ }
279
+
280
+ if (searchTerm.value) {
281
+ params._search = searchTerm.value
282
+ }
283
+
284
+ if (sortKey.value) {
285
+ params._order = { [sortKey.value]: sortOrder.value }
286
+ }
287
+
288
+ for (const [key, value] of Object.entries(filters.value)) {
289
+ if (value !== null && value !== '' && value !== false) {
290
+ params[key] = value
291
+ }
292
+ }
293
+
294
+ return params
295
+ }
296
+
297
+ async function fetchData(page) {
298
+ if (page !== undefined) {
299
+ currentPage.value = page
300
+ }
301
+ const params = buildFetchParams()
302
+ if (options.fetchFn) {
303
+ return options.fetchFn(options.objectType, params)
304
+ }
305
+ }
306
+
307
+ function onSearchInput(value) {
308
+ searchTerm.value = value
309
+ clearTimeout(searchTimeout)
310
+ searchTimeout = setTimeout(() => fetchData(1), options.debounceMs || 300)
311
+ }
312
+
313
+ function toggleSort(key) {
314
+ if (sortKey.value === key) {
315
+ if (sortOrder.value === 'asc') {
316
+ sortOrder.value = 'desc'
317
+ } else {
318
+ sortKey.value = null
319
+ sortOrder.value = 'asc'
320
+ }
321
+ } else {
322
+ sortKey.value = key
323
+ sortOrder.value = 'asc'
324
+ }
325
+ fetchData(1)
326
+ }
327
+
328
+ function setFilter(key, value) {
329
+ filters.value = { ...filters.value, [key]: value }
330
+ fetchData(1)
331
+ }
332
+
333
+ function clearAllFilters() {
334
+ searchTerm.value = ''
335
+ filters.value = {}
336
+ sortKey.value = options.defaultSort?.key || null
337
+ sortOrder.value = options.defaultSort?.order || 'asc'
338
+ fetchData(1)
339
+ }
340
+
341
+ function goToPage(page) {
342
+ currentPage.value = page
343
+ fetchData()
344
+ }
345
+
346
+ onBeforeUnmount(() => clearTimeout(searchTimeout))
347
+
348
+ return {
349
+ searchTerm,
350
+ filters,
351
+ sortKey,
352
+ sortOrder,
353
+ currentPage,
354
+ pageSize,
355
+ onSearchInput,
356
+ toggleSort,
357
+ setFilter,
358
+ clearAllFilters,
359
+ goToPage,
360
+ fetch: fetchData,
361
+ buildFetchParams,
362
+ }
363
+ }