@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,68 +1,68 @@
1
- import { createSubResourcePlugin } from '../createSubResourcePlugin.js'
2
-
3
- /**
4
- * Relations plugin for the object store.
5
- *
6
- * Adds three sub-resources for object relations:
7
- * - contracts: contractual relations between objects
8
- * - uses: outgoing references (this object uses other objects)
9
- * - used: incoming references (other objects use this object)
10
- *
11
- * Each sub-resource gets its own state, loading, error, fetch, and clear.
12
- */
13
-
14
- const contractsBase = createSubResourcePlugin('contracts', 'contracts')
15
- const usesBase = createSubResourcePlugin('uses', 'uses')
16
- const usedBase = createSubResourcePlugin('used', 'used')
17
-
18
- /**
19
- * Combined relations plugin that registers contracts, uses, and used sub-resources.
20
- *
21
- * @param {object} [options={}] Plugin options
22
- * @return {Function} Plugin factory
23
- *
24
- * @example
25
- * const useStore = createObjectStore('object', {
26
- * plugins: [relationsPlugin()],
27
- * })
28
- * const store = useStore()
29
- * await store.fetchContracts('case', caseId)
30
- * await store.fetchUses('case', caseId)
31
- * await store.fetchUsed('case', caseId)
32
- */
33
- export function relationsPlugin(options = {}) {
34
- const contracts = contractsBase(options)
35
- const uses = usesBase(options)
36
- const used = usedBase(options)
37
-
38
- return {
39
- name: 'Relations',
40
-
41
- state: () => ({
42
- ...contracts.state(),
43
- ...uses.state(),
44
- ...used.state(),
45
- }),
46
-
47
- getters: {
48
- ...contracts.getters,
49
- ...uses.getters,
50
- ...used.getters,
51
- },
52
-
53
- actions: {
54
- ...contracts.actions,
55
- ...uses.actions,
56
- ...used.actions,
57
-
58
- /**
59
- * Clear all relation sub-resources.
60
- */
61
- clearRelations() {
62
- this.clearContracts()
63
- this.clearUses()
64
- this.clearUsed()
65
- },
66
- },
67
- }
68
- }
1
+ import { createSubResourcePlugin } from '../createSubResourcePlugin.js'
2
+
3
+ /**
4
+ * Relations plugin for the object store.
5
+ *
6
+ * Adds three sub-resources for object relations:
7
+ * - contracts: contractual relations between objects
8
+ * - uses: outgoing references (this object uses other objects)
9
+ * - used: incoming references (other objects use this object)
10
+ *
11
+ * Each sub-resource gets its own state, loading, error, fetch, and clear.
12
+ */
13
+
14
+ const contractsBase = createSubResourcePlugin('contracts', 'contracts')
15
+ const usesBase = createSubResourcePlugin('uses', 'uses')
16
+ const usedBase = createSubResourcePlugin('used', 'used')
17
+
18
+ /**
19
+ * Combined relations plugin that registers contracts, uses, and used sub-resources.
20
+ *
21
+ * @param {object} [options] Plugin options
22
+ * @return {Function} Plugin factory
23
+ *
24
+ * @example
25
+ * const useStore = createObjectStore('object', {
26
+ * plugins: [relationsPlugin()],
27
+ * })
28
+ * const store = useStore()
29
+ * await store.fetchContracts('case', caseId)
30
+ * await store.fetchUses('case', caseId)
31
+ * await store.fetchUsed('case', caseId)
32
+ */
33
+ export function relationsPlugin(options = {}) {
34
+ const contracts = contractsBase(options)
35
+ const uses = usesBase(options)
36
+ const used = usedBase(options)
37
+
38
+ return {
39
+ name: 'Relations',
40
+
41
+ state: () => ({
42
+ ...contracts.state(),
43
+ ...uses.state(),
44
+ ...used.state(),
45
+ }),
46
+
47
+ getters: {
48
+ ...contracts.getters,
49
+ ...uses.getters,
50
+ ...used.getters,
51
+ },
52
+
53
+ actions: {
54
+ ...contracts.actions,
55
+ ...uses.actions,
56
+ ...used.actions,
57
+
58
+ /**
59
+ * Clear all relation sub-resources.
60
+ */
61
+ clearRelations() {
62
+ this.clearContracts()
63
+ this.clearUses()
64
+ this.clearUsed()
65
+ },
66
+ },
67
+ }
68
+ }
@@ -0,0 +1,385 @@
1
+ import { buildHeaders, buildQueryString, prefixUrl } from '../../utils/headers.js'
2
+
3
+ /**
4
+ * The type slug used internally by the search plugin.
5
+ * Consumers can import this constant to avoid hard-coding the string 'search'.
6
+ *
7
+ * @type {string}
8
+ */
9
+ export const SEARCH_TYPE = 'search'
10
+
11
+ /**
12
+ * Build the API URL for fetching a register by ID.
13
+ *
14
+ * @param {string} registerId OpenRegister register ID
15
+ * @return {string} Full API URL path
16
+ */
17
+ export function getRegisterApiUrl(registerId) {
18
+ return prefixUrl(`/apps/openregister/api/registers/${registerId}`)
19
+ }
20
+
21
+ /**
22
+ * Build the API URL for fetching a schema by ID.
23
+ *
24
+ * @param {string} schemaId OpenRegister schema ID
25
+ * @return {string} Full API URL path
26
+ */
27
+ export function getSchemaApiUrl(schemaId) {
28
+ return prefixUrl(`/apps/openregister/api/schemas/${schemaId}`)
29
+ }
30
+
31
+ /**
32
+ * Search plugin for the object store.
33
+ *
34
+ * Adds a dedicated search context to the store: a separate collection, pagination,
35
+ * loading state, schema, register, and facet cache — all scoped to the current
36
+ * `searchParams`. Unlike the main type-keyed collections, the search collection is
37
+ * a single slot that is refetched whenever `refetchSearchCollection` is called.
38
+ *
39
+ * The plugin owns two URL-builder helpers (`getRegisterApiUrl`, `getSchemaApiUrl`)
40
+ * that are also exported at module level for use outside the store.
41
+ *
42
+ * State added:
43
+ * - `searchParams` — Query parameters for the current search (register, schema, filters, …)
44
+ * - `searchVisibleColumns` — Column keys visible in the search results table
45
+ *
46
+ * Getters added:
47
+ * - `searchCollection` — Array of search result objects
48
+ * - `searchPagination` — `{ total, page, pages, limit }` for the last fetch
49
+ * - `searchLoading` — `true` while a fetch is in progress
50
+ * - `searchSchema` — Schema object for the current search register/schema pair
51
+ * - `searchRegister` — Register object for the current search register/schema pair
52
+ * - `searchFacets` — Facet data in CnIndexSidebar-compatible format
53
+ *
54
+ * Actions added:
55
+ * - `setSearchParams(params)` — Update search params (clears stale schema/register cache)
56
+ * - `setSearchVisibleColumns(columns)` — Replace the visible columns list
57
+ * - `clearSearchCollection()` — Reset collection, pagination, and facets
58
+ * - `refetchSearchCollection()` — Fetch collection using current `searchParams`
59
+ *
60
+ * @example
61
+ * import { createObjectStore, searchPlugin, SEARCH_TYPE } from '@conduction/nextcloud-vue'
62
+ *
63
+ * const useMyStore = createObjectStore('myapp', {
64
+ * plugins: [searchPlugin()],
65
+ * })
66
+ *
67
+ * const store = useMyStore()
68
+ *
69
+ * // Set params and fetch
70
+ * store.setSearchParams({ register: 'reg-1', schema: 'schema-1', _search: 'foo' })
71
+ * await store.refetchSearchCollection()
72
+ *
73
+ * // Read results
74
+ * console.log(store.searchCollection) // [{ id: '…', … }, …]
75
+ * console.log(store.searchPagination) // { total: 42, page: 1, pages: 3, limit: 20 }
76
+ * console.log(store.searchSchema) // { title: '…', properties: { … } }
77
+ *
78
+ * // Clear
79
+ * store.clearSearchCollection()
80
+ *
81
+ * @return {object} Plugin definition
82
+ */
83
+ export function searchPlugin() {
84
+ return {
85
+ name: 'search',
86
+
87
+ state: () => ({
88
+ /**
89
+ * Query parameters for the active search.
90
+ * Must include `register` and `schema` for `refetchSearchCollection` to work.
91
+ * All other keys are forwarded as query-string parameters (e.g. `_search`, `_page`).
92
+ * @type {object}
93
+ */
94
+ searchParams: {},
95
+
96
+ /**
97
+ * Column keys that are visible in the search results table.
98
+ * @type {string[]}
99
+ */
100
+ searchVisibleColumns: [],
101
+
102
+ /** @private @type {Array} */
103
+ _searchCollection: [],
104
+
105
+ /** @private @type {{ total: number, page: number, pages: number, limit: number }} */
106
+ _searchPagination: { total: 0, page: 1, pages: 1, limit: 20 },
107
+
108
+ /** @private @type {boolean} */
109
+ _searchLoading: false,
110
+
111
+ /** @private @type {object|null} */
112
+ _searchSchema: null,
113
+
114
+ /** @private @type {object|null} */
115
+ _searchRegister: null,
116
+
117
+ /** @private @type {object} */
118
+ _searchFacets: {},
119
+
120
+ /** @private @type {number} Request sequence counter to prevent race conditions */
121
+ _searchRequestId: 0,
122
+ }),
123
+
124
+ getters: {
125
+ /**
126
+ * The current search result objects.
127
+ * @param {object} state Pinia state
128
+ * @return {Array}
129
+ */
130
+ searchCollection: (state) => state._searchCollection,
131
+
132
+ /**
133
+ * Pagination state for the last search fetch.
134
+ * @param {object} state Pinia state
135
+ * @return {{ total: number, page: number, pages: number, limit: number }}
136
+ */
137
+ searchPagination: (state) => state._searchPagination,
138
+
139
+ /**
140
+ * True while a search fetch is in progress.
141
+ * @param {object} state Pinia state
142
+ * @return {boolean}
143
+ */
144
+ searchLoading: (state) => state._searchLoading,
145
+
146
+ /**
147
+ * The schema object for the current search register/schema pair.
148
+ * Populated automatically by `refetchSearchCollection`.
149
+ * @param {object} state Pinia state
150
+ * @return {object|null}
151
+ */
152
+ searchSchema: (state) => state._searchSchema,
153
+
154
+ /**
155
+ * The register object for the current search register/schema pair.
156
+ * Populated automatically by `refetchSearchCollection`.
157
+ * @param {object} state Pinia state
158
+ * @return {object|null}
159
+ */
160
+ searchRegister: (state) => state._searchRegister,
161
+
162
+ /**
163
+ * Facet data from the last search fetch, in CnIndexSidebar-compatible format:
164
+ * `{ fieldName: { values: [{ value, count }] } }`.
165
+ * @param {object} state Pinia state
166
+ * @return {object}
167
+ */
168
+ searchFacets: (state) => state._searchFacets,
169
+ },
170
+
171
+ actions: {
172
+ /**
173
+ * Update the search parameters.
174
+ * If `register` or `schema` changes, the cached register/schema objects are
175
+ * cleared so the next `refetchSearchCollection` re-fetches them.
176
+ *
177
+ * @param {object} params New search params. Must include `register` and `schema`.
178
+ */
179
+ setSearchParams(params) {
180
+ if (params.register !== this.searchParams.register) {
181
+ this._searchRegister = null
182
+ }
183
+ if (params.schema !== this.searchParams.schema) {
184
+ this._searchSchema = null
185
+ }
186
+ this.searchParams = { ...params }
187
+ },
188
+
189
+ /**
190
+ * Merge the given properties into the current search parameters.
191
+ * Only the provided keys are overwritten; all other keys remain unchanged.
192
+ * If `register` or `schema` changes, the cached register/schema objects are
193
+ * cleared so the next `refetchSearchCollection` re-fetches them.
194
+ *
195
+ * @param {object} params Partial search params to merge
196
+ */
197
+ updateSearchParams(params) {
198
+ if ('register' in params && params.register !== this.searchParams.register) {
199
+ this._searchRegister = null
200
+ }
201
+ if ('schema' in params && params.schema !== this.searchParams.schema) {
202
+ this._searchSchema = null
203
+ }
204
+ this.searchParams = { ...this.searchParams, ...params }
205
+ },
206
+
207
+ /**
208
+ * Replace the list of visible column keys for the search results table.
209
+ *
210
+ * @param {string[]} columns Column key array
211
+ */
212
+ setSearchVisibleColumns(columns) {
213
+ this.searchVisibleColumns = Array.isArray(columns) ? columns : []
214
+ },
215
+
216
+ /**
217
+ * Clear the search collection, pagination, and facets.
218
+ * Does not reset `searchParams` or `searchVisibleColumns`.
219
+ */
220
+ clearSearchCollection() {
221
+ this._searchCollection = []
222
+ this._searchPagination = { total: 0, page: 1, pages: 1, limit: 20 }
223
+ this._searchFacets = {}
224
+ },
225
+
226
+ /**
227
+ * Fetch the search collection using the current `searchParams`.
228
+ * `searchParams` must include `register` and `schema`; all other keys are
229
+ * forwarded as query-string parameters to the objects endpoint.
230
+ *
231
+ * Side-effects:
232
+ * - Fetches and caches `_searchSchema` and `_searchRegister` (non-blocking,
233
+ * only when not already cached).
234
+ * - Updates `_searchCollection`, `_searchPagination`, and `_searchFacets`.
235
+ *
236
+ * @return {Promise<Array>} The fetched collection (empty array on error)
237
+ */
238
+ async refetchSearchCollection() {
239
+ const { register, schema, filters, ...queryParams } = this.searchParams
240
+
241
+ // Flatten the filters object into individual query params so the
242
+ // backend receives them as field-level filters (e.g. ?title=foo)
243
+ // instead of a single `filters=[object Object]` param.
244
+ if (filters && typeof filters === 'object') {
245
+ for (const [field, values] of Object.entries(filters)) {
246
+ if (Array.isArray(values) && values.length > 0) {
247
+ queryParams[field] = values
248
+ } else if (values && !Array.isArray(values)) {
249
+ queryParams[field] = values
250
+ }
251
+ }
252
+ }
253
+
254
+ if (!register || !schema) {
255
+ console.warn('[searchPlugin] refetchSearchCollection called without register/schema in searchParams')
256
+ return []
257
+ }
258
+
259
+ // Increment request counter to detect stale responses
260
+ const requestId = ++this._searchRequestId
261
+
262
+ this._searchLoading = true
263
+
264
+ // Auto-register the type so saveObject/deleteObject work
265
+ const type = this.createObjectTypeSlug(register, schema)
266
+ if (type && !this.objectTypes.includes(type)) {
267
+ this.registerObjectType(type, schema, register)
268
+ }
269
+
270
+ // Kick off schema/register fetches in parallel (non-blocking)
271
+ if (!this._searchSchema) {
272
+ this._fetchSearchSchema(schema)
273
+ }
274
+ if (!this._searchRegister) {
275
+ this._fetchSearchRegister(register)
276
+ }
277
+
278
+ try {
279
+ const baseUrl = this._options?.baseUrl || prefixUrl('/apps/openregister/api/objects')
280
+ const url = `${baseUrl}/${register}/${schema}` + buildQueryString(queryParams)
281
+
282
+ const response = await fetch(url, {
283
+ method: 'GET',
284
+ headers: buildHeaders(),
285
+ })
286
+
287
+ // A newer request was fired while this one was in-flight — discard
288
+ if (requestId !== this._searchRequestId) {
289
+ return []
290
+ }
291
+
292
+ if (!response.ok) {
293
+ console.error('[searchPlugin] Failed to fetch search collection:', response.status)
294
+ return []
295
+ }
296
+
297
+ const data = await response.json()
298
+
299
+ // Re-check after JSON parsing (also async)
300
+ if (requestId !== this._searchRequestId) {
301
+ return []
302
+ }
303
+
304
+ const results = data.results || data
305
+
306
+ this._searchCollection = results
307
+ this._searchPagination = {
308
+ total: data.total || results.length,
309
+ page: data.page || 1,
310
+ pages: data.pages || 1,
311
+ limit: queryParams._limit || 20,
312
+ }
313
+
314
+ if (data.facets) {
315
+ const transformed = {}
316
+ for (const [key, facet] of Object.entries(data.facets)) {
317
+ if (facet.buckets || facet.data?.buckets) {
318
+ const buckets = facet.buckets || facet.data.buckets
319
+ transformed[key] = {
320
+ values: buckets.map((b) => ({
321
+ value: b.key ?? b.value,
322
+ count: b.count || 0,
323
+ })),
324
+ }
325
+ }
326
+ }
327
+ this._searchFacets = transformed
328
+ }
329
+
330
+ return results
331
+ } catch (error) {
332
+ console.error('[searchPlugin] Error fetching search collection:', error)
333
+ return []
334
+ } finally {
335
+ // Only clear loading if this is still the latest request
336
+ if (requestId === this._searchRequestId) {
337
+ this._searchLoading = false
338
+ }
339
+ }
340
+ },
341
+
342
+ /**
343
+ * Fetch and cache the schema object for the given schema ID.
344
+ * Internal — called by `refetchSearchCollection`.
345
+ *
346
+ * @param {string} schemaId OpenRegister schema ID
347
+ * @return {Promise<void>}
348
+ */
349
+ async _fetchSearchSchema(schemaId) {
350
+ try {
351
+ const response = await fetch(getSchemaApiUrl(schemaId), {
352
+ method: 'GET',
353
+ headers: buildHeaders(),
354
+ })
355
+ if (response.ok) {
356
+ this._searchSchema = await response.json()
357
+ }
358
+ } catch {
359
+ // Non-critical — searchSchema stays null
360
+ }
361
+ },
362
+
363
+ /**
364
+ * Fetch and cache the register object for the given register ID.
365
+ * Internal — called by `refetchSearchCollection`.
366
+ *
367
+ * @param {string} registerId OpenRegister register ID
368
+ * @return {Promise<void>}
369
+ */
370
+ async _fetchSearchRegister(registerId) {
371
+ try {
372
+ const response = await fetch(getRegisterApiUrl(registerId), {
373
+ method: 'GET',
374
+ headers: buildHeaders(),
375
+ })
376
+ if (response.ok) {
377
+ this._searchRegister = await response.json()
378
+ }
379
+ } catch {
380
+ // Non-critical — searchRegister stays null
381
+ }
382
+ },
383
+ },
384
+ }
385
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Selection plugin for the object store.
3
+ *
4
+ * Adds selection management across all object types: select individual
5
+ * objects by ID, check whether an entire collection is selected, and
6
+ * toggle all-selected in one call.
7
+ *
8
+ * State added:
9
+ * - `selectedObjects` — Array of selected object IDs (strings)
10
+ *
11
+ * Getters added:
12
+ * - `isAllSelected(type)` — true when every object in the collection is selected
13
+ *
14
+ * Actions added:
15
+ * - `setSelectedObjects(ids)` — replace selection with an array of IDs
16
+ * - `clearSelectedObjects()` — deselect everything
17
+ * - `toggleSelectAllObjects(type)` — toggle between all-selected and none-selected
18
+ *
19
+ * @example
20
+ * import { createObjectStore, selectionPlugin } from '@conduction/nextcloud-vue'
21
+ *
22
+ * const useMyStore = createObjectStore('myapp', {
23
+ * plugins: [selectionPlugin()],
24
+ * })
25
+ *
26
+ * const store = useMyStore()
27
+ * store.setSelectedObjects(['abc', 'def'])
28
+ * store.toggleSelectAllObjects('invoice')
29
+ * console.log(store.isAllSelected('invoice')) // true | false
30
+ *
31
+ * @return {object} Plugin definition
32
+ */
33
+ export function selectionPlugin() {
34
+ return {
35
+ name: 'selection',
36
+
37
+ state: () => ({
38
+ /**
39
+ * IDs of currently selected objects.
40
+ * @type {string[]}
41
+ */
42
+ selectedObjects: [],
43
+ }),
44
+
45
+ getters: {
46
+ /**
47
+ * Check if all objects in a type's collection are selected.
48
+ * Returns false when the collection is empty.
49
+ *
50
+ * @param {object} state pinia injected state
51
+ *
52
+ * @return {Function} (type: string) => boolean
53
+ */
54
+ isAllSelected: (state) => (type) => {
55
+ const collection = state.collections?.[type] || []
56
+ if (!collection.length) return false
57
+ return collection.every((r) => {
58
+ const id = r.id ?? r['@self']?.id
59
+ return id != null && state.selectedObjects.includes(id)
60
+ })
61
+ },
62
+ },
63
+
64
+ actions: {
65
+ /**
66
+ * Replace the selection with the given array of IDs.
67
+ *
68
+ * @param {string[]} ids Object IDs to select
69
+ */
70
+ setSelectedObjects(ids) {
71
+ this.selectedObjects = Array.isArray(ids) ? ids : []
72
+ },
73
+
74
+ /**
75
+ * Clear all selected objects.
76
+ */
77
+ clearSelectedObjects() {
78
+ this.selectedObjects = []
79
+ },
80
+
81
+ /**
82
+ * Toggle selection of all objects in a type's collection.
83
+ * If all objects are already selected the selection is cleared;
84
+ * otherwise every object in the collection is selected.
85
+ *
86
+ * @param {string} type The registered type slug
87
+ */
88
+ toggleSelectAllObjects(type) {
89
+ const collection = this.getCollection(type)
90
+ const ids = collection.map((r) => r.id ?? r['@self']?.id).filter(Boolean)
91
+ if (this.isAllSelected(type)) {
92
+ // Remove only this type's IDs, keep other types' selections
93
+ const idsSet = new Set(ids)
94
+ this.selectedObjects = this.selectedObjects.filter((id) => !idsSet.has(id))
95
+ } else {
96
+ // Add this type's IDs to existing selection (deduplicated)
97
+ const existing = new Set(this.selectedObjects)
98
+ for (const id of ids) existing.add(id)
99
+ this.selectedObjects = [...existing]
100
+ }
101
+ },
102
+ },
103
+ }
104
+ }