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

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/css/index.css ADDED
@@ -0,0 +1,5 @@
1
+ /* Entry point for @conduction/nextcloud-vue/css/index.css
2
+ * When installed via npm, this re-exports the library CSS.
3
+ * When resolved via webpack alias (../nextcloud-vue/src), the alias
4
+ * resolves directly to src/css/index.css instead. */
5
+ @import '../src/css/index.css';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "0.1.0-beta.2",
3
+ "version": "0.1.0-beta.3",
4
4
  "description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
5
5
  "license": "EUPL-1.2",
6
6
  "author": "Conduction B.V. <info@conduction.nl>",
@@ -10,7 +10,8 @@
10
10
  "types": "src/types/index.d.ts",
11
11
  "files": [
12
12
  "dist/",
13
- "src/"
13
+ "src/",
14
+ "css/"
14
15
  ],
15
16
  "sideEffects": true,
16
17
  "scripts": {
@@ -1,29 +1,266 @@
1
- import { ref, onBeforeUnmount } from 'vue'
1
+ import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
2
+ import { useObjectStore } from '../store/index.js'
2
3
 
3
4
  /**
4
- * Composable for managing list view state: search, filters, sorting, pagination.
5
+ * Composable for managing list view state with full objectStore integration.
5
6
  *
6
- * Extracts the search-debounce + filter + sort + pagination pattern
7
- * found in every list view across Pipelinq and Procest.
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.
8
11
  *
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
+ * 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
12
19
  * @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
20
+ * @return {object} Reactive state and event handlers
16
21
  *
17
22
  * @example
18
- * import { useListView } from '@conduction/nextcloud-vue'
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
+ * })
19
32
  *
20
- * const { searchTerm, sortKey, sortOrder, currentPage, onSearchInput, toggleSort, fetch } = useListView({
33
+ * @example
34
+ * // Legacy API — still works
35
+ * const { searchTerm, filters, onSearchInput, toggleSort } = useListView({
21
36
  * objectType: 'client',
22
37
  * fetchFn: (type, params) => objectStore.fetchCollection(type, params),
23
- * defaultSort: { key: 'name', order: 'asc' },
24
38
  * })
25
39
  */
26
- export function useListView(options) {
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) {
27
264
  const searchTerm = ref('')
28
265
  const filters = ref({})
29
266
  const sortKey = ref(options.defaultSort?.key || null)
@@ -33,10 +270,6 @@ export function useListView(options) {
33
270
 
34
271
  let searchTimeout = null
35
272
 
36
- /**
37
- * Build fetch parameters from current state.
38
- * @return {object} Parameters for the fetch function
39
- */
40
273
  function buildFetchParams() {
41
274
  const params = {
42
275
  _limit: pageSize.value,
@@ -51,7 +284,6 @@ export function useListView(options) {
51
284
  params._order = { [sortKey.value]: sortOrder.value }
52
285
  }
53
286
 
54
- // Merge active filters
55
287
  for (const [key, value] of Object.entries(filters.value)) {
56
288
  if (value !== null && value !== '' && value !== false) {
57
289
  params[key] = value
@@ -61,33 +293,22 @@ export function useListView(options) {
61
293
  return params
62
294
  }
63
295
 
64
- /**
65
- * Execute a fetch with current state.
66
- * @param {number} [page] Optional page override
67
- * @return {Promise<Array>} Fetched results
68
- */
69
296
  async function fetchData(page) {
70
297
  if (page !== undefined) {
71
298
  currentPage.value = page
72
299
  }
73
300
  const params = buildFetchParams()
74
- return options.fetchFn(options.objectType, params)
301
+ if (options.fetchFn) {
302
+ return options.fetchFn(options.objectType, params)
303
+ }
75
304
  }
76
305
 
77
- /**
78
- * Handle search input with debouncing.
79
- * @param {string} value New search value
80
- */
81
306
  function onSearchInput(value) {
82
307
  searchTerm.value = value
83
308
  clearTimeout(searchTimeout)
84
309
  searchTimeout = setTimeout(() => fetchData(1), options.debounceMs || 300)
85
310
  }
86
311
 
87
- /**
88
- * Toggle sort on a column. Cycles: asc -> desc -> null.
89
- * @param {string} key Column key
90
- */
91
312
  function toggleSort(key) {
92
313
  if (sortKey.value === key) {
93
314
  if (sortOrder.value === 'asc') {
@@ -103,19 +324,11 @@ export function useListView(options) {
103
324
  fetchData(1)
104
325
  }
105
326
 
106
- /**
107
- * Set a filter value and re-fetch.
108
- * @param {string} key Filter key
109
- * @param {*} value Filter value
110
- */
111
327
  function setFilter(key, value) {
112
328
  filters.value = { ...filters.value, [key]: value }
113
329
  fetchData(1)
114
330
  }
115
331
 
116
- /**
117
- * Clear all filters and search, then re-fetch.
118
- */
119
332
  function clearAllFilters() {
120
333
  searchTerm.value = ''
121
334
  filters.value = {}
@@ -124,10 +337,6 @@ export function useListView(options) {
124
337
  fetchData(1)
125
338
  }
126
339
 
127
- /**
128
- * Navigate to a specific page.
129
- * @param {number} page Page number
130
- */
131
340
  function goToPage(page) {
132
341
  currentPage.value = page
133
342
  fetchData()