@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 +5 -0
- package/package.json +3 -2
- package/src/composables/useListView.js +254 -45
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.
|
|
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
|
|
5
|
+
* Composable for managing list view state with full objectStore integration.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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
|
|
12
19
|
* @param {number} [options.debounceMs=300] Search debounce in milliseconds
|
|
13
|
-
* @
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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()
|