@directus/composables 11.2.2 → 11.2.4

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 (3) hide show
  1. package/dist/index.d.ts +134 -124
  2. package/dist/index.js +1455 -658
  3. package/package.json +14 -14
package/dist/index.js CHANGED
@@ -1,703 +1,1500 @@
1
- // src/use-collection.ts
2
- import { computed, ref } from "vue";
3
-
4
- // src/use-system.ts
1
+ import { computed, defineComponent, inject, isRef, nextTick, onBeforeUnmount, onMounted, onUnmounted, provide, reactive, ref, shallowRef, toRef, toRefs, unref, watch } from "vue";
5
2
  import { API_INJECT, EXTENSIONS_INJECT, SDK_INJECT, STORES_INJECT } from "@directus/constants";
6
- import { inject } from "vue";
3
+ import { nanoid } from "nanoid";
4
+ import { debounce, isEqual, isNil } from "lodash-es";
5
+ import { getEndpoint, moveInArray } from "@directus/utils";
6
+ import axios from "axios";
7
+
8
+ //#region src/use-system.ts
9
+ /**
10
+ * Vue composable that provides access to the global Directus stores through dependency injection.
11
+ *
12
+ * This composable injects the stores object that contains all the Pinia stores used throughout
13
+ * the Directus application, including user store, permissions store, collections store, etc.
14
+ *
15
+ * @returns The injected stores object containing all application stores
16
+ * @throws Error if the stores could not be found in the injection context
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * import { useStores } from '@directus/composables';
21
+ *
22
+ * export default defineComponent({
23
+ * setup() {
24
+ * const stores = useStores();
25
+ *
26
+ * // Access specific stores
27
+ * const userStore = stores.useUserStore();
28
+ * const collectionsStore = stores.useCollectionsStore();
29
+ * const permissionsStore = stores.usePermissionsStore();
30
+ *
31
+ * return {
32
+ * userInfo: userStore.currentUser,
33
+ * collections: collectionsStore.collections,
34
+ * permissions: permissionsStore.permissions
35
+ * };
36
+ * }
37
+ * });
38
+ * ```
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * // Using in a component with reactive store data
43
+ * import { useStores } from '@directus/composables';
44
+ * import { computed } from 'vue';
45
+ *
46
+ * export default defineComponent({
47
+ * setup() {
48
+ * const stores = useStores();
49
+ * const userStore = stores.useUserStore();
50
+ *
51
+ * const isAdmin = computed(() => {
52
+ * return userStore.currentUser?.role?.admin_access === true;
53
+ * });
54
+ *
55
+ * const hasCreatePermission = computed(() => {
56
+ * const permissionsStore = stores.usePermissionsStore();
57
+ * return permissionsStore.hasPermission('directus_files', 'create');
58
+ * });
59
+ *
60
+ * return { isAdmin, hasCreatePermission };
61
+ * }
62
+ * });
63
+ * ```
64
+ */
7
65
  function useStores() {
8
- const stores = inject(STORES_INJECT);
9
- if (!stores) throw new Error("[useStores]: The stores could not be found.");
10
- return stores;
66
+ const stores = inject(STORES_INJECT);
67
+ if (!stores) throw new Error("[useStores]: The stores could not be found.");
68
+ return stores;
11
69
  }
70
+ /**
71
+ * Vue composable that provides access to the Axios HTTP client instance through dependency injection.
72
+ *
73
+ * This composable injects the configured Axios instance that is set up with the proper base URL,
74
+ * authentication headers, interceptors, and other configuration needed to communicate with the
75
+ * Directus API. It provides a convenient way to make HTTP requests from components and composables.
76
+ *
77
+ * @returns The injected Axios instance configured for Directus API communication
78
+ * @throws Error if the API instance could not be found in the injection context
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * import { useApi } from '@directus/composables';
83
+ *
84
+ * export default defineComponent({
85
+ * setup() {
86
+ * const api = useApi();
87
+ *
88
+ * const fetchUserData = async (userId: string) => {
89
+ * try {
90
+ * const response = await api.get(`/users/${userId}`);
91
+ * return response.data;
92
+ * } catch (error) {
93
+ * console.error('Failed to fetch user data:', error);
94
+ * throw error;
95
+ * }
96
+ * };
97
+ *
98
+ * return { fetchUserData };
99
+ * }
100
+ * });
101
+ * ```
102
+ *
103
+ * @example
104
+ * ```typescript
105
+ * // Using with reactive data and error handling
106
+ * import { useApi } from '@directus/composables';
107
+ * import { ref, onMounted } from 'vue';
108
+ *
109
+ * export default defineComponent({
110
+ * setup() {
111
+ * const api = useApi();
112
+ * const collections = ref([]);
113
+ * const loading = ref(false);
114
+ * const error = ref(null);
115
+ *
116
+ * const loadCollections = async () => {
117
+ * loading.value = true;
118
+ * error.value = null;
119
+ *
120
+ * try {
121
+ * const response = await api.get('/collections');
122
+ * collections.value = response.data.data;
123
+ * } catch (err) {
124
+ * error.value = err.response?.data?.errors?.[0]?.message || 'Failed to load collections';
125
+ * } finally {
126
+ * loading.value = false;
127
+ * }
128
+ * };
129
+ *
130
+ * onMounted(loadCollections);
131
+ *
132
+ * return { collections, loading, error, loadCollections };
133
+ * }
134
+ * });
135
+ * ```
136
+ */
12
137
  function useApi() {
13
- const api = inject(API_INJECT);
14
- if (!api) throw new Error("[useApi]: The api could not be found.");
15
- return api;
138
+ const api = inject(API_INJECT);
139
+ if (!api) throw new Error("[useApi]: The api could not be found.");
140
+ return api;
16
141
  }
142
+ /**
143
+ * Vue composable that provides access to the Directus SDK client instance through dependency injection.
144
+ *
145
+ * This composable injects the configured Directus SDK client that provides a type-safe, modern API
146
+ * for interacting with Directus. The SDK offers methods for CRUD operations, authentication, file
147
+ * management, and more, with full TypeScript support and automatic type inference based on your schema.
148
+ *
149
+ * @template Schema - The TypeScript schema type for your Directus instance, defaults to `any`
150
+ * @returns The injected Directus SDK client with REST client capabilities
151
+ * @throws Error if the SDK instance could not be found in the injection context
152
+ *
153
+ * @example
154
+ * ```typescript
155
+ * import { useSdk } from '@directus/composables';
156
+ *
157
+ * // Using with default schema
158
+ * export default defineComponent({
159
+ * setup() {
160
+ * const sdk = useSdk();
161
+ *
162
+ * const fetchArticles = async () => {
163
+ * try {
164
+ * const articles = await sdk.items('articles').readByQuery({
165
+ * filter: { status: { _eq: 'published' } },
166
+ * sort: ['-date_created'],
167
+ * limit: 10
168
+ * });
169
+ * return articles;
170
+ * } catch (error) {
171
+ * console.error('Failed to fetch articles:', error);
172
+ * throw error;
173
+ * }
174
+ * };
175
+ *
176
+ * return { fetchArticles };
177
+ * }
178
+ * });
179
+ * ```
180
+ *
181
+ * @example
182
+ * ```typescript
183
+ * // Using with typed schema for better type safety
184
+ * import { useSdk } from '@directus/composables';
185
+ *
186
+ * interface MySchema {
187
+ * articles: {
188
+ * id: string;
189
+ * title: string;
190
+ * content: string;
191
+ * status: 'draft' | 'published';
192
+ * author: string;
193
+ * date_created: string;
194
+ * };
195
+ * authors: {
196
+ * id: string;
197
+ * name: string;
198
+ * email: string;
199
+ * };
200
+ * }
201
+ *
202
+ * export default defineComponent({
203
+ * setup() {
204
+ * const sdk = useSdk<MySchema>();
205
+ *
206
+ * const createArticle = async (articleData: Partial<MySchema['articles']>) => {
207
+ * try {
208
+ * const newArticle = await sdk.items('articles').createOne(articleData);
209
+ * return newArticle; // Fully typed return value
210
+ * } catch (error) {
211
+ * console.error('Failed to create article:', error);
212
+ * throw error;
213
+ * }
214
+ * };
215
+ *
216
+ * const updateArticle = async (id: string, updates: Partial<MySchema['articles']>) => {
217
+ * try {
218
+ * const updatedArticle = await sdk.items('articles').updateOne(id, updates);
219
+ * return updatedArticle; // Type-safe updates
220
+ * } catch (error) {
221
+ * console.error('Failed to update article:', error);
222
+ * throw error;
223
+ * }
224
+ * };
225
+ *
226
+ * return { createArticle, updateArticle };
227
+ * }
228
+ * });
229
+ * ```
230
+ */
17
231
  function useSdk() {
18
- const sdk = inject(SDK_INJECT);
19
- if (!sdk) throw new Error("[useSdk]: The sdk could not be found.");
20
- return sdk;
232
+ const sdk = inject(SDK_INJECT);
233
+ if (!sdk) throw new Error("[useSdk]: The sdk could not be found.");
234
+ return sdk;
21
235
  }
236
+ /**
237
+ * Vue composable that provides access to the registered Directus extensions through dependency injection.
238
+ *
239
+ * This composable injects the extensions configuration object that contains all registered app
240
+ * extensions including interfaces, displays, layouts, modules, panels, operations, and more.
241
+ * The extensions are provided as reactive references and can be used to dynamically access
242
+ * and utilize custom functionality within the Directus application.
243
+ *
244
+ * @returns A reactive record of extension configurations organized by extension type
245
+ * @throws Error if the extensions could not be found in the injection context
246
+ *
247
+ * @example
248
+ * ```typescript
249
+ * import { useExtensions } from '@directus/composables';
250
+ *
251
+ * export default defineComponent({
252
+ * setup() {
253
+ * const extensions = useExtensions();
254
+ *
255
+ * const getAvailableInterfaces = () => {
256
+ * return Object.values(extensions.interfaces || {});
257
+ * };
258
+ *
259
+ * const getAvailableDisplays = () => {
260
+ * return Object.values(extensions.displays || {});
261
+ * };
262
+ *
263
+ * const findInterfaceByName = (name: string) => {
264
+ * return extensions.interfaces?.[name] || null;
265
+ * };
266
+ *
267
+ * return {
268
+ * getAvailableInterfaces,
269
+ * getAvailableDisplays,
270
+ * findInterfaceByName
271
+ * };
272
+ * }
273
+ * });
274
+ * ```
275
+ *
276
+ * @example
277
+ * ```typescript
278
+ * // Using with computed properties for reactive extension lists
279
+ * import { useExtensions } from '@directus/composables';
280
+ * import { computed } from 'vue';
281
+ *
282
+ * export default defineComponent({
283
+ * setup() {
284
+ * const extensions = useExtensions();
285
+ *
286
+ * const availableLayouts = computed(() => {
287
+ * return Object.entries(extensions.layouts || {}).map(([key, config]) => ({
288
+ * id: key,
289
+ * name: config.name,
290
+ * icon: config.icon,
291
+ * component: config.component
292
+ * }));
293
+ * });
294
+ *
295
+ * const customModules = computed(() => {
296
+ * return Object.values(extensions.modules || {}).filter(module =>
297
+ * !module.preRegisterCheck || module.preRegisterCheck()
298
+ * );
299
+ * });
300
+ *
301
+ * const operationsByGroup = computed(() => {
302
+ * const operations = Object.values(extensions.operations || {});
303
+ * return operations.reduce((groups, operation) => {
304
+ * const group = operation.overview?.group || 'other';
305
+ * if (!groups[group]) groups[group] = [];
306
+ * groups[group].push(operation);
307
+ * return groups;
308
+ * }, {} as Record<string, any[]>);
309
+ * });
310
+ *
311
+ * return {
312
+ * availableLayouts,
313
+ * customModules,
314
+ * operationsByGroup
315
+ * };
316
+ * }
317
+ * });
318
+ * ```
319
+ */
22
320
  function useExtensions() {
23
- const extensions = inject(EXTENSIONS_INJECT);
24
- if (!extensions) throw new Error("[useExtensions]: The extensions could not be found.");
25
- return extensions;
321
+ const extensions = inject(EXTENSIONS_INJECT);
322
+ if (!extensions) throw new Error("[useExtensions]: The extensions could not be found.");
323
+ return extensions;
26
324
  }
27
325
 
28
- // src/use-collection.ts
326
+ //#endregion
327
+ //#region src/use-collection.ts
328
+ /**
329
+ * A Vue composable that provides reactive access to collection metadata, fields, and computed properties.
330
+ *
331
+ * This composable serves as a centralized way to access and work with Directus collections,
332
+ * providing reactive computed properties for collection information, field definitions,
333
+ * default values, and various collection-specific metadata.
334
+ *
335
+ * @param collectionKey - The collection identifier. Can be a string or a reactive reference to a string.
336
+ * If null, most computed properties will return empty/null values.
337
+ *
338
+ * @returns An object containing reactive computed properties for the collection:
339
+ * - `info` - The complete collection configuration object or null if not found
340
+ * - `fields` - Array of sorted field definitions for the collection
341
+ * - `defaults` - Object mapping field names to their default values from schema
342
+ * - `primaryKeyField` - The field marked as primary key, or null if none exists
343
+ * - `userCreatedField` - The field with 'user_created' special type, or null if none exists
344
+ * - `sortField` - The field name used for sorting, from collection meta, or null
345
+ * - `isSingleton` - Boolean indicating if the collection is configured as a singleton
346
+ * - `accountabilityScope` - The accountability scope setting ('all', 'activity', or null)
347
+ *
348
+ * @example
349
+ * ```typescript
350
+ * // Using with a static collection name
351
+ * const { info, fields, primaryKeyField } = useCollection('users');
352
+ *
353
+ * // Using with a reactive collection name
354
+ * const collectionName = ref('articles');
355
+ * const { fields, defaults, isSingleton } = useCollection(collectionName);
356
+ *
357
+ * // Accessing properties
358
+ * console.log(info.value?.name); // Collection display name
359
+ * console.log(fields.value.length); // Number of fields
360
+ * console.log(primaryKeyField.value?.field); // Primary key field name
361
+ * ```
362
+ */
29
363
  function useCollection(collectionKey) {
30
- const { useCollectionsStore, useFieldsStore } = useStores();
31
- const collectionsStore = useCollectionsStore();
32
- const fieldsStore = useFieldsStore();
33
- const collection = typeof collectionKey === "string" ? ref(collectionKey) : collectionKey;
34
- const info = computed(() => {
35
- return collectionsStore.collections.find(({ collection: key }) => key === collection.value) || null;
36
- });
37
- const fields = computed(() => {
38
- if (!collection.value) return [];
39
- return fieldsStore.getFieldsForCollectionSorted(collection.value);
40
- });
41
- const defaults = computed(() => {
42
- if (!fields.value) return {};
43
- const defaults2 = {};
44
- for (const field of fields.value) {
45
- if (field.schema !== null && "default_value" in field.schema) {
46
- defaults2[field.field] = field.schema.default_value;
47
- }
48
- }
49
- return defaults2;
50
- });
51
- const primaryKeyField = computed(() => {
52
- return fields.value.find((field) => field.collection === collection.value && field.schema?.is_primary_key === true) || null;
53
- });
54
- const userCreatedField = computed(() => {
55
- return fields.value?.find((field) => (field.meta?.special || []).includes("user_created")) || null;
56
- });
57
- const sortField = computed(() => {
58
- return info.value?.meta?.sort_field || null;
59
- });
60
- const isSingleton = computed(() => {
61
- return info.value?.meta?.singleton === true;
62
- });
63
- const accountabilityScope = computed(() => {
64
- return info.value?.meta?.accountability || null;
65
- });
66
- return { info, fields, defaults, primaryKeyField, userCreatedField, sortField, isSingleton, accountabilityScope };
364
+ const { useCollectionsStore, useFieldsStore } = useStores();
365
+ const collectionsStore = useCollectionsStore();
366
+ const fieldsStore = useFieldsStore();
367
+ const collection = typeof collectionKey === "string" ? ref(collectionKey) : collectionKey;
368
+ const info = computed(() => {
369
+ return collectionsStore.collections.find(({ collection: key }) => key === collection.value) || null;
370
+ });
371
+ const fields = computed(() => {
372
+ if (!collection.value) return [];
373
+ return fieldsStore.getFieldsForCollectionSorted(collection.value);
374
+ });
375
+ return {
376
+ info,
377
+ fields,
378
+ defaults: computed(() => {
379
+ if (!fields.value) return {};
380
+ const defaults = {};
381
+ for (const field of fields.value) if (field.schema !== null && "default_value" in field.schema) defaults[field.field] = field.schema.default_value;
382
+ return defaults;
383
+ }),
384
+ primaryKeyField: computed(() => {
385
+ return fields.value.find((field) => field.collection === collection.value && field.schema?.is_primary_key === true) || null;
386
+ }),
387
+ userCreatedField: computed(() => {
388
+ return fields.value?.find((field) => (field.meta?.special || []).includes("user_created")) || null;
389
+ }),
390
+ sortField: computed(() => {
391
+ return info.value?.meta?.sort_field || null;
392
+ }),
393
+ isSingleton: computed(() => {
394
+ return info.value?.meta?.singleton === true;
395
+ }),
396
+ accountabilityScope: computed(() => {
397
+ return info.value?.meta?.accountability || null;
398
+ })
399
+ };
67
400
  }
68
401
 
69
- // src/use-custom-selection.ts
70
- import { nanoid } from "nanoid";
71
- import { computed as computed2, ref as ref2, watch } from "vue";
402
+ //#endregion
403
+ //#region src/use-custom-selection.ts
404
+ /**
405
+ * A Vue composable for managing custom selection values that aren't present in a predefined list of items.
406
+ *
407
+ * This composable is typically used in form components where users can select from a predefined list
408
+ * of options, but also have the ability to enter custom values that aren't in the list. It manages
409
+ * the state and logic for detecting when a custom value is being used and provides a reactive
410
+ * interface for getting and setting custom values.
411
+ *
412
+ * @param currentValue - A reactive reference to the currently selected value. Can be null if no value is selected.
413
+ * @param items - A reactive reference to the array of available predefined items. Each item should have a 'value' property.
414
+ * @param emit - A callback function to emit value changes to the parent component.
415
+ *
416
+ * @returns An object containing:
417
+ * - `otherValue` - A computed ref for getting/setting custom values. Returns current value when using custom,
418
+ * empty string otherwise. Setting triggers the emit callback.
419
+ * - `usesOtherValue` - A computed boolean indicating whether the current value is a custom value
420
+ * (not found in the predefined items list).
421
+ *
422
+ * @example
423
+ * ```typescript
424
+ * const currentValue = ref('custom-option');
425
+ * const items = ref([
426
+ * { value: 'option1', label: 'Option 1' },
427
+ * { value: 'option2', label: 'Option 2' }
428
+ * ]);
429
+ * const emit = (value: string | null) => console.log('Value changed:', value);
430
+ *
431
+ * const { otherValue, usesOtherValue } = useCustomSelection(currentValue, items, emit);
432
+ *
433
+ * console.log(usesOtherValue.value); // true (custom-option not in items)
434
+ * console.log(otherValue.value); // 'custom-option'
435
+ *
436
+ * otherValue.value = 'new-custom-value'; // Triggers emit with 'new-custom-value'
437
+ * ```
438
+ */
72
439
  function useCustomSelection(currentValue, items, emit) {
73
- const localOtherValue = ref2("");
74
- const otherValue = computed2({
75
- get() {
76
- return localOtherValue.value || (usesOtherValue.value ? currentValue.value : "");
77
- },
78
- set(newValue) {
79
- if (newValue === null) {
80
- localOtherValue.value = "";
81
- emit(null);
82
- } else {
83
- localOtherValue.value = newValue;
84
- emit(newValue);
85
- }
86
- }
87
- });
88
- const usesOtherValue = computed2(() => {
89
- if (items.value === null) return false;
90
- const values = items.value.map((item) => item.value);
91
- return currentValue.value !== null && currentValue.value.length > 0 && values.includes(currentValue.value) === false;
92
- });
93
- return { otherValue, usesOtherValue };
440
+ const localOtherValue = ref("");
441
+ const otherValue = computed({
442
+ get() {
443
+ return localOtherValue.value || (usesOtherValue.value ? currentValue.value : "");
444
+ },
445
+ set(newValue) {
446
+ if (newValue === null) {
447
+ localOtherValue.value = "";
448
+ emit(null);
449
+ } else {
450
+ localOtherValue.value = newValue;
451
+ emit(newValue);
452
+ }
453
+ }
454
+ });
455
+ const usesOtherValue = computed(() => {
456
+ if (items.value === null) return false;
457
+ const values = items.value.map((item) => item.value);
458
+ return currentValue.value !== null && currentValue.value.length > 0 && values.includes(currentValue.value) === false;
459
+ });
460
+ return {
461
+ otherValue,
462
+ usesOtherValue
463
+ };
94
464
  }
465
+ /**
466
+ * A Vue composable for managing multiple custom selection values that aren't present in a predefined list of items.
467
+ *
468
+ * This composable extends the single custom selection pattern to support multiple values. It's typically used
469
+ * in multi-select form components where users can select multiple predefined options and also add custom
470
+ * values that aren't in the predefined list. It automatically detects custom values in the current selection,
471
+ * manages their state, and provides functions for adding and updating custom values.
472
+ *
473
+ * @param currentValues - A reactive reference to the currently selected values array. Can be null if no values are selected.
474
+ * @param items - A reactive reference to the array of available predefined items. Each item should have a 'value' property.
475
+ * @param emit - A callback function to emit value changes to the parent component.
476
+ *
477
+ * @returns An object containing:
478
+ * - `otherValues` - A reactive array of custom value objects, each with a unique key, value, and optional focus state.
479
+ * - `addOtherValue` - A function to add a new custom value with optional value and focus parameters.
480
+ * - `setOtherValue` - A function to update or remove a custom value by its key, automatically syncing with currentValues.
481
+ *
482
+ * @example
483
+ * ```typescript
484
+ * const currentValues = ref(['option1', 'custom-value1', 'custom-value2']);
485
+ * const items = ref([
486
+ * { value: 'option1', label: 'Option 1' },
487
+ * { value: 'option2', label: 'Option 2' }
488
+ * ]);
489
+ * const emit = (values: string[] | null) => console.log('Values changed:', values);
490
+ *
491
+ * const { otherValues, addOtherValue, setOtherValue } = useCustomSelectionMultiple(currentValues, items, emit);
492
+ *
493
+ * console.log(otherValues.value); // [{ key: 'abc123', value: 'custom-value1' }, { key: 'def456', value: 'custom-value2' }]
494
+ *
495
+ * // Add a new custom value
496
+ * addOtherValue('new-custom-value', true);
497
+ *
498
+ * // Update an existing custom value
499
+ * setOtherValue('abc123', 'updated-custom-value');
500
+ *
501
+ * // Remove a custom value
502
+ * setOtherValue('def456', null);
503
+ * ```
504
+ */
95
505
  function useCustomSelectionMultiple(currentValues, items, emit) {
96
- const otherValues = ref2([]);
97
- watch(
98
- currentValues,
99
- (newValue) => {
100
- if (newValue === null) return;
101
- if (!Array.isArray(newValue)) return;
102
- if (items.value === null) return;
103
- newValue.forEach((value) => {
104
- if (items.value === null) return;
105
- const values = items.value.map((item) => item.value);
106
- const existsInValues = values.includes(value);
107
- if (!existsInValues) {
108
- const other = otherValues.value.map((o) => o.value);
109
- const existsInOtherValues = other.includes(value);
110
- if (!existsInOtherValues) {
111
- addOtherValue(value);
112
- }
113
- }
114
- });
115
- },
116
- { immediate: true }
117
- );
118
- return { otherValues, addOtherValue, setOtherValue };
119
- function addOtherValue(value = "", focus = false) {
120
- otherValues.value = [
121
- ...otherValues.value,
122
- {
123
- key: nanoid(),
124
- value,
125
- focus
126
- }
127
- ];
128
- }
129
- function setOtherValue(key, newValue) {
130
- const previousValue = otherValues.value.find((o) => o.key === key);
131
- const valueWithoutPrevious = (currentValues.value || []).filter(
132
- (val) => val !== previousValue?.value
133
- );
134
- if (newValue === null) {
135
- otherValues.value = otherValues.value.filter((o) => o.key !== key);
136
- if (valueWithoutPrevious.length === 0) {
137
- emit(null);
138
- } else {
139
- emit(valueWithoutPrevious);
140
- }
141
- } else {
142
- otherValues.value = otherValues.value.map((otherValue) => {
143
- if (otherValue.key === key) otherValue.value = newValue;
144
- return otherValue;
145
- });
146
- if (valueWithoutPrevious.length === currentValues.value?.length) {
147
- emit(valueWithoutPrevious);
148
- } else {
149
- emit([...valueWithoutPrevious, newValue]);
150
- }
151
- }
152
- }
506
+ const otherValues = ref([]);
507
+ watch(currentValues, (newValue) => {
508
+ if (newValue === null) return;
509
+ if (!Array.isArray(newValue)) return;
510
+ if (items.value === null) return;
511
+ newValue.forEach((value) => {
512
+ if (items.value === null) return;
513
+ if (!items.value.map((item) => item.value).includes(value)) {
514
+ if (!otherValues.value.map((o) => o.value).includes(value)) addOtherValue(value);
515
+ }
516
+ });
517
+ }, { immediate: true });
518
+ return {
519
+ otherValues,
520
+ addOtherValue,
521
+ setOtherValue
522
+ };
523
+ function addOtherValue(value = "", focus = false) {
524
+ otherValues.value = [...otherValues.value, {
525
+ key: nanoid(),
526
+ value,
527
+ focus
528
+ }];
529
+ }
530
+ function setOtherValue(key, newValue) {
531
+ const previousValue = otherValues.value.find((o) => o.key === key);
532
+ const valueWithoutPrevious = (currentValues.value || []).filter((val) => val !== previousValue?.value);
533
+ if (newValue === null) {
534
+ otherValues.value = otherValues.value.filter((o) => o.key !== key);
535
+ if (valueWithoutPrevious.length === 0) emit(null);
536
+ else emit(valueWithoutPrevious);
537
+ } else {
538
+ otherValues.value = otherValues.value.map((otherValue) => {
539
+ if (otherValue.key === key) otherValue.value = newValue;
540
+ return otherValue;
541
+ });
542
+ if (valueWithoutPrevious.length === currentValues.value?.length) emit(valueWithoutPrevious);
543
+ else emit([...valueWithoutPrevious, newValue]);
544
+ }
545
+ }
153
546
  }
154
547
 
155
- // src/use-element-size.ts
156
- import { isNil } from "lodash-es";
157
- import { isRef, onMounted, onUnmounted, ref as ref3 } from "vue";
548
+ //#endregion
549
+ //#region src/use-element-size.ts
550
+ /**
551
+ * A Vue composable that reactively tracks the size of a DOM element using ResizeObserver.
552
+ *
553
+ * @template T - The type of the element being observed, must extend Element
554
+ * @param target - The element to observe. Can be:
555
+ * - A direct element reference
556
+ * - A Vue ref containing an element
557
+ * - A Vue ref that might be undefined
558
+ *
559
+ * @returns An object containing reactive width and height values:
560
+ * - width: Ref<number> - The current width of the element in pixels
561
+ * - height: Ref<number> - The current height of the element in pixels
562
+ *
563
+ * @example
564
+ * ```typescript
565
+ * // With a template ref
566
+ * const elementRef = ref<HTMLDivElement>();
567
+ * const { width, height } = useElementSize(elementRef);
568
+ *
569
+ * // With a direct element
570
+ * const element = document.getElementById('my-element');
571
+ * const { width, height } = useElementSize(element);
572
+ * ```
573
+ *
574
+ * @remarks
575
+ * - The composable automatically sets up a ResizeObserver when the component mounts
576
+ * - The observer is automatically disconnected when the component unmounts
577
+ * - Initial values are 0 until the first resize event
578
+ * - Handles cases where the target element might be undefined
579
+ */
158
580
  function useElementSize(target) {
159
- const width = ref3(0);
160
- const height = ref3(0);
161
- const resizeObserver = new ResizeObserver(([entry]) => {
162
- if (entry === void 0) return;
163
- width.value = entry.contentRect.width;
164
- height.value = entry.contentRect.height;
165
- });
166
- onMounted(() => {
167
- const t = isRef(target) ? target.value : target;
168
- if (!isNil(t)) {
169
- resizeObserver.observe(t);
170
- }
171
- });
172
- onUnmounted(() => {
173
- resizeObserver.disconnect();
174
- });
175
- return { width, height };
581
+ const width = ref(0);
582
+ const height = ref(0);
583
+ const resizeObserver = new ResizeObserver(([entry]) => {
584
+ if (entry === void 0) return;
585
+ width.value = entry.contentRect.width;
586
+ height.value = entry.contentRect.height;
587
+ });
588
+ onMounted(() => {
589
+ const t = isRef(target) ? target.value : target;
590
+ if (!isNil(t)) resizeObserver.observe(t);
591
+ });
592
+ onUnmounted(() => {
593
+ resizeObserver.disconnect();
594
+ });
595
+ return {
596
+ width,
597
+ height
598
+ };
176
599
  }
177
600
 
178
- // src/use-filter-fields.ts
179
- import { computed as computed3 } from "vue";
601
+ //#endregion
602
+ //#region src/use-filter-fields.ts
603
+ /**
604
+ * A Vue composable that filters and groups fields based on multiple filter criteria.
605
+ *
606
+ * @template T - The type of filter names as string literals
607
+ * @param fields - A Vue ref containing an array of Field objects to be filtered
608
+ * @param filters - An object where keys are filter names and values are predicate functions
609
+ * that return true if a field should be included in that group
610
+ *
611
+ * @returns An object containing:
612
+ * - fieldGroups: ComputedRef<Record<Extract<T, string>, Field[]>> - A reactive object
613
+ * where each key corresponds to a filter name and the value is an array of fields
614
+ * that pass that filter
615
+ *
616
+ * @example
617
+ * ```typescript
618
+ * // Define filter criteria
619
+ * const fieldFilters = {
620
+ * required: (field: Field) => field.required === true,
621
+ * optional: (field: Field) => field.required !== true,
622
+ * text: (field: Field) => field.type === 'string',
623
+ * numeric: (field: Field) => ['integer', 'float', 'decimal'].includes(field.type)
624
+ * };
625
+ *
626
+ * const fieldsRef = ref<Field[]>([
627
+ * { name: 'id', type: 'integer', required: true },
628
+ * { name: 'title', type: 'string', required: true },
629
+ * { name: 'description', type: 'text', required: false },
630
+ * { name: 'price', type: 'decimal', required: false }
631
+ * ]);
632
+ *
633
+ * const { fieldGroups } = useFilterFields(fieldsRef, fieldFilters);
634
+ *
635
+ * // Access filtered groups
636
+ * console.log(fieldGroups.value.required); // [id, title]
637
+ * console.log(fieldGroups.value.text); // [title]
638
+ * console.log(fieldGroups.value.numeric); // [id, price]
639
+ * ```
640
+ *
641
+ * @remarks
642
+ * - Fields can appear in multiple groups if they pass multiple filters
643
+ * - If a field doesn't pass any filter, it won't appear in any group
644
+ * - The result is reactive and will update when the input fields change
645
+ * - Filter functions are called for each field against each filter criterion
646
+ * - Groups are initialized as empty arrays even if no fields match the criteria
647
+ */
180
648
  function useFilterFields(fields, filters) {
181
- const fieldGroups = computed3(() => {
182
- const acc = {};
183
- for (const name in filters) {
184
- acc[name] = [];
185
- }
186
- return fields.value.reduce((acc2, field) => {
187
- for (const name in filters) {
188
- if (filters[name](field) === false) continue;
189
- acc2[name].push(field);
190
- }
191
- return acc2;
192
- }, acc);
193
- });
194
- return { fieldGroups };
649
+ return { fieldGroups: computed(() => {
650
+ const acc = {};
651
+ for (const name in filters) acc[name] = [];
652
+ return fields.value.reduce((acc$1, field) => {
653
+ for (const name in filters) {
654
+ if (filters[name](field) === false) continue;
655
+ acc$1[name].push(field);
656
+ }
657
+ return acc$1;
658
+ }, acc);
659
+ }) };
195
660
  }
196
661
 
197
- // src/use-groupable.ts
198
- import { isEqual, isNil as isNil2 } from "lodash-es";
199
- import { computed as computed4, inject as inject2, nextTick, onBeforeUnmount, provide, ref as ref4, shallowRef, watch as watch2 } from "vue";
662
+ //#endregion
663
+ //#region src/use-groupable.ts
664
+ /**
665
+ * Vue composable for creating groupable child items that can participate in group selection.
666
+ *
667
+ * This composable enables a component to be part of a group context managed by a parent component
668
+ * using `useGroupableParent`. It provides reactive active state management and selection control.
669
+ *
670
+ * @param options - Configuration options for the groupable item
671
+ * @param options.value - Unique identifier for this item within the group
672
+ * @param options.group - Name of the group to inject from (defaults to 'item-group')
673
+ * @param options.active - External reactive reference to control the active state
674
+ * @param options.watch - Whether to watch the external active reference for changes
675
+ *
676
+ * @returns Object containing active state and control methods
677
+ *
678
+ * @example
679
+ * ```vue
680
+ * <script setup>
681
+ * import { useGroupable } from '@directus/composables';
682
+ *
683
+ * const props = defineProps(['value', 'active']);
684
+ *
685
+ * const { active, toggle, activate, deactivate } = useGroupable({
686
+ * value: props.value,
687
+ * active: toRef(props, 'active'),
688
+ * watch: true
689
+ * });
690
+ * <\/script>
691
+ * ```
692
+ */
200
693
  function useGroupable(options) {
201
- const parentFunctions = inject2(options?.group || "item-group", null);
202
- if (isNil2(parentFunctions)) {
203
- return {
204
- active: ref4(false),
205
- toggle: () => {
206
- },
207
- activate: () => {
208
- },
209
- deactivate: () => {
210
- }
211
- };
212
- }
213
- const {
214
- register,
215
- unregister,
216
- toggle,
217
- selection
218
- } = parentFunctions;
219
- let startActive = false;
220
- if (options?.active?.value === true) startActive = true;
221
- if (options?.value && selection.value.includes(options.value)) startActive = true;
222
- const active = ref4(startActive);
223
- const item = { active, value: options?.value };
224
- register(item);
225
- if (options?.active !== void 0 && options.watch === true) {
226
- watch2(options.active, () => {
227
- if (options.active === void 0) return;
228
- if (options.active.value === true) {
229
- if (active.value === false) toggle(item);
230
- active.value = true;
231
- }
232
- if (options.active.value === false) {
233
- if (active.value === true) toggle(item);
234
- active.value = false;
235
- }
236
- });
237
- }
238
- onBeforeUnmount(() => unregister(item));
239
- return {
240
- active,
241
- toggle: () => {
242
- toggle(item);
243
- },
244
- activate: () => {
245
- if (active.value === false) toggle(item);
246
- },
247
- deactivate: () => {
248
- if (active.value === true) toggle(item);
249
- }
250
- };
694
+ const parentFunctions = inject(options?.group || "item-group", null);
695
+ if (isNil(parentFunctions)) return {
696
+ active: ref(false),
697
+ toggle: () => {},
698
+ activate: () => {},
699
+ deactivate: () => {}
700
+ };
701
+ const { register, unregister, toggle, selection } = parentFunctions;
702
+ let startActive = false;
703
+ if (options?.active?.value === true) startActive = true;
704
+ if (options?.value && selection.value.includes(options.value)) startActive = true;
705
+ const active = ref(startActive);
706
+ const item = {
707
+ active,
708
+ value: options?.value
709
+ };
710
+ register(item);
711
+ if (options?.active !== void 0 && options.watch === true) watch(options.active, () => {
712
+ if (options.active === void 0) return;
713
+ if (options.active.value === true) {
714
+ if (active.value === false) toggle(item);
715
+ active.value = true;
716
+ }
717
+ if (options.active.value === false) {
718
+ if (active.value === true) toggle(item);
719
+ active.value = false;
720
+ }
721
+ });
722
+ onBeforeUnmount(() => unregister(item));
723
+ return {
724
+ active,
725
+ toggle: () => {
726
+ toggle(item);
727
+ },
728
+ activate: () => {
729
+ if (active.value === false) toggle(item);
730
+ },
731
+ deactivate: () => {
732
+ if (active.value === true) toggle(item);
733
+ }
734
+ };
251
735
  }
736
+ /**
737
+ * Vue composable for creating a group parent that manages multiple groupable child items.
738
+ *
739
+ * This composable provides the foundation for components that need to manage a collection
740
+ * of selectable items, such as tabs, radio groups, or multi-select lists. It handles
741
+ * registration of child items, selection state management, and provides various selection
742
+ * constraints (mandatory, maximum, multiple).
743
+ *
744
+ * @param state - External state configuration for selection management
745
+ * @param state.selection - External selection state reference
746
+ * @param state.onSelectionChange - Callback fired when selection changes
747
+ * @param state.onToggle - Callback fired when an item is toggled
748
+ * @param options - Configuration options for selection behavior
749
+ * @param options.mandatory - Whether at least one item must always be selected
750
+ * @param options.max - Maximum number of items that can be selected (-1 for unlimited)
751
+ * @param options.multiple - Whether multiple items can be selected simultaneously
752
+ * @param group - Injection key for the group (defaults to 'item-group')
753
+ *
754
+ * @returns Object containing items array, selection state, and utility functions
755
+ *
756
+ * @example
757
+ * ```vue
758
+ * <script setup>
759
+ * import { useGroupableParent } from '@directus/composables';
760
+ * import { ref } from 'vue';
761
+ *
762
+ * const selectedItems = ref([]);
763
+ * const isMultiple = ref(true);
764
+ * const isMandatory = ref(false);
765
+ *
766
+ * const { items, selection } = useGroupableParent(
767
+ * {
768
+ * selection: selectedItems,
769
+ * onSelectionChange: (values) => {
770
+ * console.log('Selection changed:', values);
771
+ * }
772
+ * },
773
+ * {
774
+ * multiple: isMultiple,
775
+ * mandatory: isMandatory,
776
+ * max: ref(3)
777
+ * }
778
+ * );
779
+ * <\/script>
780
+ * ```
781
+ */
252
782
  function useGroupableParent(state = {}, options = {}, group = "item-group") {
253
- const items = shallowRef([]);
254
- const internalSelection = ref4([]);
255
- const selection = computed4({
256
- get() {
257
- if (!isNil2(state.selection) && !isNil2(state.selection.value)) {
258
- return state.selection.value;
259
- }
260
- return internalSelection.value;
261
- },
262
- set(newSelection) {
263
- if (!isNil2(state.onSelectionChange)) {
264
- state.onSelectionChange(newSelection);
265
- }
266
- internalSelection.value = [...newSelection];
267
- }
268
- });
269
- provide(group, { register, unregister, toggle, selection });
270
- watch2(selection, updateChildren, { immediate: true });
271
- nextTick().then(updateChildren);
272
- watch2(
273
- () => options?.mandatory?.value,
274
- (newValue, oldValue) => {
275
- if (isEqual(newValue, oldValue)) return;
276
- if (!selection.value || selection.value.length === 0 && options?.mandatory?.value === true) {
277
- if (items.value[0]) selection.value = [getValueForItem(items.value[0])];
278
- }
279
- }
280
- );
281
- return { items, selection, internalSelection, getValueForItem, updateChildren };
282
- function register(item) {
283
- items.value = [...items.value, item];
284
- const value = getValueForItem(item);
285
- if (selection.value.length === 0 && options?.mandatory?.value === true && items.value.length === 1) {
286
- selection.value = [value];
287
- }
288
- if (item.active.value && selection.value.includes(value) === false) {
289
- toggle(item);
290
- }
291
- }
292
- function unregister(item) {
293
- items.value = items.value.filter((existingItem) => {
294
- return existingItem !== item;
295
- });
296
- }
297
- function toggle(item) {
298
- if (options?.multiple?.value === true) {
299
- toggleMultiple(item);
300
- } else {
301
- toggleSingle(item);
302
- }
303
- if (!isNil2(state.onToggle)) {
304
- state.onToggle(item);
305
- }
306
- }
307
- function toggleSingle(item) {
308
- const itemValue = getValueForItem(item);
309
- if (selection.value[0] === itemValue && options?.mandatory?.value !== true) {
310
- selection.value = [];
311
- return;
312
- }
313
- if (selection.value[0] !== itemValue) {
314
- selection.value = [itemValue];
315
- }
316
- }
317
- function toggleMultiple(item) {
318
- const itemValue = getValueForItem(item);
319
- if (selection.value.includes(itemValue)) {
320
- if (options?.mandatory?.value === true && selection.value.length === 1) {
321
- updateChildren();
322
- return;
323
- }
324
- selection.value = selection.value.filter((value) => value !== itemValue);
325
- return;
326
- }
327
- if (options?.max?.value && options.max.value !== -1 && selection.value.length >= options.max.value) {
328
- updateChildren();
329
- return;
330
- }
331
- selection.value = [...selection.value, itemValue];
332
- }
333
- function getValueForItem(item) {
334
- return item.value || items.value.findIndex((child) => item === child);
335
- }
336
- function updateChildren() {
337
- items.value.forEach((item) => {
338
- item.active.value = selection.value.includes(getValueForItem(item));
339
- });
340
- }
783
+ const items = shallowRef([]);
784
+ const internalSelection = ref([]);
785
+ const selection = computed({
786
+ get() {
787
+ if (!isNil(state.selection) && !isNil(state.selection.value)) return state.selection.value;
788
+ return internalSelection.value;
789
+ },
790
+ set(newSelection) {
791
+ if (!isNil(state.onSelectionChange)) state.onSelectionChange(newSelection);
792
+ internalSelection.value = [...newSelection];
793
+ }
794
+ });
795
+ provide(group, {
796
+ register,
797
+ unregister,
798
+ toggle,
799
+ selection
800
+ });
801
+ watch(selection, updateChildren, { immediate: true });
802
+ nextTick().then(updateChildren);
803
+ watch(() => options?.mandatory?.value, (newValue, oldValue) => {
804
+ if (isEqual(newValue, oldValue)) return;
805
+ if (!selection.value || selection.value.length === 0 && options?.mandatory?.value === true) {
806
+ if (items.value[0]) selection.value = [getValueForItem(items.value[0])];
807
+ }
808
+ });
809
+ return {
810
+ items,
811
+ selection,
812
+ internalSelection,
813
+ getValueForItem,
814
+ updateChildren
815
+ };
816
+ function register(item) {
817
+ items.value = [...items.value, item];
818
+ const value = getValueForItem(item);
819
+ if (selection.value.length === 0 && options?.mandatory?.value === true && items.value.length === 1) selection.value = [value];
820
+ if (item.active.value && selection.value.includes(value) === false) toggle(item);
821
+ }
822
+ function unregister(item) {
823
+ items.value = items.value.filter((existingItem) => {
824
+ return existingItem !== item;
825
+ });
826
+ }
827
+ function toggle(item) {
828
+ if (options?.multiple?.value === true) toggleMultiple(item);
829
+ else toggleSingle(item);
830
+ if (!isNil(state.onToggle)) state.onToggle(item);
831
+ }
832
+ function toggleSingle(item) {
833
+ const itemValue = getValueForItem(item);
834
+ if (selection.value[0] === itemValue && options?.mandatory?.value !== true) {
835
+ selection.value = [];
836
+ return;
837
+ }
838
+ if (selection.value[0] !== itemValue) selection.value = [itemValue];
839
+ }
840
+ function toggleMultiple(item) {
841
+ const itemValue = getValueForItem(item);
842
+ if (selection.value.includes(itemValue)) {
843
+ if (options?.mandatory?.value === true && selection.value.length === 1) {
844
+ updateChildren();
845
+ return;
846
+ }
847
+ selection.value = selection.value.filter((value) => value !== itemValue);
848
+ return;
849
+ }
850
+ if (options?.max?.value && options.max.value !== -1 && selection.value.length >= options.max.value) {
851
+ updateChildren();
852
+ return;
853
+ }
854
+ selection.value = [...selection.value, itemValue];
855
+ }
856
+ function getValueForItem(item) {
857
+ return item.value || items.value.findIndex((child) => item === child);
858
+ }
859
+ function updateChildren() {
860
+ items.value.forEach((item) => {
861
+ item.active.value = selection.value.includes(getValueForItem(item));
862
+ });
863
+ }
341
864
  }
342
865
 
343
- // src/use-items.ts
344
- import { getEndpoint, moveInArray } from "@directus/utils";
345
- import axios from "axios";
346
- import { isEqual as isEqual2, throttle } from "lodash-es";
347
- import { computed as computed5, ref as ref5, toRef, unref, watch as watch3 } from "vue";
866
+ //#endregion
867
+ //#region src/use-items.ts
348
868
  function useItems(collection, query) {
349
- const api = useApi();
350
- const { primaryKeyField } = useCollection(collection);
351
- const { fields, limit, sort, search, filter, page, filterSystem, alias, deep } = query;
352
- const endpoint = computed5(() => {
353
- if (!collection.value) return null;
354
- return getEndpoint(collection.value);
355
- });
356
- const items = ref5([]);
357
- const loading = ref5(false);
358
- const error = ref5(null);
359
- const itemCount = ref5(null);
360
- const totalCount = ref5(null);
361
- const totalPages = computed5(() => {
362
- if (itemCount.value === null) return 1;
363
- if (itemCount.value < (unref(limit) ?? 100)) return 1;
364
- return Math.ceil(itemCount.value / (unref(limit) ?? 100));
365
- });
366
- const existingRequests = {
367
- items: null,
368
- total: null,
369
- filter: null
370
- };
371
- let loadingTimeout = null;
372
- const fetchItems = throttle(getItems, 500);
373
- watch3(
374
- [collection, limit, sort, search, filter, fields, page, toRef(alias), toRef(deep)],
375
- async (after, before) => {
376
- if (isEqual2(after, before)) return;
377
- const [newCollection, newLimit, newSort, newSearch, newFilter] = after;
378
- const [oldCollection, oldLimit, oldSort, oldSearch, oldFilter] = before;
379
- if (!newCollection || !query) return;
380
- if (newCollection !== oldCollection) {
381
- reset();
382
- }
383
- if (!isEqual2(newFilter, oldFilter) || !isEqual2(newSort, oldSort) || newLimit !== oldLimit || newSearch !== oldSearch) {
384
- if (oldCollection) {
385
- page.value = 1;
386
- }
387
- }
388
- if (newCollection !== oldCollection || !isEqual2(newFilter, oldFilter) || newSearch !== oldSearch) {
389
- getItemCount();
390
- }
391
- fetchItems();
392
- },
393
- { deep: true, immediate: true }
394
- );
395
- watch3(
396
- [collection, toRef(filterSystem)],
397
- async (after, before) => {
398
- if (isEqual2(after, before)) return;
399
- getTotalCount();
400
- },
401
- { deep: true, immediate: true }
402
- );
403
- return {
404
- itemCount,
405
- totalCount,
406
- items,
407
- totalPages,
408
- loading,
409
- error,
410
- changeManualSort,
411
- getItems,
412
- getItemCount,
413
- getTotalCount
414
- };
415
- async function getItems() {
416
- if (!endpoint.value) return;
417
- let isCurrentRequestCanceled = false;
418
- if (existingRequests.items) existingRequests.items.abort();
419
- existingRequests.items = new AbortController();
420
- error.value = null;
421
- if (loadingTimeout) {
422
- clearTimeout(loadingTimeout);
423
- }
424
- loadingTimeout = setTimeout(() => {
425
- loading.value = true;
426
- }, 150);
427
- let fieldsToFetch = [...unref(fields) ?? []];
428
- if (!unref(fields)?.includes("*") && primaryKeyField.value && fieldsToFetch.includes(primaryKeyField.value.field) === false) {
429
- fieldsToFetch.push(primaryKeyField.value.field);
430
- }
431
- fieldsToFetch = fieldsToFetch.filter((field) => field.startsWith("$") === false);
432
- try {
433
- const response = await api.get(endpoint.value, {
434
- params: {
435
- limit: unref(limit),
436
- fields: fieldsToFetch,
437
- ...alias ? { alias: unref(alias) } : {},
438
- sort: unref(sort),
439
- page: unref(page),
440
- search: unref(search),
441
- filter: unref(filter),
442
- deep: unref(deep)
443
- },
444
- signal: existingRequests.items.signal
445
- });
446
- let fetchedItems = response.data.data;
447
- existingRequests.items = null;
448
- if (collection.value === "directus_files") {
449
- fetchedItems = fetchedItems.map((file) => ({
450
- ...file,
451
- $thumbnail: file
452
- }));
453
- }
454
- items.value = fetchedItems;
455
- if (page && fetchedItems.length === 0 && page?.value !== 1) {
456
- page.value = 1;
457
- }
458
- } catch (err) {
459
- if (axios.isCancel(err)) {
460
- isCurrentRequestCanceled = true;
461
- } else {
462
- error.value = err;
463
- }
464
- } finally {
465
- if (loadingTimeout && !isCurrentRequestCanceled) {
466
- clearTimeout(loadingTimeout);
467
- loadingTimeout = null;
468
- }
469
- if (!loadingTimeout) loading.value = false;
470
- }
471
- }
472
- function reset() {
473
- items.value = [];
474
- totalCount.value = null;
475
- itemCount.value = null;
476
- }
477
- async function changeManualSort({ item, to }) {
478
- const pk = primaryKeyField.value?.field;
479
- if (!pk) return;
480
- const fromIndex = items.value.findIndex((existing) => existing[pk] === item);
481
- const toIndex = items.value.findIndex((existing) => existing[pk] === to);
482
- items.value = moveInArray(items.value, fromIndex, toIndex);
483
- const endpoint2 = computed5(() => `/utils/sort/${collection.value}`);
484
- await api.post(endpoint2.value, { item, to });
485
- }
486
- async function getTotalCount() {
487
- if (!endpoint.value) return;
488
- try {
489
- if (existingRequests.total) existingRequests.total.abort();
490
- existingRequests.total = new AbortController();
491
- const aggregate = primaryKeyField.value ? {
492
- countDistinct: primaryKeyField.value.field
493
- } : {
494
- count: "*"
495
- };
496
- const response = await api.get(endpoint.value, {
497
- params: {
498
- aggregate,
499
- filter: unref(filterSystem)
500
- },
501
- signal: existingRequests.total.signal
502
- });
503
- const count = primaryKeyField.value ? Number(response.data.data[0].countDistinct[primaryKeyField.value.field]) : Number(response.data.data[0].count);
504
- existingRequests.total = null;
505
- totalCount.value = count;
506
- } catch (err) {
507
- if (!axios.isCancel(err)) {
508
- throw err;
509
- }
510
- }
511
- }
512
- async function getItemCount() {
513
- if (!endpoint.value) return;
514
- try {
515
- if (existingRequests.filter) existingRequests.filter.abort();
516
- existingRequests.filter = new AbortController();
517
- const aggregate = primaryKeyField.value ? {
518
- countDistinct: primaryKeyField.value.field
519
- } : {
520
- count: "*"
521
- };
522
- const response = await api.get(endpoint.value, {
523
- params: {
524
- filter: unref(filter),
525
- search: unref(search),
526
- aggregate
527
- },
528
- signal: existingRequests.filter.signal
529
- });
530
- const count = primaryKeyField.value ? Number(response.data.data[0].countDistinct[primaryKeyField.value.field]) : Number(response.data.data[0].count);
531
- existingRequests.filter = null;
532
- itemCount.value = count;
533
- } catch (err) {
534
- if (!axios.isCancel(err)) {
535
- throw err;
536
- }
537
- }
538
- }
869
+ const api = useApi();
870
+ const { primaryKeyField } = useCollection(collection);
871
+ const { fields, limit, sort, search, filter, page, filterSystem, alias, deep } = query;
872
+ const endpoint = computed(() => {
873
+ if (!collection.value) return null;
874
+ return getEndpoint(collection.value);
875
+ });
876
+ const items = ref([]);
877
+ const loading = ref(false);
878
+ const error = ref(null);
879
+ const itemCount = ref(null);
880
+ const totalCount = ref(null);
881
+ const totalPages = computed(() => {
882
+ if (itemCount.value === null) return 1;
883
+ if (itemCount.value < (unref(limit) ?? 100)) return 1;
884
+ return Math.ceil(itemCount.value / (unref(limit) ?? 100));
885
+ });
886
+ const existingRequests = {
887
+ items: null,
888
+ total: null,
889
+ filter: null
890
+ };
891
+ let loadingTimeout = null;
892
+ const fetchItems = debounce((shouldUpdateCount) => {
893
+ Promise.all([getItems(), shouldUpdateCount ? getItemCount() : Promise.resolve()]);
894
+ }, 350);
895
+ watch([
896
+ collection,
897
+ limit,
898
+ sort,
899
+ search,
900
+ filter,
901
+ fields,
902
+ page,
903
+ toRef(alias),
904
+ toRef(deep)
905
+ ], async (after, before) => {
906
+ if (isEqual(after, before)) return;
907
+ const [newCollection, newLimit, newSort, newSearch, newFilter] = after;
908
+ const [oldCollection, oldLimit, oldSort, oldSearch, oldFilter] = before;
909
+ if (!newCollection || !query) return;
910
+ if (newCollection !== oldCollection) reset();
911
+ if (!isEqual(newFilter, oldFilter) || !isEqual(newSort, oldSort) || newLimit !== oldLimit || newSearch !== oldSearch) {
912
+ if (oldCollection) page.value = 1;
913
+ }
914
+ fetchItems(newCollection !== oldCollection || !isEqual(newFilter, oldFilter) || newSearch !== oldSearch);
915
+ }, {
916
+ deep: true,
917
+ immediate: true
918
+ });
919
+ watch([collection, toRef(filterSystem)], async (after, before) => {
920
+ if (isEqual(after, before)) return;
921
+ getTotalCount();
922
+ }, {
923
+ deep: true,
924
+ immediate: true
925
+ });
926
+ return {
927
+ itemCount,
928
+ totalCount,
929
+ items,
930
+ totalPages,
931
+ loading,
932
+ error,
933
+ changeManualSort,
934
+ getItems,
935
+ getItemCount,
936
+ getTotalCount
937
+ };
938
+ async function getItems() {
939
+ if (!endpoint.value) return;
940
+ let isCurrentRequestCanceled = false;
941
+ if (existingRequests.items) existingRequests.items.abort();
942
+ existingRequests.items = new AbortController();
943
+ error.value = null;
944
+ if (loadingTimeout) clearTimeout(loadingTimeout);
945
+ loadingTimeout = setTimeout(() => {
946
+ loading.value = true;
947
+ }, 150);
948
+ let fieldsToFetch = [...unref(fields) ?? []];
949
+ if (!unref(fields)?.includes("*") && primaryKeyField.value && fieldsToFetch.includes(primaryKeyField.value.field) === false) fieldsToFetch.push(primaryKeyField.value.field);
950
+ fieldsToFetch = fieldsToFetch.filter((field) => field.startsWith("$") === false);
951
+ try {
952
+ let fetchedItems = (await api.get(endpoint.value, {
953
+ params: {
954
+ limit: unref(limit),
955
+ fields: fieldsToFetch,
956
+ ...alias ? { alias: unref(alias) } : {},
957
+ sort: unref(sort),
958
+ page: unref(page),
959
+ search: unref(search),
960
+ filter: unref(filter),
961
+ deep: unref(deep)
962
+ },
963
+ signal: existingRequests.items.signal
964
+ })).data.data;
965
+ existingRequests.items = null;
966
+ /**
967
+ * @NOTE
968
+ *
969
+ * This is used in conjunction with the fake field in /src/stores/fields/fields.ts to be
970
+ * able to render out the directus_files collection (file library) using regular layouts
971
+ *
972
+ * Layouts expect the file to be a m2o of a `file` type, however, directus_files is the
973
+ * only collection that doesn't have this (obviously). This fake $thumbnail field is used to
974
+ * pretend there is a file m2o, so we can use the regular layout logic for files as well
975
+ */
976
+ if (collection.value === "directus_files") fetchedItems = fetchedItems.map((file) => ({
977
+ ...file,
978
+ $thumbnail: file
979
+ }));
980
+ items.value = fetchedItems;
981
+ if (page && fetchedItems.length === 0 && page?.value !== 1) page.value = 1;
982
+ } catch (err) {
983
+ if (axios.isCancel(err)) isCurrentRequestCanceled = true;
984
+ else error.value = err;
985
+ } finally {
986
+ if (loadingTimeout && !isCurrentRequestCanceled) {
987
+ clearTimeout(loadingTimeout);
988
+ loadingTimeout = null;
989
+ }
990
+ if (!loadingTimeout) loading.value = false;
991
+ }
992
+ }
993
+ function reset() {
994
+ items.value = [];
995
+ totalCount.value = null;
996
+ itemCount.value = null;
997
+ }
998
+ async function changeManualSort({ item, to }) {
999
+ const pk = primaryKeyField.value?.field;
1000
+ if (!pk) return;
1001
+ const fromIndex = items.value.findIndex((existing) => existing[pk] === item);
1002
+ const toIndex = items.value.findIndex((existing) => existing[pk] === to);
1003
+ items.value = moveInArray(items.value, fromIndex, toIndex);
1004
+ const endpoint$1 = computed(() => `/utils/sort/${collection.value}`);
1005
+ await api.post(endpoint$1.value, {
1006
+ item,
1007
+ to
1008
+ });
1009
+ }
1010
+ async function getTotalCount() {
1011
+ if (!endpoint.value) return;
1012
+ try {
1013
+ if (existingRequests.total) existingRequests.total.abort();
1014
+ existingRequests.total = new AbortController();
1015
+ const aggregate = primaryKeyField.value ? { countDistinct: primaryKeyField.value.field } : { count: "*" };
1016
+ const response = await api.get(endpoint.value, {
1017
+ params: {
1018
+ aggregate,
1019
+ filter: unref(filterSystem)
1020
+ },
1021
+ signal: existingRequests.total.signal
1022
+ });
1023
+ const count = primaryKeyField.value ? Number(response.data.data[0].countDistinct[primaryKeyField.value.field]) : Number(response.data.data[0].count);
1024
+ existingRequests.total = null;
1025
+ totalCount.value = count;
1026
+ } catch (err) {
1027
+ if (!axios.isCancel(err)) throw err;
1028
+ }
1029
+ }
1030
+ async function getItemCount() {
1031
+ if (!endpoint.value) return;
1032
+ try {
1033
+ if (existingRequests.filter) existingRequests.filter.abort();
1034
+ existingRequests.filter = new AbortController();
1035
+ const aggregate = primaryKeyField.value ? { countDistinct: primaryKeyField.value.field } : { count: "*" };
1036
+ const response = await api.get(endpoint.value, {
1037
+ params: {
1038
+ filter: unref(filter),
1039
+ search: unref(search),
1040
+ aggregate
1041
+ },
1042
+ signal: existingRequests.filter.signal
1043
+ });
1044
+ const count = primaryKeyField.value ? Number(response.data.data[0].countDistinct[primaryKeyField.value.field]) : Number(response.data.data[0].count);
1045
+ existingRequests.filter = null;
1046
+ itemCount.value = count;
1047
+ } catch (err) {
1048
+ if (!axios.isCancel(err)) throw err;
1049
+ }
1050
+ }
539
1051
  }
540
1052
 
541
- // src/use-layout.ts
542
- import { computed as computed6, defineComponent, reactive, toRefs } from "vue";
543
- var NAME_SUFFIX = "wrapper";
544
- var WRITABLE_PROPS = ["selection", "layoutOptions", "layoutQuery"];
1053
+ //#endregion
1054
+ //#region src/use-layout.ts
1055
+ const NAME_SUFFIX = "wrapper";
1056
+ const WRITABLE_PROPS = [
1057
+ "selection",
1058
+ "layoutOptions",
1059
+ "layoutQuery"
1060
+ ];
1061
+ /**
1062
+ * Type guard to check if a property is writable (can be updated via emit).
1063
+ *
1064
+ * This function determines whether a given property name corresponds to one of the
1065
+ * writable properties that can be updated through Vue's emit system.
1066
+ *
1067
+ * @param prop - The property name to check
1068
+ * @returns True if the property is writable, false otherwise
1069
+ *
1070
+ * @example
1071
+ * ```typescript
1072
+ * if (isWritableProp('selection')) {
1073
+ * // Property is writable, can emit update
1074
+ * emit('update:selection', newValue);
1075
+ * }
1076
+ * ```
1077
+ */
545
1078
  function isWritableProp(prop) {
546
- return WRITABLE_PROPS.includes(prop);
1079
+ return WRITABLE_PROPS.includes(prop);
547
1080
  }
1081
+ /**
1082
+ * Creates a Vue component wrapper for a layout configuration.
1083
+ *
1084
+ * This function creates a dynamic Vue component that wraps a layout with standardized
1085
+ * props, emits, and state management. It handles reactive state updates, prop validation,
1086
+ * and provides a consistent interface for all layout components.
1087
+ *
1088
+ * @template Options - The type for layout-specific options
1089
+ * @template Query - The type for layout-specific query parameters
1090
+ * @param layout - The layout configuration object containing id and setup function
1091
+ * @returns A Vue component that can be used to render the layout
1092
+ *
1093
+ * @example
1094
+ * ```typescript
1095
+ * interface MyLayoutOptions {
1096
+ * itemSize: number;
1097
+ * showHeaders: boolean;
1098
+ * }
1099
+ *
1100
+ * interface MyLayoutQuery {
1101
+ * page: number;
1102
+ * limit: number;
1103
+ * }
1104
+ *
1105
+ * const layoutConfig: LayoutConfig = {
1106
+ * id: 'my-layout',
1107
+ * setup: (props, { emit }) => ({
1108
+ * // Layout-specific setup logic
1109
+ * })
1110
+ * };
1111
+ *
1112
+ * const LayoutWrapper = createLayoutWrapper<MyLayoutOptions, MyLayoutQuery>(layoutConfig);
1113
+ * ```
1114
+ */
548
1115
  function createLayoutWrapper(layout) {
549
- return defineComponent({
550
- name: `${layout.id}-${NAME_SUFFIX}`,
551
- props: {
552
- collection: {
553
- type: String,
554
- required: true
555
- },
556
- selection: {
557
- type: Array,
558
- default: () => []
559
- },
560
- layoutOptions: {
561
- type: Object,
562
- default: () => ({})
563
- },
564
- layoutQuery: {
565
- type: Object,
566
- default: () => ({})
567
- },
568
- layoutProps: {
569
- type: Object,
570
- default: () => ({})
571
- },
572
- filter: {
573
- type: Object,
574
- default: null
575
- },
576
- filterUser: {
577
- type: Object,
578
- default: null
579
- },
580
- filterSystem: {
581
- type: Object,
582
- default: null
583
- },
584
- search: {
585
- type: String,
586
- default: null
587
- },
588
- showSelect: {
589
- type: String,
590
- default: "multiple"
591
- },
592
- selectMode: {
593
- type: Boolean,
594
- default: false
595
- },
596
- readonly: {
597
- type: Boolean,
598
- default: false
599
- },
600
- resetPreset: {
601
- type: Function,
602
- default: null
603
- },
604
- clearFilters: {
605
- type: Function,
606
- default: null
607
- }
608
- },
609
- emits: WRITABLE_PROPS.map((prop) => `update:${prop}`),
610
- setup(props, { emit }) {
611
- const state = reactive({ ...layout.setup(props, { emit }), ...toRefs(props) });
612
- for (const key in state) {
613
- state[`onUpdate:${key}`] = (value) => {
614
- if (isWritableProp(key)) {
615
- emit(`update:${key}`, value);
616
- } else if (!Object.keys(props).includes(key)) {
617
- state[key] = value;
618
- }
619
- };
620
- }
621
- return { state };
622
- },
623
- render(ctx) {
624
- return ctx.$slots.default !== void 0 ? ctx.$slots.default({ layoutState: ctx.state }) : null;
625
- }
626
- });
1116
+ return defineComponent({
1117
+ name: `${layout.id}-${NAME_SUFFIX}`,
1118
+ props: {
1119
+ collection: {
1120
+ type: String,
1121
+ required: true
1122
+ },
1123
+ selection: {
1124
+ type: Array,
1125
+ default: () => []
1126
+ },
1127
+ layoutOptions: {
1128
+ type: Object,
1129
+ default: () => ({})
1130
+ },
1131
+ layoutQuery: {
1132
+ type: Object,
1133
+ default: () => ({})
1134
+ },
1135
+ layoutProps: {
1136
+ type: Object,
1137
+ default: () => ({})
1138
+ },
1139
+ filter: {
1140
+ type: Object,
1141
+ default: null
1142
+ },
1143
+ filterUser: {
1144
+ type: Object,
1145
+ default: null
1146
+ },
1147
+ filterSystem: {
1148
+ type: Object,
1149
+ default: null
1150
+ },
1151
+ search: {
1152
+ type: String,
1153
+ default: null
1154
+ },
1155
+ showSelect: {
1156
+ type: String,
1157
+ default: "multiple"
1158
+ },
1159
+ selectMode: {
1160
+ type: Boolean,
1161
+ default: false
1162
+ },
1163
+ readonly: {
1164
+ type: Boolean,
1165
+ default: false
1166
+ },
1167
+ resetPreset: {
1168
+ type: Function,
1169
+ default: null
1170
+ },
1171
+ clearFilters: {
1172
+ type: Function,
1173
+ default: null
1174
+ }
1175
+ },
1176
+ emits: WRITABLE_PROPS.map((prop) => `update:${prop}`),
1177
+ setup(props, { emit }) {
1178
+ const state = reactive({
1179
+ ...layout.setup(props, { emit }),
1180
+ ...toRefs(props)
1181
+ });
1182
+ for (const key in state) state[`onUpdate:${key}`] = (value) => {
1183
+ if (isWritableProp(key)) emit(`update:${key}`, value);
1184
+ else if (!Object.keys(props).includes(key)) state[key] = value;
1185
+ };
1186
+ return { state };
1187
+ },
1188
+ render(ctx) {
1189
+ return ctx.$slots.default !== void 0 ? ctx.$slots.default({ layoutState: ctx.state }) : null;
1190
+ }
1191
+ });
627
1192
  }
1193
+ /**
1194
+ * Composable for managing layout components in Directus.
1195
+ *
1196
+ * This composable provides access to layout components and handles the dynamic
1197
+ * selection of layout wrappers based on the provided layout ID. It automatically
1198
+ * falls back to the tabular layout if the requested layout is not found.
1199
+ *
1200
+ * @template Options - The type for layout-specific options (default: any)
1201
+ * @template Query - The type for layout-specific query parameters (default: any)
1202
+ * @param layoutId - A reactive reference to the layout ID
1203
+ * @returns An object containing the layout wrapper component
1204
+ *
1205
+ * @example
1206
+ * ```typescript
1207
+ * import { ref } from 'vue';
1208
+ * import { useLayout } from './use-layout';
1209
+ *
1210
+ * const selectedLayoutId = ref('table');
1211
+ * const { layoutWrapper } = useLayout(selectedLayoutId);
1212
+ *
1213
+ * // Use the layout wrapper in your template
1214
+ * // <component :is="layoutWrapper" :collection="'users'" />
1215
+ * ```
1216
+ *
1217
+ * @example
1218
+ * ```typescript
1219
+ * // With typed options and query
1220
+ * interface TableOptions {
1221
+ * spacing: 'cozy' | 'comfortable' | 'compact';
1222
+ * showHeaders: boolean;
1223
+ * }
1224
+ *
1225
+ * interface TableQuery {
1226
+ * sort: string[];
1227
+ * limit: number;
1228
+ * }
1229
+ *
1230
+ * const layoutId = ref<string | null>('table');
1231
+ * const { layoutWrapper } = useLayout<TableOptions, TableQuery>(layoutId);
1232
+ * ```
1233
+ */
628
1234
  function useLayout(layoutId) {
629
- const { layouts } = useExtensions();
630
- const layoutWrappers = computed6(() => layouts.value.map((layout) => createLayoutWrapper(layout)));
631
- const layoutWrapper = computed6(() => {
632
- const layout = layoutWrappers.value.find((layout2) => layout2.name === `${layoutId.value}-${NAME_SUFFIX}`);
633
- if (layout === void 0) {
634
- return layoutWrappers.value.find((layout2) => layout2.name === `tabular-${NAME_SUFFIX}`);
635
- }
636
- return layout;
637
- });
638
- return { layoutWrapper };
1235
+ const { layouts } = useExtensions();
1236
+ const layoutWrappers = computed(() => layouts.value.map((layout) => createLayoutWrapper(layout)));
1237
+ return { layoutWrapper: computed(() => {
1238
+ const layout = layoutWrappers.value.find((layout$1) => layout$1.name === `${layoutId.value}-${NAME_SUFFIX}`);
1239
+ if (layout === void 0) return layoutWrappers.value.find((layout$1) => layout$1.name === `tabular-${NAME_SUFFIX}`);
1240
+ return layout;
1241
+ }) };
639
1242
  }
640
1243
 
641
- // src/use-size-class.ts
642
- import { computed as computed7 } from "vue";
643
- var sizeProps = {
644
- xSmall: {
645
- type: Boolean,
646
- default: false
647
- },
648
- small: {
649
- type: Boolean,
650
- default: false
651
- },
652
- large: {
653
- type: Boolean,
654
- default: false
655
- },
656
- xLarge: {
657
- type: Boolean,
658
- default: false
659
- }
1244
+ //#endregion
1245
+ //#region src/use-size-class.ts
1246
+ /**
1247
+ * Vue props definition for size-related boolean properties.
1248
+ *
1249
+ * This object defines the standard size props that can be used in Vue components
1250
+ * to control size-based styling through CSS classes.
1251
+ *
1252
+ * @example
1253
+ * ```typescript
1254
+ * // In a Vue component
1255
+ * export default defineComponent({
1256
+ * props: {
1257
+ * ...sizeProps,
1258
+ * // other props
1259
+ * },
1260
+ * setup(props) {
1261
+ * const sizeClass = useSizeClass(props);
1262
+ * return { sizeClass };
1263
+ * }
1264
+ * });
1265
+ * ```
1266
+ */
1267
+ const sizeProps = {
1268
+ xSmall: {
1269
+ type: Boolean,
1270
+ default: false
1271
+ },
1272
+ small: {
1273
+ type: Boolean,
1274
+ default: false
1275
+ },
1276
+ large: {
1277
+ type: Boolean,
1278
+ default: false
1279
+ },
1280
+ xLarge: {
1281
+ type: Boolean,
1282
+ default: false
1283
+ }
660
1284
  };
1285
+ /**
1286
+ * Composable for generating CSS size class names based on size props.
1287
+ *
1288
+ * This composable takes props containing size boolean flags and returns a computed
1289
+ * CSS class name string. It follows a priority order: xSmall > small > large > xLarge.
1290
+ * If no size props are true, it returns null.
1291
+ *
1292
+ * @template T - The type of additional props that extend SizeProps
1293
+ * @param props - The props object containing size boolean properties
1294
+ * @returns A computed ref that resolves to the appropriate CSS class name or null
1295
+ *
1296
+ * @example
1297
+ * ```typescript
1298
+ * // Basic usage in a Vue component
1299
+ * const props = { small: true, large: false };
1300
+ * const sizeClass = useSizeClass(props);
1301
+ * console.log(sizeClass.value); // 'small'
1302
+ * ```
1303
+ *
1304
+ * @example
1305
+ * ```typescript
1306
+ * // Usage with additional props
1307
+ * interface MyProps {
1308
+ * color: string;
1309
+ * disabled: boolean;
1310
+ * }
1311
+ *
1312
+ * const props: MyProps & SizeProps = {
1313
+ * color: 'blue',
1314
+ * disabled: false,
1315
+ * xLarge: true
1316
+ * };
1317
+ *
1318
+ * const sizeClass = useSizeClass(props);
1319
+ * console.log(sizeClass.value); // 'x-large'
1320
+ * ```
1321
+ *
1322
+ * @example
1323
+ * ```typescript
1324
+ * // In a Vue component with reactive props
1325
+ * export default defineComponent({
1326
+ * props: {
1327
+ * ...sizeProps,
1328
+ * label: String,
1329
+ * },
1330
+ * setup(props) {
1331
+ * const sizeClass = useSizeClass(props);
1332
+ *
1333
+ * return { sizeClass };
1334
+ * },
1335
+ * template: `
1336
+ * <button :class="['btn', sizeClass]">
1337
+ * {{ label }}
1338
+ * </button>
1339
+ * `
1340
+ * });
1341
+ * ```
1342
+ */
661
1343
  function useSizeClass(props) {
662
- const sizeClass = computed7(() => {
663
- if (props.xSmall) return "x-small";
664
- if (props.small) return "small";
665
- if (props.large) return "large";
666
- if (props.xLarge) return "x-large";
667
- return null;
668
- });
669
- return sizeClass;
1344
+ return computed(() => {
1345
+ if (props.xSmall) return "x-small";
1346
+ if (props.small) return "small";
1347
+ if (props.large) return "large";
1348
+ if (props.xLarge) return "x-large";
1349
+ return null;
1350
+ });
670
1351
  }
671
1352
 
672
- // src/use-sync.ts
673
- import { computed as computed8 } from "vue";
1353
+ //#endregion
1354
+ //#region src/use-sync.ts
1355
+ /**
1356
+ * Composable for creating two-way binding between parent and child components.
1357
+ *
1358
+ * @deprecated Use Vue's native `defineModel()` instead. This composable is kept for backward compatibility.
1359
+ * Vue 3.4+ provides `defineModel()` which offers a more streamlined and performant way to create v-model bindings.
1360
+ *
1361
+ * @see {@link https://vuejs.org/api/sfc-script-setup.html#definemodel} Vue's defineModel documentation
1362
+ *
1363
+ * This composable creates a computed ref that synchronizes a prop value with
1364
+ * its parent component through Vue's v-model pattern. It provides a getter
1365
+ * that returns the current prop value and a setter that emits an update event
1366
+ * to notify the parent component of changes.
1367
+ *
1368
+ * This is particularly useful for creating custom form components that need
1369
+ * to work with v-model while maintaining proper data flow patterns.
1370
+ *
1371
+ * @template T - The type of the props object
1372
+ * @template K - The key of the prop to sync (must be a string key of T)
1373
+ * @template E - The emit function type with proper event typing
1374
+ *
1375
+ * @param props - The component props object containing the value to sync
1376
+ * @param key - The specific prop key to create a two-way binding for
1377
+ * @param emit - The Vue emit function for sending update events to parent
1378
+ *
1379
+ * @returns A computed ref that can be used with v-model pattern
1380
+ *
1381
+ * @example
1382
+ * ```typescript
1383
+ * // DEPRECATED: Old way using useSync
1384
+ * export default defineComponent({
1385
+ * props: {
1386
+ * modelValue: String,
1387
+ * disabled: Boolean,
1388
+ * },
1389
+ * emits: ['update:modelValue'],
1390
+ * setup(props, { emit }) {
1391
+ * const syncedValue = useSync(props, 'modelValue', emit);
1392
+ * return { syncedValue };
1393
+ * }
1394
+ * });
1395
+ *
1396
+ * // RECOMMENDED: New way using defineModel (Vue 3.4+)
1397
+ * <script setup lang="ts">
1398
+ * const modelValue = defineModel<string>();
1399
+ * const disabled = defineProps<{ disabled?: boolean }>();
1400
+ * <\/script>
1401
+ *
1402
+ * <template>
1403
+ * <input v-model="modelValue" :disabled="disabled" />
1404
+ * </template>
1405
+ * ```
1406
+ *
1407
+ * @example
1408
+ * ```typescript
1409
+ * // DEPRECATED: Custom input component with useSync
1410
+ * interface Props {
1411
+ * value: string;
1412
+ * placeholder?: string;
1413
+ * type?: string;
1414
+ * }
1415
+ *
1416
+ * export default defineComponent({
1417
+ * props: {
1418
+ * value: { type: String, required: true },
1419
+ * placeholder: String,
1420
+ * type: { type: String, default: 'text' },
1421
+ * },
1422
+ * emits: ['update:value'],
1423
+ * setup(props: Props, { emit }) {
1424
+ * const syncedValue = useSync(props, 'value', emit);
1425
+ * return { syncedValue };
1426
+ * }
1427
+ * });
1428
+ *
1429
+ * // RECOMMENDED: Using defineModel with custom prop name
1430
+ * <script setup lang="ts">
1431
+ * const value = defineModel<string>('value', { required: true });
1432
+ * const { placeholder, type = 'text' } = defineProps<{
1433
+ * placeholder?: string;
1434
+ * type?: string;
1435
+ * }>();
1436
+ * <\/script>
1437
+ * ```
1438
+ *
1439
+ * @example
1440
+ * ```typescript
1441
+ * // DEPRECATED: Usage with complex objects using useSync
1442
+ * interface UserData {
1443
+ * name: string;
1444
+ * email: string;
1445
+ * age: number;
1446
+ * }
1447
+ *
1448
+ * export default defineComponent({
1449
+ * props: {
1450
+ * userData: { type: Object as PropType<UserData>, required: true },
1451
+ * isLoading: Boolean,
1452
+ * },
1453
+ * emits: ['update:userData'],
1454
+ * setup(props, { emit }) {
1455
+ * const syncedUserData = useSync(props, 'userData', emit);
1456
+ *
1457
+ * const updateName = (newName: string) => {
1458
+ * syncedUserData.value = {
1459
+ * ...syncedUserData.value,
1460
+ * name: newName
1461
+ * };
1462
+ * };
1463
+ *
1464
+ * return { syncedUserData, updateName };
1465
+ * }
1466
+ * });
1467
+ *
1468
+ * // RECOMMENDED: Using defineModel with complex objects
1469
+ * <script setup lang="ts">
1470
+ * interface UserData {
1471
+ * name: string;
1472
+ * email: string;
1473
+ * age: number;
1474
+ * }
1475
+ *
1476
+ * const userData = defineModel<UserData>('userData', { required: true });
1477
+ * const { isLoading } = defineProps<{ isLoading?: boolean }>();
1478
+ *
1479
+ * const updateName = (newName: string) => {
1480
+ * userData.value = {
1481
+ * ...userData.value,
1482
+ * name: newName
1483
+ * };
1484
+ * };
1485
+ * <\/script>
1486
+ * ```
1487
+ */
674
1488
  function useSync(props, key, emit) {
675
- return computed8({
676
- get() {
677
- return props[key];
678
- },
679
- set(newVal) {
680
- emit(`update:${key}`, newVal);
681
- }
682
- });
1489
+ return computed({
1490
+ get() {
1491
+ return props[key];
1492
+ },
1493
+ set(newVal) {
1494
+ emit(`update:${key}`, newVal);
1495
+ }
1496
+ });
683
1497
  }
684
- export {
685
- createLayoutWrapper,
686
- isWritableProp,
687
- sizeProps,
688
- useApi,
689
- useCollection,
690
- useCustomSelection,
691
- useCustomSelectionMultiple,
692
- useElementSize,
693
- useExtensions,
694
- useFilterFields,
695
- useGroupable,
696
- useGroupableParent,
697
- useItems,
698
- useLayout,
699
- useSdk,
700
- useSizeClass,
701
- useStores,
702
- useSync
703
- };
1498
+
1499
+ //#endregion
1500
+ export { createLayoutWrapper, isWritableProp, sizeProps, useApi, useCollection, useCustomSelection, useCustomSelectionMultiple, useElementSize, useExtensions, useFilterFields, useGroupable, useGroupableParent, useItems, useLayout, useSdk, useSizeClass, useStores, useSync };