@directus/composables 11.2.0 → 11.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { AppCollection, Field, Item, Query, RefRecord } from '@directus/types';
2
2
  import { ComputedRef, Ref, WritableComputedRef, Component } from 'vue';
3
- import { AppExtensionConfigs } from '@directus/extensions';
3
+ import { LayoutConfig, AppExtensionConfigs } from '@directus/extensions';
4
4
  import { DirectusClient, RestClient } from '@directus/sdk';
5
5
  import { AxiosInstance } from 'axios';
6
6
 
@@ -14,12 +14,82 @@ type UsableCollection = {
14
14
  isSingleton: ComputedRef<boolean>;
15
15
  accountabilityScope: ComputedRef<'all' | 'activity' | null>;
16
16
  };
17
+ /**
18
+ * A Vue composable that provides reactive access to collection metadata, fields, and computed properties.
19
+ *
20
+ * This composable serves as a centralized way to access and work with Directus collections,
21
+ * providing reactive computed properties for collection information, field definitions,
22
+ * default values, and various collection-specific metadata.
23
+ *
24
+ * @param collectionKey - The collection identifier. Can be a string or a reactive reference to a string.
25
+ * If null, most computed properties will return empty/null values.
26
+ *
27
+ * @returns An object containing reactive computed properties for the collection:
28
+ * - `info` - The complete collection configuration object or null if not found
29
+ * - `fields` - Array of sorted field definitions for the collection
30
+ * - `defaults` - Object mapping field names to their default values from schema
31
+ * - `primaryKeyField` - The field marked as primary key, or null if none exists
32
+ * - `userCreatedField` - The field with 'user_created' special type, or null if none exists
33
+ * - `sortField` - The field name used for sorting, from collection meta, or null
34
+ * - `isSingleton` - Boolean indicating if the collection is configured as a singleton
35
+ * - `accountabilityScope` - The accountability scope setting ('all', 'activity', or null)
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * // Using with a static collection name
40
+ * const { info, fields, primaryKeyField } = useCollection('users');
41
+ *
42
+ * // Using with a reactive collection name
43
+ * const collectionName = ref('articles');
44
+ * const { fields, defaults, isSingleton } = useCollection(collectionName);
45
+ *
46
+ * // Accessing properties
47
+ * console.log(info.value?.name); // Collection display name
48
+ * console.log(fields.value.length); // Number of fields
49
+ * console.log(primaryKeyField.value?.field); // Primary key field name
50
+ * ```
51
+ */
17
52
  declare function useCollection(collectionKey: string | Ref<string | null>): UsableCollection;
18
53
 
19
54
  type UsableCustomSelection = {
20
55
  otherValue: Ref<string | null>;
21
56
  usesOtherValue: ComputedRef<boolean>;
22
57
  };
58
+ /**
59
+ * A Vue composable for managing custom selection values that aren't present in a predefined list of items.
60
+ *
61
+ * This composable is typically used in form components where users can select from a predefined list
62
+ * of options, but also have the ability to enter custom values that aren't in the list. It manages
63
+ * the state and logic for detecting when a custom value is being used and provides a reactive
64
+ * interface for getting and setting custom values.
65
+ *
66
+ * @param currentValue - A reactive reference to the currently selected value. Can be null if no value is selected.
67
+ * @param items - A reactive reference to the array of available predefined items. Each item should have a 'value' property.
68
+ * @param emit - A callback function to emit value changes to the parent component.
69
+ *
70
+ * @returns An object containing:
71
+ * - `otherValue` - A computed ref for getting/setting custom values. Returns current value when using custom,
72
+ * empty string otherwise. Setting triggers the emit callback.
73
+ * - `usesOtherValue` - A computed boolean indicating whether the current value is a custom value
74
+ * (not found in the predefined items list).
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * const currentValue = ref('custom-option');
79
+ * const items = ref([
80
+ * { value: 'option1', label: 'Option 1' },
81
+ * { value: 'option2', label: 'Option 2' }
82
+ * ]);
83
+ * const emit = (value: string | null) => console.log('Value changed:', value);
84
+ *
85
+ * const { otherValue, usesOtherValue } = useCustomSelection(currentValue, items, emit);
86
+ *
87
+ * console.log(usesOtherValue.value); // true (custom-option not in items)
88
+ * console.log(otherValue.value); // 'custom-option'
89
+ *
90
+ * otherValue.value = 'new-custom-value'; // Triggers emit with 'new-custom-value'
91
+ * ```
92
+ */
23
93
  declare function useCustomSelection(currentValue: Ref<string | null>, items: Ref<any[]>, emit: (event: string | null) => void): UsableCustomSelection;
24
94
  type OtherValue = {
25
95
  key: string;
@@ -31,6 +101,46 @@ type UsableCustomSelectionMultiple = {
31
101
  addOtherValue: (value?: string, focus?: boolean) => void;
32
102
  setOtherValue: (key: string, newValue: string | null) => void;
33
103
  };
104
+ /**
105
+ * A Vue composable for managing multiple custom selection values that aren't present in a predefined list of items.
106
+ *
107
+ * This composable extends the single custom selection pattern to support multiple values. It's typically used
108
+ * in multi-select form components where users can select multiple predefined options and also add custom
109
+ * values that aren't in the predefined list. It automatically detects custom values in the current selection,
110
+ * manages their state, and provides functions for adding and updating custom values.
111
+ *
112
+ * @param currentValues - A reactive reference to the currently selected values array. Can be null if no values are selected.
113
+ * @param items - A reactive reference to the array of available predefined items. Each item should have a 'value' property.
114
+ * @param emit - A callback function to emit value changes to the parent component.
115
+ *
116
+ * @returns An object containing:
117
+ * - `otherValues` - A reactive array of custom value objects, each with a unique key, value, and optional focus state.
118
+ * - `addOtherValue` - A function to add a new custom value with optional value and focus parameters.
119
+ * - `setOtherValue` - A function to update or remove a custom value by its key, automatically syncing with currentValues.
120
+ *
121
+ * @example
122
+ * ```typescript
123
+ * const currentValues = ref(['option1', 'custom-value1', 'custom-value2']);
124
+ * const items = ref([
125
+ * { value: 'option1', label: 'Option 1' },
126
+ * { value: 'option2', label: 'Option 2' }
127
+ * ]);
128
+ * const emit = (values: string[] | null) => console.log('Values changed:', values);
129
+ *
130
+ * const { otherValues, addOtherValue, setOtherValue } = useCustomSelectionMultiple(currentValues, items, emit);
131
+ *
132
+ * console.log(otherValues.value); // [{ key: 'abc123', value: 'custom-value1' }, { key: 'def456', value: 'custom-value2' }]
133
+ *
134
+ * // Add a new custom value
135
+ * addOtherValue('new-custom-value', true);
136
+ *
137
+ * // Update an existing custom value
138
+ * setOtherValue('abc123', 'updated-custom-value');
139
+ *
140
+ * // Remove a custom value
141
+ * setOtherValue('def456', null);
142
+ * ```
143
+ */
34
144
  declare function useCustomSelectionMultiple(currentValues: Ref<string[] | null>, items: Ref<any[]>, emit: (event: string[] | null) => void): UsableCustomSelectionMultiple;
35
145
 
36
146
  declare global {
@@ -38,56 +148,239 @@ declare global {
38
148
  ResizeObserver: any;
39
149
  }
40
150
  }
151
+ /**
152
+ * A Vue composable that reactively tracks the size of a DOM element using ResizeObserver.
153
+ *
154
+ * @template T - The type of the element being observed, must extend Element
155
+ * @param target - The element to observe. Can be:
156
+ * - A direct element reference
157
+ * - A Vue ref containing an element
158
+ * - A Vue ref that might be undefined
159
+ *
160
+ * @returns An object containing reactive width and height values:
161
+ * - width: Ref<number> - The current width of the element in pixels
162
+ * - height: Ref<number> - The current height of the element in pixels
163
+ *
164
+ * @example
165
+ * ```typescript
166
+ * // With a template ref
167
+ * const elementRef = ref<HTMLDivElement>();
168
+ * const { width, height } = useElementSize(elementRef);
169
+ *
170
+ * // With a direct element
171
+ * const element = document.getElementById('my-element');
172
+ * const { width, height } = useElementSize(element);
173
+ * ```
174
+ *
175
+ * @remarks
176
+ * - The composable automatically sets up a ResizeObserver when the component mounts
177
+ * - The observer is automatically disconnected when the component unmounts
178
+ * - Initial values are 0 until the first resize event
179
+ * - Handles cases where the target element might be undefined
180
+ */
41
181
  declare function useElementSize<T extends Element>(target: T | Ref<T> | Ref<undefined>): {
42
182
  width: Ref<number>;
43
183
  height: Ref<number>;
44
184
  };
45
185
 
186
+ /**
187
+ * A Vue composable that filters and groups fields based on multiple filter criteria.
188
+ *
189
+ * @template T - The type of filter names as string literals
190
+ * @param fields - A Vue ref containing an array of Field objects to be filtered
191
+ * @param filters - An object where keys are filter names and values are predicate functions
192
+ * that return true if a field should be included in that group
193
+ *
194
+ * @returns An object containing:
195
+ * - fieldGroups: ComputedRef<Record<Extract<T, string>, Field[]>> - A reactive object
196
+ * where each key corresponds to a filter name and the value is an array of fields
197
+ * that pass that filter
198
+ *
199
+ * @example
200
+ * ```typescript
201
+ * // Define filter criteria
202
+ * const fieldFilters = {
203
+ * required: (field: Field) => field.required === true,
204
+ * optional: (field: Field) => field.required !== true,
205
+ * text: (field: Field) => field.type === 'string',
206
+ * numeric: (field: Field) => ['integer', 'float', 'decimal'].includes(field.type)
207
+ * };
208
+ *
209
+ * const fieldsRef = ref<Field[]>([
210
+ * { name: 'id', type: 'integer', required: true },
211
+ * { name: 'title', type: 'string', required: true },
212
+ * { name: 'description', type: 'text', required: false },
213
+ * { name: 'price', type: 'decimal', required: false }
214
+ * ]);
215
+ *
216
+ * const { fieldGroups } = useFilterFields(fieldsRef, fieldFilters);
217
+ *
218
+ * // Access filtered groups
219
+ * console.log(fieldGroups.value.required); // [id, title]
220
+ * console.log(fieldGroups.value.text); // [title]
221
+ * console.log(fieldGroups.value.numeric); // [id, price]
222
+ * ```
223
+ *
224
+ * @remarks
225
+ * - Fields can appear in multiple groups if they pass multiple filters
226
+ * - If a field doesn't pass any filter, it won't appear in any group
227
+ * - The result is reactive and will update when the input fields change
228
+ * - Filter functions are called for each field against each filter criterion
229
+ * - Groups are initialized as empty arrays even if no fields match the criteria
230
+ */
46
231
  declare function useFilterFields<T extends string>(fields: Ref<Field[]>, filters: Record<T, (field: Field) => boolean>): {
47
232
  fieldGroups: ComputedRef<Record<Extract<T, string>, Field[]>>;
48
233
  };
49
234
 
235
+ /**
236
+ * Represents a groupable item instance with its active state and value.
237
+ */
50
238
  type GroupableInstance = {
239
+ /** Reactive reference to the active state of the item */
51
240
  active: Ref<boolean>;
241
+ /** Unique identifier for the item within the group */
52
242
  value: string | number | undefined;
53
243
  };
54
244
  /**
245
+ * Configuration options for the useGroupable composable.
55
246
  * Used to make child item part of the group context. Needs to be used in a component that is a child
56
- * of a component that has the `useGroupableParent` composition enabled
247
+ * of a component that has the `useGroupableParent` composition enabled.
57
248
  */
58
249
  type GroupableOptions = {
250
+ /** Unique identifier for this groupable item */
59
251
  value?: string | number;
252
+ /** Name of the group to inject from (defaults to 'item-group') */
60
253
  group?: string;
254
+ /** External reactive reference to control the active state */
61
255
  active?: Ref<boolean>;
256
+ /** Whether to watch the external active reference for changes */
62
257
  watch?: boolean;
63
258
  };
259
+ /**
260
+ * Return type for the useGroupable composable.
261
+ */
64
262
  type UsableGroupable = {
263
+ /** Reactive reference to the active state */
65
264
  active: Ref<boolean>;
265
+ /** Toggle the active state of this item */
66
266
  toggle: () => void;
267
+ /** Activate this item (if not already active) */
67
268
  activate: () => void;
269
+ /** Deactivate this item (if currently active) */
68
270
  deactivate: () => void;
69
271
  };
272
+ /**
273
+ * Vue composable for creating groupable child items that can participate in group selection.
274
+ *
275
+ * This composable enables a component to be part of a group context managed by a parent component
276
+ * using `useGroupableParent`. It provides reactive active state management and selection control.
277
+ *
278
+ * @param options - Configuration options for the groupable item
279
+ * @param options.value - Unique identifier for this item within the group
280
+ * @param options.group - Name of the group to inject from (defaults to 'item-group')
281
+ * @param options.active - External reactive reference to control the active state
282
+ * @param options.watch - Whether to watch the external active reference for changes
283
+ *
284
+ * @returns Object containing active state and control methods
285
+ *
286
+ * @example
287
+ * ```vue
288
+ * <script setup>
289
+ * import { useGroupable } from '@directus/composables';
290
+ *
291
+ * const props = defineProps(['value', 'active']);
292
+ *
293
+ * const { active, toggle, activate, deactivate } = useGroupable({
294
+ * value: props.value,
295
+ * active: toRef(props, 'active'),
296
+ * watch: true
297
+ * });
298
+ * </script>
299
+ * ```
300
+ */
70
301
  declare function useGroupable(options?: GroupableOptions): UsableGroupable;
302
+ /**
303
+ * State configuration for the groupable parent.
304
+ */
71
305
  type GroupableParentState = {
306
+ /** External selection state (can be readonly) */
72
307
  selection?: Ref<(string | number)[] | undefined> | Ref<readonly (string | number)[] | undefined>;
308
+ /** Callback fired when the selection changes */
73
309
  onSelectionChange?: (newSelectionValues: readonly (string | number)[]) => void;
310
+ /** Callback fired when an item is toggled */
74
311
  onToggle?: (item: GroupableInstance) => void;
75
312
  };
313
+ /**
314
+ * Configuration options for the groupable parent.
315
+ */
76
316
  type GroupableParentOptions = {
317
+ /** Whether at least one item must always be selected */
77
318
  mandatory?: Ref<boolean>;
319
+ /** Maximum number of items that can be selected (-1 for unlimited) */
78
320
  max?: Ref<number>;
321
+ /** Whether multiple items can be selected simultaneously */
79
322
  multiple?: Ref<boolean>;
80
323
  };
324
+ /**
325
+ * Return type for the useGroupableParent composable.
326
+ */
81
327
  type UsableGroupableParent = {
328
+ /** Reactive array of all registered groupable items */
82
329
  items: Ref<GroupableInstance[]>;
330
+ /** Computed selection array (readonly) */
83
331
  selection: Ref<readonly (string | number)[]>;
332
+ /** Internal selection state */
84
333
  internalSelection: Ref<(string | number)[]>;
334
+ /** Get the value identifier for a given item */
85
335
  getValueForItem: (item: GroupableInstance) => string | number;
336
+ /** Synchronize children's active state with selection */
86
337
  updateChildren: () => void;
87
338
  };
88
339
  /**
89
- * Used to make a component a group parent component. Provides the registration / toggle functions
90
- * to its group children
340
+ * Vue composable for creating a group parent that manages multiple groupable child items.
341
+ *
342
+ * This composable provides the foundation for components that need to manage a collection
343
+ * of selectable items, such as tabs, radio groups, or multi-select lists. It handles
344
+ * registration of child items, selection state management, and provides various selection
345
+ * constraints (mandatory, maximum, multiple).
346
+ *
347
+ * @param state - External state configuration for selection management
348
+ * @param state.selection - External selection state reference
349
+ * @param state.onSelectionChange - Callback fired when selection changes
350
+ * @param state.onToggle - Callback fired when an item is toggled
351
+ * @param options - Configuration options for selection behavior
352
+ * @param options.mandatory - Whether at least one item must always be selected
353
+ * @param options.max - Maximum number of items that can be selected (-1 for unlimited)
354
+ * @param options.multiple - Whether multiple items can be selected simultaneously
355
+ * @param group - Injection key for the group (defaults to 'item-group')
356
+ *
357
+ * @returns Object containing items array, selection state, and utility functions
358
+ *
359
+ * @example
360
+ * ```vue
361
+ * <script setup>
362
+ * import { useGroupableParent } from '@directus/composables';
363
+ * import { ref } from 'vue';
364
+ *
365
+ * const selectedItems = ref([]);
366
+ * const isMultiple = ref(true);
367
+ * const isMandatory = ref(false);
368
+ *
369
+ * const { items, selection } = useGroupableParent(
370
+ * {
371
+ * selection: selectedItems,
372
+ * onSelectionChange: (values) => {
373
+ * console.log('Selection changed:', values);
374
+ * }
375
+ * },
376
+ * {
377
+ * multiple: isMultiple,
378
+ * mandatory: isMandatory,
379
+ * max: ref(3)
380
+ * }
381
+ * );
382
+ * </script>
383
+ * ```
91
384
  */
92
385
  declare function useGroupableParent(state?: GroupableParentState, options?: GroupableParentOptions, group?: string): UsableGroupableParent;
93
386
 
@@ -121,10 +414,127 @@ type ComputedQuery = {
121
414
  };
122
415
  declare function useItems(collection: Ref<string | null>, query: ComputedQuery): UsableItems;
123
416
 
417
+ declare const WRITABLE_PROPS: readonly ["selection", "layoutOptions", "layoutQuery"];
418
+ type WritableProp = (typeof WRITABLE_PROPS)[number];
419
+ /**
420
+ * Type guard to check if a property is writable (can be updated via emit).
421
+ *
422
+ * This function determines whether a given property name corresponds to one of the
423
+ * writable properties that can be updated through Vue's emit system.
424
+ *
425
+ * @param prop - The property name to check
426
+ * @returns True if the property is writable, false otherwise
427
+ *
428
+ * @example
429
+ * ```typescript
430
+ * if (isWritableProp('selection')) {
431
+ * // Property is writable, can emit update
432
+ * emit('update:selection', newValue);
433
+ * }
434
+ * ```
435
+ */
436
+ declare function isWritableProp(prop: string): prop is WritableProp;
437
+ /**
438
+ * Creates a Vue component wrapper for a layout configuration.
439
+ *
440
+ * This function creates a dynamic Vue component that wraps a layout with standardized
441
+ * props, emits, and state management. It handles reactive state updates, prop validation,
442
+ * and provides a consistent interface for all layout components.
443
+ *
444
+ * @template Options - The type for layout-specific options
445
+ * @template Query - The type for layout-specific query parameters
446
+ * @param layout - The layout configuration object containing id and setup function
447
+ * @returns A Vue component that can be used to render the layout
448
+ *
449
+ * @example
450
+ * ```typescript
451
+ * interface MyLayoutOptions {
452
+ * itemSize: number;
453
+ * showHeaders: boolean;
454
+ * }
455
+ *
456
+ * interface MyLayoutQuery {
457
+ * page: number;
458
+ * limit: number;
459
+ * }
460
+ *
461
+ * const layoutConfig: LayoutConfig = {
462
+ * id: 'my-layout',
463
+ * setup: (props, { emit }) => ({
464
+ * // Layout-specific setup logic
465
+ * })
466
+ * };
467
+ *
468
+ * const LayoutWrapper = createLayoutWrapper<MyLayoutOptions, MyLayoutQuery>(layoutConfig);
469
+ * ```
470
+ */
471
+ declare function createLayoutWrapper<Options, Query>(layout: LayoutConfig): Component;
472
+ /**
473
+ * Composable for managing layout components in Directus.
474
+ *
475
+ * This composable provides access to layout components and handles the dynamic
476
+ * selection of layout wrappers based on the provided layout ID. It automatically
477
+ * falls back to the tabular layout if the requested layout is not found.
478
+ *
479
+ * @template Options - The type for layout-specific options (default: any)
480
+ * @template Query - The type for layout-specific query parameters (default: any)
481
+ * @param layoutId - A reactive reference to the layout ID
482
+ * @returns An object containing the layout wrapper component
483
+ *
484
+ * @example
485
+ * ```typescript
486
+ * import { ref } from 'vue';
487
+ * import { useLayout } from './use-layout';
488
+ *
489
+ * const selectedLayoutId = ref('table');
490
+ * const { layoutWrapper } = useLayout(selectedLayoutId);
491
+ *
492
+ * // Use the layout wrapper in your template
493
+ * // <component :is="layoutWrapper" :collection="'users'" />
494
+ * ```
495
+ *
496
+ * @example
497
+ * ```typescript
498
+ * // With typed options and query
499
+ * interface TableOptions {
500
+ * spacing: 'cozy' | 'comfortable' | 'compact';
501
+ * showHeaders: boolean;
502
+ * }
503
+ *
504
+ * interface TableQuery {
505
+ * sort: string[];
506
+ * limit: number;
507
+ * }
508
+ *
509
+ * const layoutId = ref<string | null>('table');
510
+ * const { layoutWrapper } = useLayout<TableOptions, TableQuery>(layoutId);
511
+ * ```
512
+ */
124
513
  declare function useLayout<Options = any, Query = any>(layoutId: Ref<string | null>): {
125
514
  layoutWrapper: ComputedRef<Component>;
126
515
  };
127
516
 
517
+ /**
518
+ * Vue props definition for size-related boolean properties.
519
+ *
520
+ * This object defines the standard size props that can be used in Vue components
521
+ * to control size-based styling through CSS classes.
522
+ *
523
+ * @example
524
+ * ```typescript
525
+ * // In a Vue component
526
+ * export default defineComponent({
527
+ * props: {
528
+ * ...sizeProps,
529
+ * // other props
530
+ * },
531
+ * setup(props) {
532
+ * const sizeClass = useSizeClass(props);
533
+ * return { sizeClass };
534
+ * }
535
+ * });
536
+ * ```
537
+ */
128
538
  declare const sizeProps: {
129
539
  xSmall: {
130
540
  type: BooleanConstructor;
@@ -149,13 +559,500 @@ interface SizeProps {
149
559
  large?: boolean;
150
560
  xLarge?: boolean;
151
561
  }
562
+ /**
563
+ * Composable for generating CSS size class names based on size props.
564
+ *
565
+ * This composable takes props containing size boolean flags and returns a computed
566
+ * CSS class name string. It follows a priority order: xSmall > small > large > xLarge.
567
+ * If no size props are true, it returns null.
568
+ *
569
+ * @template T - The type of additional props that extend SizeProps
570
+ * @param props - The props object containing size boolean properties
571
+ * @returns A computed ref that resolves to the appropriate CSS class name or null
572
+ *
573
+ * @example
574
+ * ```typescript
575
+ * // Basic usage in a Vue component
576
+ * const props = { small: true, large: false };
577
+ * const sizeClass = useSizeClass(props);
578
+ * console.log(sizeClass.value); // 'small'
579
+ * ```
580
+ *
581
+ * @example
582
+ * ```typescript
583
+ * // Usage with additional props
584
+ * interface MyProps {
585
+ * color: string;
586
+ * disabled: boolean;
587
+ * }
588
+ *
589
+ * const props: MyProps & SizeProps = {
590
+ * color: 'blue',
591
+ * disabled: false,
592
+ * xLarge: true
593
+ * };
594
+ *
595
+ * const sizeClass = useSizeClass(props);
596
+ * console.log(sizeClass.value); // 'x-large'
597
+ * ```
598
+ *
599
+ * @example
600
+ * ```typescript
601
+ * // In a Vue component with reactive props
602
+ * export default defineComponent({
603
+ * props: {
604
+ * ...sizeProps,
605
+ * label: String,
606
+ * },
607
+ * setup(props) {
608
+ * const sizeClass = useSizeClass(props);
609
+ *
610
+ * return { sizeClass };
611
+ * },
612
+ * template: `
613
+ * <button :class="['btn', sizeClass]">
614
+ * {{ label }}
615
+ * </button>
616
+ * `
617
+ * });
618
+ * ```
619
+ */
152
620
  declare function useSizeClass<T>(props: T & SizeProps): ComputedRef<string | null>;
153
621
 
622
+ /**
623
+ * Composable for creating two-way binding between parent and child components.
624
+ *
625
+ * @deprecated Use Vue's native `defineModel()` instead. This composable is kept for backward compatibility.
626
+ * Vue 3.4+ provides `defineModel()` which offers a more streamlined and performant way to create v-model bindings.
627
+ *
628
+ * @see {@link https://vuejs.org/api/sfc-script-setup.html#definemodel} Vue's defineModel documentation
629
+ *
630
+ * This composable creates a computed ref that synchronizes a prop value with
631
+ * its parent component through Vue's v-model pattern. It provides a getter
632
+ * that returns the current prop value and a setter that emits an update event
633
+ * to notify the parent component of changes.
634
+ *
635
+ * This is particularly useful for creating custom form components that need
636
+ * to work with v-model while maintaining proper data flow patterns.
637
+ *
638
+ * @template T - The type of the props object
639
+ * @template K - The key of the prop to sync (must be a string key of T)
640
+ * @template E - The emit function type with proper event typing
641
+ *
642
+ * @param props - The component props object containing the value to sync
643
+ * @param key - The specific prop key to create a two-way binding for
644
+ * @param emit - The Vue emit function for sending update events to parent
645
+ *
646
+ * @returns A computed ref that can be used with v-model pattern
647
+ *
648
+ * @example
649
+ * ```typescript
650
+ * // DEPRECATED: Old way using useSync
651
+ * export default defineComponent({
652
+ * props: {
653
+ * modelValue: String,
654
+ * disabled: Boolean,
655
+ * },
656
+ * emits: ['update:modelValue'],
657
+ * setup(props, { emit }) {
658
+ * const syncedValue = useSync(props, 'modelValue', emit);
659
+ * return { syncedValue };
660
+ * }
661
+ * });
662
+ *
663
+ * // RECOMMENDED: New way using defineModel (Vue 3.4+)
664
+ * <script setup lang="ts">
665
+ * const modelValue = defineModel<string>();
666
+ * const disabled = defineProps<{ disabled?: boolean }>();
667
+ * </script>
668
+ *
669
+ * <template>
670
+ * <input v-model="modelValue" :disabled="disabled" />
671
+ * </template>
672
+ * ```
673
+ *
674
+ * @example
675
+ * ```typescript
676
+ * // DEPRECATED: Custom input component with useSync
677
+ * interface Props {
678
+ * value: string;
679
+ * placeholder?: string;
680
+ * type?: string;
681
+ * }
682
+ *
683
+ * export default defineComponent({
684
+ * props: {
685
+ * value: { type: String, required: true },
686
+ * placeholder: String,
687
+ * type: { type: String, default: 'text' },
688
+ * },
689
+ * emits: ['update:value'],
690
+ * setup(props: Props, { emit }) {
691
+ * const syncedValue = useSync(props, 'value', emit);
692
+ * return { syncedValue };
693
+ * }
694
+ * });
695
+ *
696
+ * // RECOMMENDED: Using defineModel with custom prop name
697
+ * <script setup lang="ts">
698
+ * const value = defineModel<string>('value', { required: true });
699
+ * const { placeholder, type = 'text' } = defineProps<{
700
+ * placeholder?: string;
701
+ * type?: string;
702
+ * }>();
703
+ * </script>
704
+ * ```
705
+ *
706
+ * @example
707
+ * ```typescript
708
+ * // DEPRECATED: Usage with complex objects using useSync
709
+ * interface UserData {
710
+ * name: string;
711
+ * email: string;
712
+ * age: number;
713
+ * }
714
+ *
715
+ * export default defineComponent({
716
+ * props: {
717
+ * userData: { type: Object as PropType<UserData>, required: true },
718
+ * isLoading: Boolean,
719
+ * },
720
+ * emits: ['update:userData'],
721
+ * setup(props, { emit }) {
722
+ * const syncedUserData = useSync(props, 'userData', emit);
723
+ *
724
+ * const updateName = (newName: string) => {
725
+ * syncedUserData.value = {
726
+ * ...syncedUserData.value,
727
+ * name: newName
728
+ * };
729
+ * };
730
+ *
731
+ * return { syncedUserData, updateName };
732
+ * }
733
+ * });
734
+ *
735
+ * // RECOMMENDED: Using defineModel with complex objects
736
+ * <script setup lang="ts">
737
+ * interface UserData {
738
+ * name: string;
739
+ * email: string;
740
+ * age: number;
741
+ * }
742
+ *
743
+ * const userData = defineModel<UserData>('userData', { required: true });
744
+ * const { isLoading } = defineProps<{ isLoading?: boolean }>();
745
+ *
746
+ * const updateName = (newName: string) => {
747
+ * userData.value = {
748
+ * ...userData.value,
749
+ * name: newName
750
+ * };
751
+ * };
752
+ * </script>
753
+ * ```
754
+ */
154
755
  declare function useSync<T, K extends keyof T & string, E extends (event: `update:${K}`, ...args: any[]) => void>(props: T, key: K, emit: E): Ref<T[K]>;
155
756
 
757
+ /**
758
+ * Vue composable that provides access to the global Directus stores through dependency injection.
759
+ *
760
+ * This composable injects the stores object that contains all the Pinia stores used throughout
761
+ * the Directus application, including user store, permissions store, collections store, etc.
762
+ *
763
+ * @returns The injected stores object containing all application stores
764
+ * @throws Error if the stores could not be found in the injection context
765
+ *
766
+ * @example
767
+ * ```typescript
768
+ * import { useStores } from '@directus/composables';
769
+ *
770
+ * export default defineComponent({
771
+ * setup() {
772
+ * const stores = useStores();
773
+ *
774
+ * // Access specific stores
775
+ * const userStore = stores.useUserStore();
776
+ * const collectionsStore = stores.useCollectionsStore();
777
+ * const permissionsStore = stores.usePermissionsStore();
778
+ *
779
+ * return {
780
+ * userInfo: userStore.currentUser,
781
+ * collections: collectionsStore.collections,
782
+ * permissions: permissionsStore.permissions
783
+ * };
784
+ * }
785
+ * });
786
+ * ```
787
+ *
788
+ * @example
789
+ * ```typescript
790
+ * // Using in a component with reactive store data
791
+ * import { useStores } from '@directus/composables';
792
+ * import { computed } from 'vue';
793
+ *
794
+ * export default defineComponent({
795
+ * setup() {
796
+ * const stores = useStores();
797
+ * const userStore = stores.useUserStore();
798
+ *
799
+ * const isAdmin = computed(() => {
800
+ * return userStore.currentUser?.role?.admin_access === true;
801
+ * });
802
+ *
803
+ * const hasCreatePermission = computed(() => {
804
+ * const permissionsStore = stores.usePermissionsStore();
805
+ * return permissionsStore.hasPermission('directus_files', 'create');
806
+ * });
807
+ *
808
+ * return { isAdmin, hasCreatePermission };
809
+ * }
810
+ * });
811
+ * ```
812
+ */
156
813
  declare function useStores(): Record<string, any>;
814
+ /**
815
+ * Vue composable that provides access to the Axios HTTP client instance through dependency injection.
816
+ *
817
+ * This composable injects the configured Axios instance that is set up with the proper base URL,
818
+ * authentication headers, interceptors, and other configuration needed to communicate with the
819
+ * Directus API. It provides a convenient way to make HTTP requests from components and composables.
820
+ *
821
+ * @returns The injected Axios instance configured for Directus API communication
822
+ * @throws Error if the API instance could not be found in the injection context
823
+ *
824
+ * @example
825
+ * ```typescript
826
+ * import { useApi } from '@directus/composables';
827
+ *
828
+ * export default defineComponent({
829
+ * setup() {
830
+ * const api = useApi();
831
+ *
832
+ * const fetchUserData = async (userId: string) => {
833
+ * try {
834
+ * const response = await api.get(`/users/${userId}`);
835
+ * return response.data;
836
+ * } catch (error) {
837
+ * console.error('Failed to fetch user data:', error);
838
+ * throw error;
839
+ * }
840
+ * };
841
+ *
842
+ * return { fetchUserData };
843
+ * }
844
+ * });
845
+ * ```
846
+ *
847
+ * @example
848
+ * ```typescript
849
+ * // Using with reactive data and error handling
850
+ * import { useApi } from '@directus/composables';
851
+ * import { ref, onMounted } from 'vue';
852
+ *
853
+ * export default defineComponent({
854
+ * setup() {
855
+ * const api = useApi();
856
+ * const collections = ref([]);
857
+ * const loading = ref(false);
858
+ * const error = ref(null);
859
+ *
860
+ * const loadCollections = async () => {
861
+ * loading.value = true;
862
+ * error.value = null;
863
+ *
864
+ * try {
865
+ * const response = await api.get('/collections');
866
+ * collections.value = response.data.data;
867
+ * } catch (err) {
868
+ * error.value = err.response?.data?.errors?.[0]?.message || 'Failed to load collections';
869
+ * } finally {
870
+ * loading.value = false;
871
+ * }
872
+ * };
873
+ *
874
+ * onMounted(loadCollections);
875
+ *
876
+ * return { collections, loading, error, loadCollections };
877
+ * }
878
+ * });
879
+ * ```
880
+ */
157
881
  declare function useApi(): AxiosInstance;
882
+ /**
883
+ * Vue composable that provides access to the Directus SDK client instance through dependency injection.
884
+ *
885
+ * This composable injects the configured Directus SDK client that provides a type-safe, modern API
886
+ * for interacting with Directus. The SDK offers methods for CRUD operations, authentication, file
887
+ * management, and more, with full TypeScript support and automatic type inference based on your schema.
888
+ *
889
+ * @template Schema - The TypeScript schema type for your Directus instance, defaults to `any`
890
+ * @returns The injected Directus SDK client with REST client capabilities
891
+ * @throws Error if the SDK instance could not be found in the injection context
892
+ *
893
+ * @example
894
+ * ```typescript
895
+ * import { useSdk } from '@directus/composables';
896
+ *
897
+ * // Using with default schema
898
+ * export default defineComponent({
899
+ * setup() {
900
+ * const sdk = useSdk();
901
+ *
902
+ * const fetchArticles = async () => {
903
+ * try {
904
+ * const articles = await sdk.items('articles').readByQuery({
905
+ * filter: { status: { _eq: 'published' } },
906
+ * sort: ['-date_created'],
907
+ * limit: 10
908
+ * });
909
+ * return articles;
910
+ * } catch (error) {
911
+ * console.error('Failed to fetch articles:', error);
912
+ * throw error;
913
+ * }
914
+ * };
915
+ *
916
+ * return { fetchArticles };
917
+ * }
918
+ * });
919
+ * ```
920
+ *
921
+ * @example
922
+ * ```typescript
923
+ * // Using with typed schema for better type safety
924
+ * import { useSdk } from '@directus/composables';
925
+ *
926
+ * interface MySchema {
927
+ * articles: {
928
+ * id: string;
929
+ * title: string;
930
+ * content: string;
931
+ * status: 'draft' | 'published';
932
+ * author: string;
933
+ * date_created: string;
934
+ * };
935
+ * authors: {
936
+ * id: string;
937
+ * name: string;
938
+ * email: string;
939
+ * };
940
+ * }
941
+ *
942
+ * export default defineComponent({
943
+ * setup() {
944
+ * const sdk = useSdk<MySchema>();
945
+ *
946
+ * const createArticle = async (articleData: Partial<MySchema['articles']>) => {
947
+ * try {
948
+ * const newArticle = await sdk.items('articles').createOne(articleData);
949
+ * return newArticle; // Fully typed return value
950
+ * } catch (error) {
951
+ * console.error('Failed to create article:', error);
952
+ * throw error;
953
+ * }
954
+ * };
955
+ *
956
+ * const updateArticle = async (id: string, updates: Partial<MySchema['articles']>) => {
957
+ * try {
958
+ * const updatedArticle = await sdk.items('articles').updateOne(id, updates);
959
+ * return updatedArticle; // Type-safe updates
960
+ * } catch (error) {
961
+ * console.error('Failed to update article:', error);
962
+ * throw error;
963
+ * }
964
+ * };
965
+ *
966
+ * return { createArticle, updateArticle };
967
+ * }
968
+ * });
969
+ * ```
970
+ */
158
971
  declare function useSdk<Schema extends object = any>(): DirectusClient<Schema> & RestClient<Schema>;
972
+ /**
973
+ * Vue composable that provides access to the registered Directus extensions through dependency injection.
974
+ *
975
+ * This composable injects the extensions configuration object that contains all registered app
976
+ * extensions including interfaces, displays, layouts, modules, panels, operations, and more.
977
+ * The extensions are provided as reactive references and can be used to dynamically access
978
+ * and utilize custom functionality within the Directus application.
979
+ *
980
+ * @returns A reactive record of extension configurations organized by extension type
981
+ * @throws Error if the extensions could not be found in the injection context
982
+ *
983
+ * @example
984
+ * ```typescript
985
+ * import { useExtensions } from '@directus/composables';
986
+ *
987
+ * export default defineComponent({
988
+ * setup() {
989
+ * const extensions = useExtensions();
990
+ *
991
+ * const getAvailableInterfaces = () => {
992
+ * return Object.values(extensions.interfaces || {});
993
+ * };
994
+ *
995
+ * const getAvailableDisplays = () => {
996
+ * return Object.values(extensions.displays || {});
997
+ * };
998
+ *
999
+ * const findInterfaceByName = (name: string) => {
1000
+ * return extensions.interfaces?.[name] || null;
1001
+ * };
1002
+ *
1003
+ * return {
1004
+ * getAvailableInterfaces,
1005
+ * getAvailableDisplays,
1006
+ * findInterfaceByName
1007
+ * };
1008
+ * }
1009
+ * });
1010
+ * ```
1011
+ *
1012
+ * @example
1013
+ * ```typescript
1014
+ * // Using with computed properties for reactive extension lists
1015
+ * import { useExtensions } from '@directus/composables';
1016
+ * import { computed } from 'vue';
1017
+ *
1018
+ * export default defineComponent({
1019
+ * setup() {
1020
+ * const extensions = useExtensions();
1021
+ *
1022
+ * const availableLayouts = computed(() => {
1023
+ * return Object.entries(extensions.layouts || {}).map(([key, config]) => ({
1024
+ * id: key,
1025
+ * name: config.name,
1026
+ * icon: config.icon,
1027
+ * component: config.component
1028
+ * }));
1029
+ * });
1030
+ *
1031
+ * const customModules = computed(() => {
1032
+ * return Object.values(extensions.modules || {}).filter(module =>
1033
+ * !module.preRegisterCheck || module.preRegisterCheck()
1034
+ * );
1035
+ * });
1036
+ *
1037
+ * const operationsByGroup = computed(() => {
1038
+ * const operations = Object.values(extensions.operations || {});
1039
+ * return operations.reduce((groups, operation) => {
1040
+ * const group = operation.overview?.group || 'other';
1041
+ * if (!groups[group]) groups[group] = [];
1042
+ * groups[group].push(operation);
1043
+ * return groups;
1044
+ * }, {} as Record<string, any[]>);
1045
+ * });
1046
+ *
1047
+ * return {
1048
+ * availableLayouts,
1049
+ * customModules,
1050
+ * operationsByGroup
1051
+ * };
1052
+ * }
1053
+ * });
1054
+ * ```
1055
+ */
159
1056
  declare function useExtensions(): RefRecord<AppExtensionConfigs>;
160
1057
 
161
- export { type ComputedQuery, type GroupableInstance, type GroupableOptions, type ManualSortData, type OtherValue, type UsableCollection, type UsableCustomSelection, type UsableGroupable, type UsableItems, sizeProps, useApi, useCollection, useCustomSelection, useCustomSelectionMultiple, useElementSize, useExtensions, useFilterFields, useGroupable, useGroupableParent, useItems, useLayout, useSdk, useSizeClass, useStores, useSync };
1058
+ export { type ComputedQuery, type GroupableInstance, type GroupableOptions, type ManualSortData, type OtherValue, type UsableCollection, type UsableCustomSelection, type UsableGroupable, type UsableItems, createLayoutWrapper, isWritableProp, sizeProps, useApi, useCollection, useCustomSelection, useCustomSelectionMultiple, useElementSize, useExtensions, useFilterFields, useGroupable, useGroupableParent, useItems, useLayout, useSdk, useSizeClass, useStores, useSync };
package/dist/index.js CHANGED
@@ -61,9 +61,7 @@ function useCollection(collectionKey) {
61
61
  return info.value?.meta?.singleton === true;
62
62
  });
63
63
  const accountabilityScope = computed(() => {
64
- if (!info.value) return null;
65
- if (!info.value.meta) return null;
66
- return info.value.meta.accountability;
64
+ return info.value?.meta?.accountability || null;
67
65
  });
68
66
  return { info, fields, defaults, primaryKeyField, userCreatedField, sortField, isSingleton, accountabilityScope };
69
67
  }
@@ -684,6 +682,8 @@ function useSync(props, key, emit) {
684
682
  });
685
683
  }
686
684
  export {
685
+ createLayoutWrapper,
686
+ isWritableProp,
687
687
  sizeProps,
688
688
  useApi,
689
689
  useCollection,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directus/composables",
3
- "version": "11.2.0",
3
+ "version": "11.2.1",
4
4
  "description": "Shared Vue composables for Directus use",
5
5
  "homepage": "https://directus.io",
6
6
  "repository": {
@@ -22,23 +22,23 @@
22
22
  "dist"
23
23
  ],
24
24
  "dependencies": {
25
- "axios": "1.8.4",
25
+ "axios": "1.11.0",
26
26
  "lodash-es": "4.17.21",
27
- "nanoid": "5.1.2",
27
+ "nanoid": "5.1.5",
28
28
  "@directus/constants": "13.0.1",
29
- "@directus/utils": "13.0.7"
29
+ "@directus/utils": "13.0.8"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@directus/tsconfig": "3.0.0",
33
33
  "@types/lodash-es": "4.17.12",
34
- "@vitest/coverage-v8": "2.1.9",
34
+ "@vitest/coverage-v8": "3.2.4",
35
35
  "@vue/test-utils": "2.4.6",
36
- "tsup": "8.4.0",
37
- "typescript": "5.8.2",
38
- "vitest": "2.1.9",
39
- "vue": "3.5.13",
40
- "@directus/extensions": "3.0.7",
41
- "@directus/sdk": "20.0.0",
36
+ "tsup": "8.5.0",
37
+ "typescript": "5.8.3",
38
+ "vitest": "3.2.4",
39
+ "vue": "3.5.18",
40
+ "@directus/extensions": "3.0.8",
41
+ "@directus/sdk": "20.0.1",
42
42
  "@directus/types": "13.2.0"
43
43
  },
44
44
  "peerDependencies": {
@@ -47,6 +47,7 @@
47
47
  "scripts": {
48
48
  "build": "tsup src/index.ts --format=esm --dts",
49
49
  "dev": "tsup src/index.ts --format=esm --dts --watch",
50
- "test": "vitest --watch=false"
50
+ "test": "vitest run",
51
+ "test:coverage": "vitest run --coverage"
51
52
  }
52
53
  }