@conduction/nextcloud-vue 0.1.0-beta.1 → 0.1.0-beta.10

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