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

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 (60) hide show
  1. package/dist/nextcloud-vue.cjs.js +64663 -63467
  2. package/dist/nextcloud-vue.cjs.js.map +1 -1
  3. package/dist/nextcloud-vue.css +443 -444
  4. package/dist/nextcloud-vue.esm.js +64637 -63443
  5. package/dist/nextcloud-vue.esm.js.map +1 -1
  6. package/l10n/en.json +164 -0
  7. package/l10n/nl.json +164 -0
  8. package/package.json +19 -3
  9. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +8 -7
  10. package/src/components/CnAdvancedFormDialog/CnDataTab.vue +2 -2
  11. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +5 -5
  12. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +2 -2
  13. package/src/components/CnCardGrid/CnCardGrid.vue +2 -1
  14. package/src/components/CnChartWidget/CnChartWidget.vue +29 -1
  15. package/src/components/CnCopyDialog/CnCopyDialog.vue +15 -6
  16. package/src/components/CnDashboardPage/CnDashboardPage.vue +5 -4
  17. package/src/components/CnDetailGrid/CnDetailGrid.vue +3 -1
  18. package/src/components/CnDetailPage/CnDetailPage.vue +5 -4
  19. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +3 -2
  20. package/src/components/CnFilterBar/CnFilterBar.vue +3 -2
  21. package/src/components/CnFormDialog/CnFormDialog.vue +122 -9
  22. package/src/components/CnIndexPage/CnIndexPage.vue +1 -0
  23. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +8 -7
  24. package/src/components/CnJsonViewer/CnJsonViewer.vue +33 -4
  25. package/src/components/CnMassActionBar/CnMassActionBar.vue +1 -1
  26. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +2 -2
  27. package/src/components/CnNotesCard/CnNotesCard.vue +7 -6
  28. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +11 -10
  29. package/src/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue +3 -2
  30. package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +8 -7
  31. package/src/components/CnObjectSidebar/CnFilesTab.vue +6 -5
  32. package/src/components/CnObjectSidebar/CnNotesTab.vue +8 -7
  33. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +6 -5
  34. package/src/components/CnObjectSidebar/CnTagsTab.vue +3 -2
  35. package/src/components/CnObjectSidebar/CnTasksTab.vue +11 -10
  36. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +14 -13
  37. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +15 -14
  38. package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +4 -4
  39. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +10 -10
  40. package/src/components/CnSettingsSection/CnSettingsSection.vue +5 -4
  41. package/src/components/CnStatsBlock/CnStatsBlock.vue +5 -4
  42. package/src/components/CnStatsPanel/CnStatsPanel.vue +3 -2
  43. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +9 -8
  44. package/src/components/CnTableWidget/CnTableWidget.vue +3 -2
  45. package/src/components/CnTasksCard/CnTasksCard.vue +5 -4
  46. package/src/components/CnTimelineStages/CnTimelineStages.vue +3 -1
  47. package/src/components/CnUserActionMenu/CnUserActionMenu.vue +7 -6
  48. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +4 -3
  49. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +3 -1
  50. package/src/index.js +4 -0
  51. package/src/l10n/index.js +12 -0
  52. package/src/store/createCrudStore.d.ts +350 -0
  53. package/src/store/createCrudStore.js +58 -5
  54. package/src/store/pluginMerge.js +55 -0
  55. package/src/store/plugins/index.js +1 -0
  56. package/src/store/plugins/logs.d.ts +22 -0
  57. package/src/store/plugins/logs.js +172 -0
  58. package/src/store/useObjectStore.js +19 -49
  59. package/src/types/index.d.ts +32 -0
  60. package/src/utils/schema.js +3 -2
@@ -70,6 +70,7 @@
70
70
  </template>
71
71
 
72
72
  <script>
73
+ import { translate as t } from '@nextcloud/l10n'
73
74
  import { CnSettingsSection } from '../CnSettingsSection/index.js'
74
75
  import { NcLoadingIcon, NcButton } from '@nextcloud/vue'
75
76
  import Check from 'vue-material-design-icons/Check.vue'
@@ -122,12 +123,12 @@ export default {
122
123
  /** Section title */
123
124
  title: {
124
125
  type: String,
125
- default: 'Version Information',
126
+ default: () => t('nextcloud-vue', 'Version information'),
126
127
  },
127
128
  /** Section description */
128
129
  description: {
129
130
  type: String,
130
- default: 'Information about the current application installation',
131
+ default: () => t('nextcloud-vue', 'Information about the current application installation'),
131
132
  },
132
133
  /** Documentation URL (shows info icon next to title) */
133
134
  docUrl: {
@@ -137,7 +138,7 @@ export default {
137
138
  /** Card heading text */
138
139
  cardTitle: {
139
140
  type: String,
140
- default: 'Application Information',
141
+ default: () => t('nextcloud-vue', 'Application information'),
141
142
  },
142
143
  /** Application name to display */
143
144
  appName: {
@@ -55,6 +55,8 @@
55
55
  </template>
56
56
 
57
57
  <script>
58
+ import { translate as t } from '@nextcloud/l10n'
59
+
58
60
  /**
59
61
  * CnWidgetWrapper — Widget container with header, content, and footer.
60
62
  *
@@ -79,7 +81,7 @@ export default {
79
81
  /** Widget title */
80
82
  title: {
81
83
  type: String,
82
- default: 'Widget',
84
+ default: () => t('nextcloud-vue', 'Widget'),
83
85
  },
84
86
  /** Whether to show the header with title */
85
87
  showTitle: {
package/src/index.js CHANGED
@@ -74,6 +74,7 @@ export {
74
74
  relationsPlugin,
75
75
  filesPlugin,
76
76
  lifecyclePlugin,
77
+ logsPlugin,
77
78
  registerMappingPlugin,
78
79
  selectionPlugin,
79
80
  searchPlugin,
@@ -85,6 +86,9 @@ export {
85
86
  // Composables
86
87
  export { useListView, useDetailView, useSubResource, useDashboardView, useContextMenu } from './composables/index.js'
87
88
 
89
+ // Localization
90
+ export { registerTranslations } from './l10n/index.js'
91
+
88
92
  // Utilities
89
93
  export { buildHeaders, buildQueryString, parseResponseError, networkError, genericError } from './utils/index.js'
90
94
  export { columnsFromSchema, formatValue, filtersFromSchema, fieldsFromSchema } from './utils/index.js'
@@ -0,0 +1,12 @@
1
+ import { getLanguage, register } from '@nextcloud/l10n'
2
+ import en from '../../l10n/en.json'
3
+ import nl from '../../l10n/nl.json'
4
+
5
+ const BUNDLES = { en, nl }
6
+ const APP_NAME = 'nextcloud-vue'
7
+
8
+ export function registerTranslations() {
9
+ const lang = (getLanguage() || 'en').split(/[-_]/)[0]
10
+ const bundle = BUNDLES[lang] ?? BUNDLES.en
11
+ register(APP_NAME, bundle.translations)
12
+ }
@@ -0,0 +1,350 @@
1
+ /**
2
+ * Hand-written type definitions for `createCrudStore` (implementation is in
3
+ * `createCrudStore.js`). The library ships as JavaScript, but this file gives
4
+ * TypeScript consumers full entity inference, feature-flag gating, and
5
+ * `extend` merging with correct `this` typing.
6
+ *
7
+ * Design notes:
8
+ * - `const Id extends string` / `const F extends Features` preserves literal
9
+ * types so `features: { loading: true }` flows `true` through the
10
+ * conditional types without requiring `as const` at the call site.
11
+ * Requires TypeScript 5.0+.
12
+ * - Entity inference: if `config.entity` is a class constructor, `T` is the
13
+ * instance type. Otherwise `T` falls back to `unknown` unless the caller
14
+ * passes it explicitly via the second overload.
15
+ * - `ThisType<...>` inside `extend.actions` / `extend.getters` makes `this`
16
+ * resolve to the fully-merged store (state + getters + base actions +
17
+ * extended actions), matching Pinia's own runtime semantics.
18
+ * - `Omit<BaseActions<T>, keyof ExtActions>` implements override precedence:
19
+ * an extend action with the same name as a base action replaces it.
20
+ */
21
+
22
+ import type { StoreDefinition, _GettersTree } from 'pinia'
23
+
24
+ // ─────────────────────────────────────────────────────────────────────────────
25
+ // Utility types
26
+ // ─────────────────────────────────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Forces TypeScript to eagerly evaluate an intersection of types into a
30
+ * single flat object shape. Purely a hover/tooltip ergonomics helper —
31
+ * semantically identical to the input.
32
+ *
33
+ * Without this, VSCode's quick-info tooltip shows `StoreDefinition<...,
34
+ * FullState<Source, ...> & ..., ..., MergedActions<...>>` with the aliases
35
+ * left unexpanded, making it hard to see which properties the store has.
36
+ * With this, hover shows `{ item: Source | null; list: Source[]; ... }`
37
+ * directly.
38
+ */
39
+ export type Prettify<T> = { [K in keyof T]: T[K] } & {}
40
+
41
+ /**
42
+ * A class constructor accepting raw data (e.g. `new Source(data)`).
43
+ */
44
+ export type EntityClass<T> = new (data: any) => T
45
+
46
+ /**
47
+ * Extract the instance type from an entity-class config field.
48
+ * Falls back to `unknown` when no class is provided.
49
+ */
50
+ export type InferEntity<E> =
51
+ E extends EntityClass<infer T> ? T :
52
+ E extends null | undefined ? unknown :
53
+ unknown
54
+
55
+ // ─────────────────────────────────────────────────────────────────────────────
56
+ // Feature flags
57
+ // ─────────────────────────────────────────────────────────────────────────────
58
+
59
+ /**
60
+ * Optional behavioral toggles. When a flag is set, the factory adds the
61
+ * corresponding state fields, getters, and actions to the store.
62
+ */
63
+ export interface Features {
64
+ /** Add `loading` / `error` state plus the `isLoading` / `getError` getters. */
65
+ loading?: boolean
66
+ /** Add `viewMode` state, the `getViewMode` getter, and a `setViewMode(mode)` action. */
67
+ viewMode?: boolean
68
+ }
69
+
70
+ export type LoadingState<F> = F extends { loading: true } ? { loading: boolean; error: string | null } : {}
71
+ export type ViewModeState<F> = F extends { viewMode: true } ? { viewMode: string } : {}
72
+
73
+ // Getter trees are declared as `(state) => value` and Pinia exposes them on
74
+ // the store as `.name: value`. We declare them as getter functions so the
75
+ // `StoreDefinition<..., Getters, ...>` return-type mapping kicks in.
76
+ export type LoadingGetters<F> = F extends { loading: true }
77
+ ? {
78
+ isLoading: (state: { loading: boolean }) => boolean
79
+ getError: (state: { error: string | null }) => string | null
80
+ }
81
+ : {}
82
+ export type ViewModeGetters<F> = F extends { viewMode: true }
83
+ ? { getViewMode: (state: { viewMode: string }) => string }
84
+ : {}
85
+
86
+ export type ViewModeActions<F> = F extends { viewMode: true } ? { setViewMode(mode: string): void } : {}
87
+
88
+ // ─────────────────────────────────────────────────────────────────────────────
89
+ // Base state, getters and actions
90
+ // ─────────────────────────────────────────────────────────────────────────────
91
+
92
+ /**
93
+ * State shape every store receives by default, independent of features and
94
+ * extensions. Feature-specific fields (`loading`, `error`, `viewMode`) and
95
+ * anything contributed by `extend.state` / plugins are added on top.
96
+ */
97
+ export interface BaseState<T> {
98
+ /** The currently active/selected item, or `null` when nothing is selected. */
99
+ item: T | null
100
+ /** The full list of items as last returned by `refreshList`. */
101
+ list: T[]
102
+ /** Active filter criteria (merged via `setFilters`). */
103
+ filters: Record<string, unknown>
104
+ /** Current pagination position. `limit` defaults to 20. */
105
+ pagination: { page: number; limit: number }
106
+ /**
107
+ * Internal, runtime-resolved configuration exposed so extend actions and
108
+ * plugins can build URLs, clean fields, or instantiate the entity class
109
+ * without re-reading user config.
110
+ */
111
+ _options: {
112
+ /** The raw endpoint segment supplied to the factory (e.g. `'sources'`). */
113
+ endpoint: string
114
+ /** Fields stripped by `cleanForSave` before POST/PUT. */
115
+ cleanFields: readonly string[]
116
+ /** Fully-qualified base URL for this store's REST endpoint, already `prefixUrl`-normalized. */
117
+ baseApiUrl: string
118
+ /** Entity class constructor, or `null` when the store returns raw data. */
119
+ entity: EntityClass<T> | null
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Actions available on every store regardless of configuration. Extend
125
+ * actions with the same name replace these (see `MergedActions`).
126
+ */
127
+ export interface BaseActions<T> {
128
+ /** Set the active item. Wraps in the configured Entity class when present; pass `null` to clear. */
129
+ setItem(data: T | Partial<T> | null): void
130
+ /** Replace the item list. Maps each entry through the Entity class when configured. */
131
+ setList(data: Array<T | Partial<T>>): void
132
+ /** Set `pagination.page` and `pagination.limit`. `limit` defaults to 20. */
133
+ setPagination(page: number, limit?: number): void
134
+ /** Merge filter key/value pairs into the current `filters` object. */
135
+ setFilters(filters: Record<string, unknown>): void
136
+ /** GET the list endpoint. Optional `search` is appended as `?_search=`; `soft=true` skips the loading toggle. */
137
+ refreshList(search?: string | null, soft?: boolean): Promise<{ response: Response; data: T[] }>
138
+ /** GET `/:id`, set it as the active item, and return the raw response data. */
139
+ getOne(id: string | number): Promise<T>
140
+ /** DELETE `/:id`, refresh the list, and clear the active item. */
141
+ deleteOne(idOrItem: string | number | { id: string | number }): Promise<{ response: Response }>
142
+ /** POST or PUT the item depending on whether it has an `id`, then refresh the list. */
143
+ save(item: Partial<T>): Promise<{ response: Response; data: T }>
144
+ /** Return a copy of `item` with `cleanFields` stripped, suitable for POST/PUT bodies. */
145
+ cleanForSave(item: T | Partial<T>): Partial<T>
146
+ }
147
+
148
+ /**
149
+ * Action merge rule:
150
+ * - Base actions not overridden by the extend block are preserved.
151
+ * - Actions declared in `extend.actions` replace base actions of the same name.
152
+ * - `viewMode` feature action is appended when the flag is set.
153
+ * - Plugin-contributed actions are reachable via the loose index signature
154
+ * on `PluginActionContribution` below (typed as `any`-returning callables).
155
+ */
156
+ export type PluginActionContribution = { [key: string]: (...args: any[]) => any }
157
+
158
+ export type MergedActions<T, ExtActions, F> =
159
+ Omit<BaseActions<T>, keyof ExtActions> & ExtActions & ViewModeActions<F> & PluginActionContribution
160
+
161
+ // ─────────────────────────────────────────────────────────────────────────────
162
+ // Full store shape (state + getters + actions) used inside ThisType<...>
163
+ // ─────────────────────────────────────────────────────────────────────────────
164
+
165
+ // Plugins contribute arbitrary state, getters, and actions at runtime. We spread
166
+ // a loose index signature into the resolved store shape so plugin-contributed
167
+ // members are reachable via dot access without `as any`. The index signature
168
+ // returns `any` (not `unknown`) so callable plugin actions (`store.refreshLogs()`)
169
+ // type-check without an intermediate cast — trade-off is that typos on
170
+ // plugin-contributed members are not caught. Plugins needing strict typing
171
+ // should ship their own `.d.ts` augmenting the specific store.
172
+ export type PluginContribution = { [key: string]: any }
173
+
174
+ export type FullState<T, ExtState, F> =
175
+ BaseState<T> & ExtState & LoadingState<F> & ViewModeState<F> & PluginContribution
176
+
177
+ export type FullGetters<ExtGetters, F> =
178
+ ExtGetters & LoadingGetters<F> & ViewModeGetters<F>
179
+
180
+ export type StoreThis<T, ExtState, ExtGetters, ExtActions, F> =
181
+ FullState<T, ExtState, F> &
182
+ // Getters are callable-free on the Pinia store instance — accessed as properties.
183
+ // We narrow each getter declaration to its return type so `this.myGetter` is typed.
184
+ { [K in keyof ExtGetters]: ExtGetters[K] extends (state: any) => infer R ? R : never } &
185
+ LoadingGetters<F> & ViewModeGetters<F> &
186
+ MergedActions<T, ExtActions, F>
187
+
188
+ // ─────────────────────────────────────────────────────────────────────────────
189
+ // Plugins
190
+ // ─────────────────────────────────────────────────────────────────────────────
191
+
192
+ /**
193
+ * A plugin definition. Merged into the store at creation time:
194
+ * state is spread into store state, getters into getters, actions into actions.
195
+ *
196
+ * Merge precedence: base actions → plugin actions → extend.actions.
197
+ *
198
+ * Plugin-contributed properties are loosely typed on the resulting store
199
+ * (they appear as unknown state / any-returning actions). Consumers needing
200
+ * strict types on plugin output should augment the store's types at the
201
+ * call site, or use a plugin that ships a dedicated `.d.ts` with the shape.
202
+ *
203
+ * If `setup` is provided, it is invoked once per store instance the first
204
+ * time `useStore()` resolves it. Use this to register
205
+ * `store.$onAction` / `store.$subscribe` observers that react to base or
206
+ * other plugin actions without having to override them.
207
+ */
208
+ export interface CrudPlugin {
209
+ /** Unique plugin identifier; used by `clearAllSubResources` and for debugging. */
210
+ name: string
211
+ /** State factory returning the state fields this plugin contributes. */
212
+ state?: () => Record<string, unknown>
213
+ /** Getters this plugin contributes — each receives the store state. */
214
+ getters?: Record<string, (state: any) => unknown>
215
+ /** Actions this plugin contributes. May override a base action with the same name. */
216
+ actions?: Record<string, (...args: any[]) => any>
217
+ /**
218
+ * Optional lifecycle hook run once per store instance, the first time
219
+ * `useStore()` resolves it. Typically used to register
220
+ * `store.$onAction` / `store.$subscribe` subscribers so the plugin can
221
+ * react to base or other-plugin actions without overriding them.
222
+ */
223
+ setup?: (store: any) => void
224
+ }
225
+
226
+ export interface PluginContrib {
227
+ [key: string]: unknown
228
+ }
229
+
230
+ // ─────────────────────────────────────────────────────────────────────────────
231
+ // Config & extend shapes
232
+ // ─────────────────────────────────────────────────────────────────────────────
233
+
234
+ /**
235
+ * Domain-specific additions layered on top of the base store. Merged last,
236
+ * so `extend` can still override anything a plugin contributed.
237
+ */
238
+ export interface ExtendConfig<
239
+ T,
240
+ ExtState extends Record<string, unknown>,
241
+ ExtGetters extends _GettersTree<BaseState<T> & ExtState>,
242
+ ExtActions extends Record<string, (...args: any[]) => any>,
243
+ F extends Features,
244
+ > {
245
+ /** State factory returning extra state properties merged into the store. */
246
+ state?: () => ExtState
247
+ /** Extra getters, or overrides of base/plugin getters with the same name. */
248
+ getters?: ExtGetters & ThisType<StoreThis<T, ExtState, ExtGetters, ExtActions, F>>
249
+ /** Extra actions, or overrides of base/plugin actions with the same name. */
250
+ actions?: ExtActions & ThisType<StoreThis<T, ExtState, ExtGetters, ExtActions, F>>
251
+ }
252
+
253
+ /**
254
+ * Full configuration object accepted by `createCrudStore`. Mirrors the
255
+ * runtime options documented in `createCrudStore.js`.
256
+ */
257
+ export interface CrudConfig<
258
+ T,
259
+ ExtState extends Record<string, unknown>,
260
+ ExtGetters extends _GettersTree<BaseState<T> & ExtState>,
261
+ ExtActions extends Record<string, (...args: any[]) => any>,
262
+ F extends Features,
263
+ Entity = EntityClass<T> | null | undefined,
264
+ > {
265
+ /** API resource path segment appended to `baseUrl` (e.g. `'sources'`). Required. */
266
+ endpoint: string
267
+ /** API base URL before the endpoint. Defaults to `'/apps/openregister/api'`. */
268
+ baseUrl?: string
269
+ /** Entity class constructor used to wrap raw API data; pass `null` for raw data. */
270
+ entity?: Entity
271
+ /** Fields stripped from items before POST/PUT. Defaults to `['id','uuid','created','updated']`. */
272
+ cleanFields?: readonly string[]
273
+ /** Feature flags enabling optional state/getters/actions. See `Features`. */
274
+ features?: F
275
+ /**
276
+ * Custom parser for `refreshList`'s JSON response body. Called with the
277
+ * store as `this`, so it can also perform side-effects (e.g. storing
278
+ * pagination info). Must return an array of items. Default:
279
+ * `(json) => json.results`.
280
+ */
281
+ parseListResponse?: (this: StoreThis<T, ExtState, ExtGetters, ExtActions, F>, json: unknown) => T[] | Array<Partial<T>>
282
+ /**
283
+ * Plugin definitions merged into the store. Same shape as object-store
284
+ * plugins (`{ name, state?, getters?, actions?, setup? }`). Merge order
285
+ * is base → plugins → extend.
286
+ */
287
+ plugins?: readonly CrudPlugin[]
288
+ /** Extra state/getters/actions merged on top of base and plugin contributions. */
289
+ extend?: ExtendConfig<T, ExtState, ExtGetters, ExtActions, F>
290
+ }
291
+
292
+ // ─────────────────────────────────────────────────────────────────────────────
293
+ // The factory — overloaded
294
+ // ─────────────────────────────────────────────────────────────────────────────
295
+
296
+ /**
297
+ * Overload 1 — entity inference.
298
+ *
299
+ * Provide `config.entity` as a class constructor; `T` is inferred as the
300
+ * instance type:
301
+ *
302
+ * ```ts
303
+ * const useSourceStore = createCrudStore('source', {
304
+ * endpoint: 'sources',
305
+ * entity: Source, // T = Source
306
+ * features: { loading: true },
307
+ * })
308
+ * ```
309
+ */
310
+ export function createCrudStore<
311
+ Entity extends EntityClass<any>,
312
+ const Id extends string = string,
313
+ const F extends Features = {},
314
+ ExtState extends Record<string, unknown> = {},
315
+ ExtGetters extends _GettersTree<BaseState<InferEntity<Entity>> & ExtState> = {},
316
+ ExtActions extends Record<string, (...args: any[]) => any> = {},
317
+ >(
318
+ name: Id,
319
+ config: CrudConfig<InferEntity<Entity>, ExtState, ExtGetters, ExtActions, F, Entity>,
320
+ ): StoreDefinition<
321
+ Id,
322
+ Prettify<FullState<InferEntity<Entity>, ExtState, F>>,
323
+ Prettify<FullGetters<ExtGetters, F>>,
324
+ Prettify<MergedActions<InferEntity<Entity>, ExtActions, F>>
325
+ >
326
+
327
+ /**
328
+ * Overload 2 — explicit `T` for raw-data stores (no entity class).
329
+ *
330
+ * ```ts
331
+ * interface LogShape { id: number; message: string }
332
+ * const useLogStore = createCrudStore<'log', LogShape>('log', { endpoint: 'logs' })
333
+ * ```
334
+ */
335
+ export function createCrudStore<
336
+ const Id extends string,
337
+ T,
338
+ const F extends Features = {},
339
+ ExtState extends Record<string, unknown> = {},
340
+ ExtGetters extends _GettersTree<BaseState<T> & ExtState> = {},
341
+ ExtActions extends Record<string, (...args: any[]) => any> = {},
342
+ >(
343
+ name: Id,
344
+ config: CrudConfig<T, ExtState, ExtGetters, ExtActions, F, null | undefined>,
345
+ ): StoreDefinition<
346
+ Id,
347
+ FullState<T, ExtState, F>,
348
+ FullGetters<ExtGetters, F>,
349
+ MergedActions<T, ExtActions, F>
350
+ >
@@ -1,6 +1,7 @@
1
1
  import { defineStore } from 'pinia'
2
2
  import { buildHeaders, prefixUrl } from '../utils/headers.js'
3
3
  import { parseResponseError } from '../utils/errors.js'
4
+ import { mergePluginState, mergePluginGetters, mergePluginActions } from './pluginMerge.js'
4
5
 
5
6
  /**
6
7
  * Default fields stripped from items before POST/PUT.
@@ -76,10 +77,14 @@ function defaultParseListResponse(json) {
76
77
  * @param {Function} [config.parseListResponse] Custom response parser for refreshList.
77
78
  * Receives the parsed JSON body with the store instance as `this`.
78
79
  * Must return an array of items. Default: `(json) => json.results`
80
+ * @param {Array} [config.plugins] Array of plugin definitions to merge into the store.
81
+ * Each plugin is `{ name, state?, getters?, actions? }` — same shape as object-store
82
+ * plugins. Merge order is base → plugins → extend, so `extend` can still override
83
+ * anything a plugin provides.
79
84
  * @param {object} [config.extend] Extra state/getters/actions to merge into the store
80
85
  * @param {Function} [config.extend.state] State factory returning extra state properties
81
86
  * @param {object} [config.extend.getters] Extra getters (or overrides of base getters)
82
- * @param {object} [config.extend.actions] Extra actions (or overrides of base actions)
87
+ * @param {object} [config.extend.actions] Extra actions (or overrides of base/plugin actions)
83
88
  * @return {Function} Pinia store composable (useXxxStore)
84
89
  */
85
90
  export function createCrudStore(name, config = {}) {
@@ -90,6 +95,7 @@ export function createCrudStore(name, config = {}) {
90
95
  cleanFields = DEFAULT_CLEAN_FIELDS,
91
96
  features = {},
92
97
  parseListResponse = defaultParseListResponse,
98
+ plugins = [],
93
99
  extend = {},
94
100
  } = config
95
101
 
@@ -99,7 +105,18 @@ export function createCrudStore(name, config = {}) {
99
105
 
100
106
  const baseApiUrl = prefixUrl(`${baseUrl}/${endpoint}`)
101
107
 
102
- return defineStore(name, {
108
+ const pluginState = mergePluginState(plugins)
109
+ const pluginGetters = mergePluginGetters(plugins)
110
+ const pluginActions = mergePluginActions(plugins)
111
+ const setupPlugins = plugins.filter((p) => typeof p.setup === 'function')
112
+ // Track which store instances have already been set up so plugin setup
113
+ // hooks run exactly once per instance, even if useStore() is called many
114
+ // times. WeakSet lets garbage collection reclaim entries when a Pinia
115
+ // instance (and therefore its stores) are discarded — e.g. between tests
116
+ // that call createPinia() afresh.
117
+ const initialized = new WeakSet()
118
+
119
+ const useStore = defineStore(name, {
103
120
  state: () => ({
104
121
  // ── Core state ──
105
122
  item: null,
@@ -111,8 +128,11 @@ export function createCrudStore(name, config = {}) {
111
128
  ...(features.loading ? { loading: false, error: null } : {}),
112
129
  ...(features.viewMode ? { viewMode: 'cards' } : {}),
113
130
 
114
- // ── Internal config (available to extend actions) ──
115
- _options: { endpoint, cleanFields, baseApiUrl },
131
+ // ── Plugin state ──
132
+ ...pluginState,
133
+
134
+ // ── Internal config (available to extend actions and plugins) ──
135
+ _options: { endpoint, cleanFields, baseApiUrl, entity: Entity },
116
136
 
117
137
  // ── Domain-specific state ──
118
138
  ...(typeof extend.state === 'function' ? extend.state() : {}),
@@ -128,6 +148,9 @@ export function createCrudStore(name, config = {}) {
128
148
  }
129
149
  : {}),
130
150
 
151
+ // ── Plugin getters ──
152
+ ...pluginGetters,
153
+
131
154
  // ── Domain-specific getters ──
132
155
  ...(extend.getters ?? {}),
133
156
  },
@@ -353,8 +376,38 @@ export function createCrudStore(name, config = {}) {
353
376
  }
354
377
  },
355
378
 
356
- // ── Domain-specific actions (may override base actions) ──
379
+ // ── Plugin actions (may override base actions) ──
380
+ ...pluginActions,
381
+
382
+ // ── Domain-specific actions (may override base/plugin actions) ──
357
383
  ...(extend.actions ?? {}),
358
384
  },
359
385
  })
386
+
387
+ // When no plugin declares a setup hook, return Pinia's composable
388
+ // directly — zero runtime overhead for the common case.
389
+ if (setupPlugins.length === 0) {
390
+ return useStore
391
+ }
392
+
393
+ /**
394
+ * Wrapped composable: resolves the Pinia store, then runs each plugin's
395
+ * `setup(store)` exactly once per instance. Plugins typically use the
396
+ * setup hook to register `store.$onAction` / `store.$subscribe`
397
+ * subscriptions that observe base or other plugin actions without
398
+ * overriding them.
399
+ *
400
+ * @param {import('pinia').Pinia} [pinia] Optional Pinia instance override
401
+ * @return {object} The Pinia store instance with all plugin setups applied
402
+ */
403
+ return function useCrudStore(pinia) {
404
+ const store = useStore(pinia)
405
+ if (!initialized.has(store)) {
406
+ initialized.add(store)
407
+ for (const plugin of setupPlugins) {
408
+ plugin.setup(store)
409
+ }
410
+ }
411
+ return store
412
+ }
360
413
  }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Shared helpers for merging Pinia store plugins into a store definition.
3
+ *
4
+ * Used by both `createObjectStore` (via `useObjectStore.js`) and
5
+ * `createCrudStore`, so plugin authors get the same shape everywhere:
6
+ * `{ name, state?, getters?, actions? }`.
7
+ */
8
+
9
+ /**
10
+ * Merge plugin state factories into a single state object.
11
+ *
12
+ * @param {Array} plugins Array of plugin definitions
13
+ * @return {object} Merged state object
14
+ */
15
+ export function mergePluginState(plugins) {
16
+ const merged = {}
17
+ for (const plugin of plugins) {
18
+ if (plugin.state) {
19
+ Object.assign(merged, plugin.state())
20
+ }
21
+ }
22
+ return merged
23
+ }
24
+
25
+ /**
26
+ * Merge plugin getters into a single getters object.
27
+ *
28
+ * @param {Array} plugins Array of plugin definitions
29
+ * @return {object} Merged getters object
30
+ */
31
+ export function mergePluginGetters(plugins) {
32
+ const merged = {}
33
+ for (const plugin of plugins) {
34
+ if (plugin.getters) {
35
+ Object.assign(merged, plugin.getters)
36
+ }
37
+ }
38
+ return merged
39
+ }
40
+
41
+ /**
42
+ * Merge plugin actions into a single actions object.
43
+ *
44
+ * @param {Array} plugins Array of plugin definitions
45
+ * @return {object} Merged actions object
46
+ */
47
+ export function mergePluginActions(plugins) {
48
+ const merged = {}
49
+ for (const plugin of plugins) {
50
+ if (plugin.actions) {
51
+ Object.assign(merged, plugin.actions)
52
+ }
53
+ }
54
+ return merged
55
+ }
@@ -2,6 +2,7 @@ export { auditTrailsPlugin } from './auditTrails.js'
2
2
  export { relationsPlugin } from './relations.js'
3
3
  export { filesPlugin } from './files.js'
4
4
  export { lifecyclePlugin } from './lifecycle.js'
5
+ export { logsPlugin } from './logs.js'
5
6
  export { registerMappingPlugin } from './registerMapping.js'
6
7
  export { selectionPlugin } from './selection.js'
7
8
  export { searchPlugin, SEARCH_TYPE, getRegisterApiUrl, getSchemaApiUrl } from './search.js'
@@ -0,0 +1,22 @@
1
+ import type { CrudPlugin } from '../createCrudStore'
2
+
3
+ export interface LogsPluginOptions {
4
+ /** Required. Query-param name carrying the active item's id (e.g. 'source_id'). */
5
+ parentIdParam: string
6
+ /** Path segment appended to the store's base API URL. Default: 'logs'. */
7
+ path?: string
8
+ /** Default query params merged before caller-supplied filters. Default: `{ '_sort[created]': 'desc' }`. */
9
+ defaultSort?: Record<string, string>
10
+ /**
11
+ * When true, the plugin's `setup` hook registers a `store.$onAction`
12
+ * subscriber that auto-fires `refreshLogs()` after every `setItem` with
13
+ * an id (or `clearLogs()` when the item is cleared). Composes with other
14
+ * plugins that observe `setItem`.
15
+ */
16
+ autoRefreshOnItemChange?: boolean
17
+ }
18
+
19
+ /**
20
+ * Create a logs sub-resource plugin for a `createCrudStore`.
21
+ */
22
+ export function logsPlugin(options: LogsPluginOptions): CrudPlugin