@directus/composables 11.2.2 → 11.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +132 -122
  2. package/dist/index.js +1460 -658
  3. package/package.json +9 -9
package/dist/index.js CHANGED
@@ -1,703 +1,1505 @@
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 { isEqual, isNil, throttle } 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
+ const defaults = computed(() => {
376
+ if (!fields.value) return {};
377
+ const defaults$1 = {};
378
+ for (const field of fields.value) if (field.schema !== null && "default_value" in field.schema) defaults$1[field.field] = field.schema.default_value;
379
+ return defaults$1;
380
+ });
381
+ const primaryKeyField = computed(() => {
382
+ return fields.value.find((field) => field.collection === collection.value && field.schema?.is_primary_key === true) || null;
383
+ });
384
+ const userCreatedField = computed(() => {
385
+ return fields.value?.find((field) => (field.meta?.special || []).includes("user_created")) || null;
386
+ });
387
+ const sortField = computed(() => {
388
+ return info.value?.meta?.sort_field || null;
389
+ });
390
+ const isSingleton = computed(() => {
391
+ return info.value?.meta?.singleton === true;
392
+ });
393
+ const accountabilityScope = computed(() => {
394
+ return info.value?.meta?.accountability || null;
395
+ });
396
+ return {
397
+ info,
398
+ fields,
399
+ defaults,
400
+ primaryKeyField,
401
+ userCreatedField,
402
+ sortField,
403
+ isSingleton,
404
+ accountabilityScope
405
+ };
67
406
  }
68
407
 
69
- // src/use-custom-selection.ts
70
- import { nanoid } from "nanoid";
71
- import { computed as computed2, ref as ref2, watch } from "vue";
408
+ //#endregion
409
+ //#region src/use-custom-selection.ts
410
+ /**
411
+ * A Vue composable for managing custom selection values that aren't present in a predefined list of items.
412
+ *
413
+ * This composable is typically used in form components where users can select from a predefined list
414
+ * of options, but also have the ability to enter custom values that aren't in the list. It manages
415
+ * the state and logic for detecting when a custom value is being used and provides a reactive
416
+ * interface for getting and setting custom values.
417
+ *
418
+ * @param currentValue - A reactive reference to the currently selected value. Can be null if no value is selected.
419
+ * @param items - A reactive reference to the array of available predefined items. Each item should have a 'value' property.
420
+ * @param emit - A callback function to emit value changes to the parent component.
421
+ *
422
+ * @returns An object containing:
423
+ * - `otherValue` - A computed ref for getting/setting custom values. Returns current value when using custom,
424
+ * empty string otherwise. Setting triggers the emit callback.
425
+ * - `usesOtherValue` - A computed boolean indicating whether the current value is a custom value
426
+ * (not found in the predefined items list).
427
+ *
428
+ * @example
429
+ * ```typescript
430
+ * const currentValue = ref('custom-option');
431
+ * const items = ref([
432
+ * { value: 'option1', label: 'Option 1' },
433
+ * { value: 'option2', label: 'Option 2' }
434
+ * ]);
435
+ * const emit = (value: string | null) => console.log('Value changed:', value);
436
+ *
437
+ * const { otherValue, usesOtherValue } = useCustomSelection(currentValue, items, emit);
438
+ *
439
+ * console.log(usesOtherValue.value); // true (custom-option not in items)
440
+ * console.log(otherValue.value); // 'custom-option'
441
+ *
442
+ * otherValue.value = 'new-custom-value'; // Triggers emit with 'new-custom-value'
443
+ * ```
444
+ */
72
445
  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 };
446
+ const localOtherValue = ref("");
447
+ const otherValue = computed({
448
+ get() {
449
+ return localOtherValue.value || (usesOtherValue.value ? currentValue.value : "");
450
+ },
451
+ set(newValue) {
452
+ if (newValue === null) {
453
+ localOtherValue.value = "";
454
+ emit(null);
455
+ } else {
456
+ localOtherValue.value = newValue;
457
+ emit(newValue);
458
+ }
459
+ }
460
+ });
461
+ const usesOtherValue = computed(() => {
462
+ if (items.value === null) return false;
463
+ const values = items.value.map((item) => item.value);
464
+ return currentValue.value !== null && currentValue.value.length > 0 && values.includes(currentValue.value) === false;
465
+ });
466
+ return {
467
+ otherValue,
468
+ usesOtherValue
469
+ };
94
470
  }
471
+ /**
472
+ * A Vue composable for managing multiple custom selection values that aren't present in a predefined list of items.
473
+ *
474
+ * This composable extends the single custom selection pattern to support multiple values. It's typically used
475
+ * in multi-select form components where users can select multiple predefined options and also add custom
476
+ * values that aren't in the predefined list. It automatically detects custom values in the current selection,
477
+ * manages their state, and provides functions for adding and updating custom values.
478
+ *
479
+ * @param currentValues - A reactive reference to the currently selected values array. Can be null if no values are selected.
480
+ * @param items - A reactive reference to the array of available predefined items. Each item should have a 'value' property.
481
+ * @param emit - A callback function to emit value changes to the parent component.
482
+ *
483
+ * @returns An object containing:
484
+ * - `otherValues` - A reactive array of custom value objects, each with a unique key, value, and optional focus state.
485
+ * - `addOtherValue` - A function to add a new custom value with optional value and focus parameters.
486
+ * - `setOtherValue` - A function to update or remove a custom value by its key, automatically syncing with currentValues.
487
+ *
488
+ * @example
489
+ * ```typescript
490
+ * const currentValues = ref(['option1', 'custom-value1', 'custom-value2']);
491
+ * const items = ref([
492
+ * { value: 'option1', label: 'Option 1' },
493
+ * { value: 'option2', label: 'Option 2' }
494
+ * ]);
495
+ * const emit = (values: string[] | null) => console.log('Values changed:', values);
496
+ *
497
+ * const { otherValues, addOtherValue, setOtherValue } = useCustomSelectionMultiple(currentValues, items, emit);
498
+ *
499
+ * console.log(otherValues.value); // [{ key: 'abc123', value: 'custom-value1' }, { key: 'def456', value: 'custom-value2' }]
500
+ *
501
+ * // Add a new custom value
502
+ * addOtherValue('new-custom-value', true);
503
+ *
504
+ * // Update an existing custom value
505
+ * setOtherValue('abc123', 'updated-custom-value');
506
+ *
507
+ * // Remove a custom value
508
+ * setOtherValue('def456', null);
509
+ * ```
510
+ */
95
511
  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
- }
512
+ const otherValues = ref([]);
513
+ watch(currentValues, (newValue) => {
514
+ if (newValue === null) return;
515
+ if (!Array.isArray(newValue)) return;
516
+ if (items.value === null) return;
517
+ newValue.forEach((value) => {
518
+ if (items.value === null) return;
519
+ if (!items.value.map((item) => item.value).includes(value)) {
520
+ if (!otherValues.value.map((o) => o.value).includes(value)) addOtherValue(value);
521
+ }
522
+ });
523
+ }, { immediate: true });
524
+ return {
525
+ otherValues,
526
+ addOtherValue,
527
+ setOtherValue
528
+ };
529
+ function addOtherValue(value = "", focus = false) {
530
+ otherValues.value = [...otherValues.value, {
531
+ key: nanoid(),
532
+ value,
533
+ focus
534
+ }];
535
+ }
536
+ function setOtherValue(key, newValue) {
537
+ const previousValue = otherValues.value.find((o) => o.key === key);
538
+ const valueWithoutPrevious = (currentValues.value || []).filter((val) => val !== previousValue?.value);
539
+ if (newValue === null) {
540
+ otherValues.value = otherValues.value.filter((o) => o.key !== key);
541
+ if (valueWithoutPrevious.length === 0) emit(null);
542
+ else emit(valueWithoutPrevious);
543
+ } else {
544
+ otherValues.value = otherValues.value.map((otherValue) => {
545
+ if (otherValue.key === key) otherValue.value = newValue;
546
+ return otherValue;
547
+ });
548
+ if (valueWithoutPrevious.length === currentValues.value?.length) emit(valueWithoutPrevious);
549
+ else emit([...valueWithoutPrevious, newValue]);
550
+ }
551
+ }
153
552
  }
154
553
 
155
- // src/use-element-size.ts
156
- import { isNil } from "lodash-es";
157
- import { isRef, onMounted, onUnmounted, ref as ref3 } from "vue";
554
+ //#endregion
555
+ //#region src/use-element-size.ts
556
+ /**
557
+ * A Vue composable that reactively tracks the size of a DOM element using ResizeObserver.
558
+ *
559
+ * @template T - The type of the element being observed, must extend Element
560
+ * @param target - The element to observe. Can be:
561
+ * - A direct element reference
562
+ * - A Vue ref containing an element
563
+ * - A Vue ref that might be undefined
564
+ *
565
+ * @returns An object containing reactive width and height values:
566
+ * - width: Ref<number> - The current width of the element in pixels
567
+ * - height: Ref<number> - The current height of the element in pixels
568
+ *
569
+ * @example
570
+ * ```typescript
571
+ * // With a template ref
572
+ * const elementRef = ref<HTMLDivElement>();
573
+ * const { width, height } = useElementSize(elementRef);
574
+ *
575
+ * // With a direct element
576
+ * const element = document.getElementById('my-element');
577
+ * const { width, height } = useElementSize(element);
578
+ * ```
579
+ *
580
+ * @remarks
581
+ * - The composable automatically sets up a ResizeObserver when the component mounts
582
+ * - The observer is automatically disconnected when the component unmounts
583
+ * - Initial values are 0 until the first resize event
584
+ * - Handles cases where the target element might be undefined
585
+ */
158
586
  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 };
587
+ const width = ref(0);
588
+ const height = ref(0);
589
+ const resizeObserver = new ResizeObserver(([entry]) => {
590
+ if (entry === void 0) return;
591
+ width.value = entry.contentRect.width;
592
+ height.value = entry.contentRect.height;
593
+ });
594
+ onMounted(() => {
595
+ const t = isRef(target) ? target.value : target;
596
+ if (!isNil(t)) resizeObserver.observe(t);
597
+ });
598
+ onUnmounted(() => {
599
+ resizeObserver.disconnect();
600
+ });
601
+ return {
602
+ width,
603
+ height
604
+ };
176
605
  }
177
606
 
178
- // src/use-filter-fields.ts
179
- import { computed as computed3 } from "vue";
607
+ //#endregion
608
+ //#region src/use-filter-fields.ts
609
+ /**
610
+ * A Vue composable that filters and groups fields based on multiple filter criteria.
611
+ *
612
+ * @template T - The type of filter names as string literals
613
+ * @param fields - A Vue ref containing an array of Field objects to be filtered
614
+ * @param filters - An object where keys are filter names and values are predicate functions
615
+ * that return true if a field should be included in that group
616
+ *
617
+ * @returns An object containing:
618
+ * - fieldGroups: ComputedRef<Record<Extract<T, string>, Field[]>> - A reactive object
619
+ * where each key corresponds to a filter name and the value is an array of fields
620
+ * that pass that filter
621
+ *
622
+ * @example
623
+ * ```typescript
624
+ * // Define filter criteria
625
+ * const fieldFilters = {
626
+ * required: (field: Field) => field.required === true,
627
+ * optional: (field: Field) => field.required !== true,
628
+ * text: (field: Field) => field.type === 'string',
629
+ * numeric: (field: Field) => ['integer', 'float', 'decimal'].includes(field.type)
630
+ * };
631
+ *
632
+ * const fieldsRef = ref<Field[]>([
633
+ * { name: 'id', type: 'integer', required: true },
634
+ * { name: 'title', type: 'string', required: true },
635
+ * { name: 'description', type: 'text', required: false },
636
+ * { name: 'price', type: 'decimal', required: false }
637
+ * ]);
638
+ *
639
+ * const { fieldGroups } = useFilterFields(fieldsRef, fieldFilters);
640
+ *
641
+ * // Access filtered groups
642
+ * console.log(fieldGroups.value.required); // [id, title]
643
+ * console.log(fieldGroups.value.text); // [title]
644
+ * console.log(fieldGroups.value.numeric); // [id, price]
645
+ * ```
646
+ *
647
+ * @remarks
648
+ * - Fields can appear in multiple groups if they pass multiple filters
649
+ * - If a field doesn't pass any filter, it won't appear in any group
650
+ * - The result is reactive and will update when the input fields change
651
+ * - Filter functions are called for each field against each filter criterion
652
+ * - Groups are initialized as empty arrays even if no fields match the criteria
653
+ */
180
654
  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 };
655
+ return { fieldGroups: computed(() => {
656
+ const acc = {};
657
+ for (const name in filters) acc[name] = [];
658
+ return fields.value.reduce((acc$1, field) => {
659
+ for (const name in filters) {
660
+ if (filters[name](field) === false) continue;
661
+ acc$1[name].push(field);
662
+ }
663
+ return acc$1;
664
+ }, acc);
665
+ }) };
195
666
  }
196
667
 
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";
668
+ //#endregion
669
+ //#region src/use-groupable.ts
670
+ /**
671
+ * Vue composable for creating groupable child items that can participate in group selection.
672
+ *
673
+ * This composable enables a component to be part of a group context managed by a parent component
674
+ * using `useGroupableParent`. It provides reactive active state management and selection control.
675
+ *
676
+ * @param options - Configuration options for the groupable item
677
+ * @param options.value - Unique identifier for this item within the group
678
+ * @param options.group - Name of the group to inject from (defaults to 'item-group')
679
+ * @param options.active - External reactive reference to control the active state
680
+ * @param options.watch - Whether to watch the external active reference for changes
681
+ *
682
+ * @returns Object containing active state and control methods
683
+ *
684
+ * @example
685
+ * ```vue
686
+ * <script setup>
687
+ * import { useGroupable } from '@directus/composables';
688
+ *
689
+ * const props = defineProps(['value', 'active']);
690
+ *
691
+ * const { active, toggle, activate, deactivate } = useGroupable({
692
+ * value: props.value,
693
+ * active: toRef(props, 'active'),
694
+ * watch: true
695
+ * });
696
+ * <\/script>
697
+ * ```
698
+ */
200
699
  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
- };
700
+ const parentFunctions = inject(options?.group || "item-group", null);
701
+ if (isNil(parentFunctions)) return {
702
+ active: ref(false),
703
+ toggle: () => {},
704
+ activate: () => {},
705
+ deactivate: () => {}
706
+ };
707
+ const { register, unregister, toggle, selection } = parentFunctions;
708
+ let startActive = false;
709
+ if (options?.active?.value === true) startActive = true;
710
+ if (options?.value && selection.value.includes(options.value)) startActive = true;
711
+ const active = ref(startActive);
712
+ const item = {
713
+ active,
714
+ value: options?.value
715
+ };
716
+ register(item);
717
+ if (options?.active !== void 0 && options.watch === true) watch(options.active, () => {
718
+ if (options.active === void 0) return;
719
+ if (options.active.value === true) {
720
+ if (active.value === false) toggle(item);
721
+ active.value = true;
722
+ }
723
+ if (options.active.value === false) {
724
+ if (active.value === true) toggle(item);
725
+ active.value = false;
726
+ }
727
+ });
728
+ onBeforeUnmount(() => unregister(item));
729
+ return {
730
+ active,
731
+ toggle: () => {
732
+ toggle(item);
733
+ },
734
+ activate: () => {
735
+ if (active.value === false) toggle(item);
736
+ },
737
+ deactivate: () => {
738
+ if (active.value === true) toggle(item);
739
+ }
740
+ };
251
741
  }
742
+ /**
743
+ * Vue composable for creating a group parent that manages multiple groupable child items.
744
+ *
745
+ * This composable provides the foundation for components that need to manage a collection
746
+ * of selectable items, such as tabs, radio groups, or multi-select lists. It handles
747
+ * registration of child items, selection state management, and provides various selection
748
+ * constraints (mandatory, maximum, multiple).
749
+ *
750
+ * @param state - External state configuration for selection management
751
+ * @param state.selection - External selection state reference
752
+ * @param state.onSelectionChange - Callback fired when selection changes
753
+ * @param state.onToggle - Callback fired when an item is toggled
754
+ * @param options - Configuration options for selection behavior
755
+ * @param options.mandatory - Whether at least one item must always be selected
756
+ * @param options.max - Maximum number of items that can be selected (-1 for unlimited)
757
+ * @param options.multiple - Whether multiple items can be selected simultaneously
758
+ * @param group - Injection key for the group (defaults to 'item-group')
759
+ *
760
+ * @returns Object containing items array, selection state, and utility functions
761
+ *
762
+ * @example
763
+ * ```vue
764
+ * <script setup>
765
+ * import { useGroupableParent } from '@directus/composables';
766
+ * import { ref } from 'vue';
767
+ *
768
+ * const selectedItems = ref([]);
769
+ * const isMultiple = ref(true);
770
+ * const isMandatory = ref(false);
771
+ *
772
+ * const { items, selection } = useGroupableParent(
773
+ * {
774
+ * selection: selectedItems,
775
+ * onSelectionChange: (values) => {
776
+ * console.log('Selection changed:', values);
777
+ * }
778
+ * },
779
+ * {
780
+ * multiple: isMultiple,
781
+ * mandatory: isMandatory,
782
+ * max: ref(3)
783
+ * }
784
+ * );
785
+ * <\/script>
786
+ * ```
787
+ */
252
788
  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
- }
789
+ const items = shallowRef([]);
790
+ const internalSelection = ref([]);
791
+ const selection = computed({
792
+ get() {
793
+ if (!isNil(state.selection) && !isNil(state.selection.value)) return state.selection.value;
794
+ return internalSelection.value;
795
+ },
796
+ set(newSelection) {
797
+ if (!isNil(state.onSelectionChange)) state.onSelectionChange(newSelection);
798
+ internalSelection.value = [...newSelection];
799
+ }
800
+ });
801
+ provide(group, {
802
+ register,
803
+ unregister,
804
+ toggle,
805
+ selection
806
+ });
807
+ watch(selection, updateChildren, { immediate: true });
808
+ nextTick().then(updateChildren);
809
+ watch(() => options?.mandatory?.value, (newValue, oldValue) => {
810
+ if (isEqual(newValue, oldValue)) return;
811
+ if (!selection.value || selection.value.length === 0 && options?.mandatory?.value === true) {
812
+ if (items.value[0]) selection.value = [getValueForItem(items.value[0])];
813
+ }
814
+ });
815
+ return {
816
+ items,
817
+ selection,
818
+ internalSelection,
819
+ getValueForItem,
820
+ updateChildren
821
+ };
822
+ function register(item) {
823
+ items.value = [...items.value, item];
824
+ const value = getValueForItem(item);
825
+ if (selection.value.length === 0 && options?.mandatory?.value === true && items.value.length === 1) selection.value = [value];
826
+ if (item.active.value && selection.value.includes(value) === false) toggle(item);
827
+ }
828
+ function unregister(item) {
829
+ items.value = items.value.filter((existingItem) => {
830
+ return existingItem !== item;
831
+ });
832
+ }
833
+ function toggle(item) {
834
+ if (options?.multiple?.value === true) toggleMultiple(item);
835
+ else toggleSingle(item);
836
+ if (!isNil(state.onToggle)) state.onToggle(item);
837
+ }
838
+ function toggleSingle(item) {
839
+ const itemValue = getValueForItem(item);
840
+ if (selection.value[0] === itemValue && options?.mandatory?.value !== true) {
841
+ selection.value = [];
842
+ return;
843
+ }
844
+ if (selection.value[0] !== itemValue) selection.value = [itemValue];
845
+ }
846
+ function toggleMultiple(item) {
847
+ const itemValue = getValueForItem(item);
848
+ if (selection.value.includes(itemValue)) {
849
+ if (options?.mandatory?.value === true && selection.value.length === 1) {
850
+ updateChildren();
851
+ return;
852
+ }
853
+ selection.value = selection.value.filter((value) => value !== itemValue);
854
+ return;
855
+ }
856
+ if (options?.max?.value && options.max.value !== -1 && selection.value.length >= options.max.value) {
857
+ updateChildren();
858
+ return;
859
+ }
860
+ selection.value = [...selection.value, itemValue];
861
+ }
862
+ function getValueForItem(item) {
863
+ return item.value || items.value.findIndex((child) => item === child);
864
+ }
865
+ function updateChildren() {
866
+ items.value.forEach((item) => {
867
+ item.active.value = selection.value.includes(getValueForItem(item));
868
+ });
869
+ }
341
870
  }
342
871
 
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";
872
+ //#endregion
873
+ //#region src/use-items.ts
348
874
  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
- }
875
+ const api = useApi();
876
+ const { primaryKeyField } = useCollection(collection);
877
+ const { fields, limit, sort, search, filter, page, filterSystem, alias, deep } = query;
878
+ const endpoint = computed(() => {
879
+ if (!collection.value) return null;
880
+ return getEndpoint(collection.value);
881
+ });
882
+ const items = ref([]);
883
+ const loading = ref(false);
884
+ const error = ref(null);
885
+ const itemCount = ref(null);
886
+ const totalCount = ref(null);
887
+ const totalPages = computed(() => {
888
+ if (itemCount.value === null) return 1;
889
+ if (itemCount.value < (unref(limit) ?? 100)) return 1;
890
+ return Math.ceil(itemCount.value / (unref(limit) ?? 100));
891
+ });
892
+ const existingRequests = {
893
+ items: null,
894
+ total: null,
895
+ filter: null
896
+ };
897
+ let loadingTimeout = null;
898
+ const fetchItems = throttle(getItems, 500);
899
+ watch([
900
+ collection,
901
+ limit,
902
+ sort,
903
+ search,
904
+ filter,
905
+ fields,
906
+ page,
907
+ toRef(alias),
908
+ toRef(deep)
909
+ ], async (after, before) => {
910
+ if (isEqual(after, before)) return;
911
+ const [newCollection, newLimit, newSort, newSearch, newFilter] = after;
912
+ const [oldCollection, oldLimit, oldSort, oldSearch, oldFilter] = before;
913
+ if (!newCollection || !query) return;
914
+ if (newCollection !== oldCollection) reset();
915
+ if (!isEqual(newFilter, oldFilter) || !isEqual(newSort, oldSort) || newLimit !== oldLimit || newSearch !== oldSearch) {
916
+ if (oldCollection) page.value = 1;
917
+ }
918
+ if (newCollection !== oldCollection || !isEqual(newFilter, oldFilter) || newSearch !== oldSearch) getItemCount();
919
+ fetchItems();
920
+ }, {
921
+ deep: true,
922
+ immediate: true
923
+ });
924
+ watch([collection, toRef(filterSystem)], async (after, before) => {
925
+ if (isEqual(after, before)) return;
926
+ getTotalCount();
927
+ }, {
928
+ deep: true,
929
+ immediate: true
930
+ });
931
+ return {
932
+ itemCount,
933
+ totalCount,
934
+ items,
935
+ totalPages,
936
+ loading,
937
+ error,
938
+ changeManualSort,
939
+ getItems,
940
+ getItemCount,
941
+ getTotalCount
942
+ };
943
+ async function getItems() {
944
+ if (!endpoint.value) return;
945
+ let isCurrentRequestCanceled = false;
946
+ if (existingRequests.items) existingRequests.items.abort();
947
+ existingRequests.items = new AbortController();
948
+ error.value = null;
949
+ if (loadingTimeout) clearTimeout(loadingTimeout);
950
+ loadingTimeout = setTimeout(() => {
951
+ loading.value = true;
952
+ }, 150);
953
+ let fieldsToFetch = [...unref(fields) ?? []];
954
+ if (!unref(fields)?.includes("*") && primaryKeyField.value && fieldsToFetch.includes(primaryKeyField.value.field) === false) fieldsToFetch.push(primaryKeyField.value.field);
955
+ fieldsToFetch = fieldsToFetch.filter((field) => field.startsWith("$") === false);
956
+ try {
957
+ let fetchedItems = (await api.get(endpoint.value, {
958
+ params: {
959
+ limit: unref(limit),
960
+ fields: fieldsToFetch,
961
+ ...alias ? { alias: unref(alias) } : {},
962
+ sort: unref(sort),
963
+ page: unref(page),
964
+ search: unref(search),
965
+ filter: unref(filter),
966
+ deep: unref(deep)
967
+ },
968
+ signal: existingRequests.items.signal
969
+ })).data.data;
970
+ existingRequests.items = null;
971
+ /**
972
+ * @NOTE
973
+ *
974
+ * This is used in conjunction with the fake field in /src/stores/fields/fields.ts to be
975
+ * able to render out the directus_files collection (file library) using regular layouts
976
+ *
977
+ * Layouts expect the file to be a m2o of a `file` type, however, directus_files is the
978
+ * only collection that doesn't have this (obviously). This fake $thumbnail field is used to
979
+ * pretend there is a file m2o, so we can use the regular layout logic for files as well
980
+ */
981
+ if (collection.value === "directus_files") fetchedItems = fetchedItems.map((file) => ({
982
+ ...file,
983
+ $thumbnail: file
984
+ }));
985
+ items.value = fetchedItems;
986
+ if (page && fetchedItems.length === 0 && page?.value !== 1) page.value = 1;
987
+ } catch (err) {
988
+ if (axios.isCancel(err)) isCurrentRequestCanceled = true;
989
+ else error.value = err;
990
+ } finally {
991
+ if (loadingTimeout && !isCurrentRequestCanceled) {
992
+ clearTimeout(loadingTimeout);
993
+ loadingTimeout = null;
994
+ }
995
+ if (!loadingTimeout) loading.value = false;
996
+ }
997
+ }
998
+ function reset() {
999
+ items.value = [];
1000
+ totalCount.value = null;
1001
+ itemCount.value = null;
1002
+ }
1003
+ async function changeManualSort({ item, to }) {
1004
+ const pk = primaryKeyField.value?.field;
1005
+ if (!pk) return;
1006
+ const fromIndex = items.value.findIndex((existing) => existing[pk] === item);
1007
+ const toIndex = items.value.findIndex((existing) => existing[pk] === to);
1008
+ items.value = moveInArray(items.value, fromIndex, toIndex);
1009
+ const endpoint$1 = computed(() => `/utils/sort/${collection.value}`);
1010
+ await api.post(endpoint$1.value, {
1011
+ item,
1012
+ to
1013
+ });
1014
+ }
1015
+ async function getTotalCount() {
1016
+ if (!endpoint.value) return;
1017
+ try {
1018
+ if (existingRequests.total) existingRequests.total.abort();
1019
+ existingRequests.total = new AbortController();
1020
+ const aggregate = primaryKeyField.value ? { countDistinct: primaryKeyField.value.field } : { count: "*" };
1021
+ const response = await api.get(endpoint.value, {
1022
+ params: {
1023
+ aggregate,
1024
+ filter: unref(filterSystem)
1025
+ },
1026
+ signal: existingRequests.total.signal
1027
+ });
1028
+ const count = primaryKeyField.value ? Number(response.data.data[0].countDistinct[primaryKeyField.value.field]) : Number(response.data.data[0].count);
1029
+ existingRequests.total = null;
1030
+ totalCount.value = count;
1031
+ } catch (err) {
1032
+ if (!axios.isCancel(err)) throw err;
1033
+ }
1034
+ }
1035
+ async function getItemCount() {
1036
+ if (!endpoint.value) return;
1037
+ try {
1038
+ if (existingRequests.filter) existingRequests.filter.abort();
1039
+ existingRequests.filter = new AbortController();
1040
+ const aggregate = primaryKeyField.value ? { countDistinct: primaryKeyField.value.field } : { count: "*" };
1041
+ const response = await api.get(endpoint.value, {
1042
+ params: {
1043
+ filter: unref(filter),
1044
+ search: unref(search),
1045
+ aggregate
1046
+ },
1047
+ signal: existingRequests.filter.signal
1048
+ });
1049
+ const count = primaryKeyField.value ? Number(response.data.data[0].countDistinct[primaryKeyField.value.field]) : Number(response.data.data[0].count);
1050
+ existingRequests.filter = null;
1051
+ itemCount.value = count;
1052
+ } catch (err) {
1053
+ if (!axios.isCancel(err)) throw err;
1054
+ }
1055
+ }
539
1056
  }
540
1057
 
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"];
1058
+ //#endregion
1059
+ //#region src/use-layout.ts
1060
+ const NAME_SUFFIX = "wrapper";
1061
+ const WRITABLE_PROPS = [
1062
+ "selection",
1063
+ "layoutOptions",
1064
+ "layoutQuery"
1065
+ ];
1066
+ /**
1067
+ * Type guard to check if a property is writable (can be updated via emit).
1068
+ *
1069
+ * This function determines whether a given property name corresponds to one of the
1070
+ * writable properties that can be updated through Vue's emit system.
1071
+ *
1072
+ * @param prop - The property name to check
1073
+ * @returns True if the property is writable, false otherwise
1074
+ *
1075
+ * @example
1076
+ * ```typescript
1077
+ * if (isWritableProp('selection')) {
1078
+ * // Property is writable, can emit update
1079
+ * emit('update:selection', newValue);
1080
+ * }
1081
+ * ```
1082
+ */
545
1083
  function isWritableProp(prop) {
546
- return WRITABLE_PROPS.includes(prop);
1084
+ return WRITABLE_PROPS.includes(prop);
547
1085
  }
1086
+ /**
1087
+ * Creates a Vue component wrapper for a layout configuration.
1088
+ *
1089
+ * This function creates a dynamic Vue component that wraps a layout with standardized
1090
+ * props, emits, and state management. It handles reactive state updates, prop validation,
1091
+ * and provides a consistent interface for all layout components.
1092
+ *
1093
+ * @template Options - The type for layout-specific options
1094
+ * @template Query - The type for layout-specific query parameters
1095
+ * @param layout - The layout configuration object containing id and setup function
1096
+ * @returns A Vue component that can be used to render the layout
1097
+ *
1098
+ * @example
1099
+ * ```typescript
1100
+ * interface MyLayoutOptions {
1101
+ * itemSize: number;
1102
+ * showHeaders: boolean;
1103
+ * }
1104
+ *
1105
+ * interface MyLayoutQuery {
1106
+ * page: number;
1107
+ * limit: number;
1108
+ * }
1109
+ *
1110
+ * const layoutConfig: LayoutConfig = {
1111
+ * id: 'my-layout',
1112
+ * setup: (props, { emit }) => ({
1113
+ * // Layout-specific setup logic
1114
+ * })
1115
+ * };
1116
+ *
1117
+ * const LayoutWrapper = createLayoutWrapper<MyLayoutOptions, MyLayoutQuery>(layoutConfig);
1118
+ * ```
1119
+ */
548
1120
  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
- });
1121
+ return defineComponent({
1122
+ name: `${layout.id}-${NAME_SUFFIX}`,
1123
+ props: {
1124
+ collection: {
1125
+ type: String,
1126
+ required: true
1127
+ },
1128
+ selection: {
1129
+ type: Array,
1130
+ default: () => []
1131
+ },
1132
+ layoutOptions: {
1133
+ type: Object,
1134
+ default: () => ({})
1135
+ },
1136
+ layoutQuery: {
1137
+ type: Object,
1138
+ default: () => ({})
1139
+ },
1140
+ layoutProps: {
1141
+ type: Object,
1142
+ default: () => ({})
1143
+ },
1144
+ filter: {
1145
+ type: Object,
1146
+ default: null
1147
+ },
1148
+ filterUser: {
1149
+ type: Object,
1150
+ default: null
1151
+ },
1152
+ filterSystem: {
1153
+ type: Object,
1154
+ default: null
1155
+ },
1156
+ search: {
1157
+ type: String,
1158
+ default: null
1159
+ },
1160
+ showSelect: {
1161
+ type: String,
1162
+ default: "multiple"
1163
+ },
1164
+ selectMode: {
1165
+ type: Boolean,
1166
+ default: false
1167
+ },
1168
+ readonly: {
1169
+ type: Boolean,
1170
+ default: false
1171
+ },
1172
+ resetPreset: {
1173
+ type: Function,
1174
+ default: null
1175
+ },
1176
+ clearFilters: {
1177
+ type: Function,
1178
+ default: null
1179
+ }
1180
+ },
1181
+ emits: WRITABLE_PROPS.map((prop) => `update:${prop}`),
1182
+ setup(props, { emit }) {
1183
+ const state = reactive({
1184
+ ...layout.setup(props, { emit }),
1185
+ ...toRefs(props)
1186
+ });
1187
+ for (const key in state) state[`onUpdate:${key}`] = (value) => {
1188
+ if (isWritableProp(key)) emit(`update:${key}`, value);
1189
+ else if (!Object.keys(props).includes(key)) state[key] = value;
1190
+ };
1191
+ return { state };
1192
+ },
1193
+ render(ctx) {
1194
+ return ctx.$slots.default !== void 0 ? ctx.$slots.default({ layoutState: ctx.state }) : null;
1195
+ }
1196
+ });
627
1197
  }
1198
+ /**
1199
+ * Composable for managing layout components in Directus.
1200
+ *
1201
+ * This composable provides access to layout components and handles the dynamic
1202
+ * selection of layout wrappers based on the provided layout ID. It automatically
1203
+ * falls back to the tabular layout if the requested layout is not found.
1204
+ *
1205
+ * @template Options - The type for layout-specific options (default: any)
1206
+ * @template Query - The type for layout-specific query parameters (default: any)
1207
+ * @param layoutId - A reactive reference to the layout ID
1208
+ * @returns An object containing the layout wrapper component
1209
+ *
1210
+ * @example
1211
+ * ```typescript
1212
+ * import { ref } from 'vue';
1213
+ * import { useLayout } from './use-layout';
1214
+ *
1215
+ * const selectedLayoutId = ref('table');
1216
+ * const { layoutWrapper } = useLayout(selectedLayoutId);
1217
+ *
1218
+ * // Use the layout wrapper in your template
1219
+ * // <component :is="layoutWrapper" :collection="'users'" />
1220
+ * ```
1221
+ *
1222
+ * @example
1223
+ * ```typescript
1224
+ * // With typed options and query
1225
+ * interface TableOptions {
1226
+ * spacing: 'cozy' | 'comfortable' | 'compact';
1227
+ * showHeaders: boolean;
1228
+ * }
1229
+ *
1230
+ * interface TableQuery {
1231
+ * sort: string[];
1232
+ * limit: number;
1233
+ * }
1234
+ *
1235
+ * const layoutId = ref<string | null>('table');
1236
+ * const { layoutWrapper } = useLayout<TableOptions, TableQuery>(layoutId);
1237
+ * ```
1238
+ */
628
1239
  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 };
1240
+ const { layouts } = useExtensions();
1241
+ const layoutWrappers = computed(() => layouts.value.map((layout) => createLayoutWrapper(layout)));
1242
+ return { layoutWrapper: computed(() => {
1243
+ const layout = layoutWrappers.value.find((layout$1) => layout$1.name === `${layoutId.value}-${NAME_SUFFIX}`);
1244
+ if (layout === void 0) return layoutWrappers.value.find((layout$1) => layout$1.name === `tabular-${NAME_SUFFIX}`);
1245
+ return layout;
1246
+ }) };
639
1247
  }
640
1248
 
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
- }
1249
+ //#endregion
1250
+ //#region src/use-size-class.ts
1251
+ /**
1252
+ * Vue props definition for size-related boolean properties.
1253
+ *
1254
+ * This object defines the standard size props that can be used in Vue components
1255
+ * to control size-based styling through CSS classes.
1256
+ *
1257
+ * @example
1258
+ * ```typescript
1259
+ * // In a Vue component
1260
+ * export default defineComponent({
1261
+ * props: {
1262
+ * ...sizeProps,
1263
+ * // other props
1264
+ * },
1265
+ * setup(props) {
1266
+ * const sizeClass = useSizeClass(props);
1267
+ * return { sizeClass };
1268
+ * }
1269
+ * });
1270
+ * ```
1271
+ */
1272
+ const sizeProps = {
1273
+ xSmall: {
1274
+ type: Boolean,
1275
+ default: false
1276
+ },
1277
+ small: {
1278
+ type: Boolean,
1279
+ default: false
1280
+ },
1281
+ large: {
1282
+ type: Boolean,
1283
+ default: false
1284
+ },
1285
+ xLarge: {
1286
+ type: Boolean,
1287
+ default: false
1288
+ }
660
1289
  };
1290
+ /**
1291
+ * Composable for generating CSS size class names based on size props.
1292
+ *
1293
+ * This composable takes props containing size boolean flags and returns a computed
1294
+ * CSS class name string. It follows a priority order: xSmall > small > large > xLarge.
1295
+ * If no size props are true, it returns null.
1296
+ *
1297
+ * @template T - The type of additional props that extend SizeProps
1298
+ * @param props - The props object containing size boolean properties
1299
+ * @returns A computed ref that resolves to the appropriate CSS class name or null
1300
+ *
1301
+ * @example
1302
+ * ```typescript
1303
+ * // Basic usage in a Vue component
1304
+ * const props = { small: true, large: false };
1305
+ * const sizeClass = useSizeClass(props);
1306
+ * console.log(sizeClass.value); // 'small'
1307
+ * ```
1308
+ *
1309
+ * @example
1310
+ * ```typescript
1311
+ * // Usage with additional props
1312
+ * interface MyProps {
1313
+ * color: string;
1314
+ * disabled: boolean;
1315
+ * }
1316
+ *
1317
+ * const props: MyProps & SizeProps = {
1318
+ * color: 'blue',
1319
+ * disabled: false,
1320
+ * xLarge: true
1321
+ * };
1322
+ *
1323
+ * const sizeClass = useSizeClass(props);
1324
+ * console.log(sizeClass.value); // 'x-large'
1325
+ * ```
1326
+ *
1327
+ * @example
1328
+ * ```typescript
1329
+ * // In a Vue component with reactive props
1330
+ * export default defineComponent({
1331
+ * props: {
1332
+ * ...sizeProps,
1333
+ * label: String,
1334
+ * },
1335
+ * setup(props) {
1336
+ * const sizeClass = useSizeClass(props);
1337
+ *
1338
+ * return { sizeClass };
1339
+ * },
1340
+ * template: `
1341
+ * <button :class="['btn', sizeClass]">
1342
+ * {{ label }}
1343
+ * </button>
1344
+ * `
1345
+ * });
1346
+ * ```
1347
+ */
661
1348
  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;
1349
+ return computed(() => {
1350
+ if (props.xSmall) return "x-small";
1351
+ if (props.small) return "small";
1352
+ if (props.large) return "large";
1353
+ if (props.xLarge) return "x-large";
1354
+ return null;
1355
+ });
670
1356
  }
671
1357
 
672
- // src/use-sync.ts
673
- import { computed as computed8 } from "vue";
1358
+ //#endregion
1359
+ //#region src/use-sync.ts
1360
+ /**
1361
+ * Composable for creating two-way binding between parent and child components.
1362
+ *
1363
+ * @deprecated Use Vue's native `defineModel()` instead. This composable is kept for backward compatibility.
1364
+ * Vue 3.4+ provides `defineModel()` which offers a more streamlined and performant way to create v-model bindings.
1365
+ *
1366
+ * @see {@link https://vuejs.org/api/sfc-script-setup.html#definemodel} Vue's defineModel documentation
1367
+ *
1368
+ * This composable creates a computed ref that synchronizes a prop value with
1369
+ * its parent component through Vue's v-model pattern. It provides a getter
1370
+ * that returns the current prop value and a setter that emits an update event
1371
+ * to notify the parent component of changes.
1372
+ *
1373
+ * This is particularly useful for creating custom form components that need
1374
+ * to work with v-model while maintaining proper data flow patterns.
1375
+ *
1376
+ * @template T - The type of the props object
1377
+ * @template K - The key of the prop to sync (must be a string key of T)
1378
+ * @template E - The emit function type with proper event typing
1379
+ *
1380
+ * @param props - The component props object containing the value to sync
1381
+ * @param key - The specific prop key to create a two-way binding for
1382
+ * @param emit - The Vue emit function for sending update events to parent
1383
+ *
1384
+ * @returns A computed ref that can be used with v-model pattern
1385
+ *
1386
+ * @example
1387
+ * ```typescript
1388
+ * // DEPRECATED: Old way using useSync
1389
+ * export default defineComponent({
1390
+ * props: {
1391
+ * modelValue: String,
1392
+ * disabled: Boolean,
1393
+ * },
1394
+ * emits: ['update:modelValue'],
1395
+ * setup(props, { emit }) {
1396
+ * const syncedValue = useSync(props, 'modelValue', emit);
1397
+ * return { syncedValue };
1398
+ * }
1399
+ * });
1400
+ *
1401
+ * // RECOMMENDED: New way using defineModel (Vue 3.4+)
1402
+ * <script setup lang="ts">
1403
+ * const modelValue = defineModel<string>();
1404
+ * const disabled = defineProps<{ disabled?: boolean }>();
1405
+ * <\/script>
1406
+ *
1407
+ * <template>
1408
+ * <input v-model="modelValue" :disabled="disabled" />
1409
+ * </template>
1410
+ * ```
1411
+ *
1412
+ * @example
1413
+ * ```typescript
1414
+ * // DEPRECATED: Custom input component with useSync
1415
+ * interface Props {
1416
+ * value: string;
1417
+ * placeholder?: string;
1418
+ * type?: string;
1419
+ * }
1420
+ *
1421
+ * export default defineComponent({
1422
+ * props: {
1423
+ * value: { type: String, required: true },
1424
+ * placeholder: String,
1425
+ * type: { type: String, default: 'text' },
1426
+ * },
1427
+ * emits: ['update:value'],
1428
+ * setup(props: Props, { emit }) {
1429
+ * const syncedValue = useSync(props, 'value', emit);
1430
+ * return { syncedValue };
1431
+ * }
1432
+ * });
1433
+ *
1434
+ * // RECOMMENDED: Using defineModel with custom prop name
1435
+ * <script setup lang="ts">
1436
+ * const value = defineModel<string>('value', { required: true });
1437
+ * const { placeholder, type = 'text' } = defineProps<{
1438
+ * placeholder?: string;
1439
+ * type?: string;
1440
+ * }>();
1441
+ * <\/script>
1442
+ * ```
1443
+ *
1444
+ * @example
1445
+ * ```typescript
1446
+ * // DEPRECATED: Usage with complex objects using useSync
1447
+ * interface UserData {
1448
+ * name: string;
1449
+ * email: string;
1450
+ * age: number;
1451
+ * }
1452
+ *
1453
+ * export default defineComponent({
1454
+ * props: {
1455
+ * userData: { type: Object as PropType<UserData>, required: true },
1456
+ * isLoading: Boolean,
1457
+ * },
1458
+ * emits: ['update:userData'],
1459
+ * setup(props, { emit }) {
1460
+ * const syncedUserData = useSync(props, 'userData', emit);
1461
+ *
1462
+ * const updateName = (newName: string) => {
1463
+ * syncedUserData.value = {
1464
+ * ...syncedUserData.value,
1465
+ * name: newName
1466
+ * };
1467
+ * };
1468
+ *
1469
+ * return { syncedUserData, updateName };
1470
+ * }
1471
+ * });
1472
+ *
1473
+ * // RECOMMENDED: Using defineModel with complex objects
1474
+ * <script setup lang="ts">
1475
+ * interface UserData {
1476
+ * name: string;
1477
+ * email: string;
1478
+ * age: number;
1479
+ * }
1480
+ *
1481
+ * const userData = defineModel<UserData>('userData', { required: true });
1482
+ * const { isLoading } = defineProps<{ isLoading?: boolean }>();
1483
+ *
1484
+ * const updateName = (newName: string) => {
1485
+ * userData.value = {
1486
+ * ...userData.value,
1487
+ * name: newName
1488
+ * };
1489
+ * };
1490
+ * <\/script>
1491
+ * ```
1492
+ */
674
1493
  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
- });
1494
+ return computed({
1495
+ get() {
1496
+ return props[key];
1497
+ },
1498
+ set(newVal) {
1499
+ emit(`update:${key}`, newVal);
1500
+ }
1501
+ });
683
1502
  }
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
- };
1503
+
1504
+ //#endregion
1505
+ export { createLayoutWrapper, isWritableProp, sizeProps, useApi, useCollection, useCustomSelection, useCustomSelectionMultiple, useElementSize, useExtensions, useFilterFields, useGroupable, useGroupableParent, useItems, useLayout, useSdk, useSizeClass, useStores, useSync };