@directus/composables 9.25.0
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.js +10 -0
- package/dist/use-collection.js +48 -0
- package/dist/use-custom-selection.js +84 -0
- package/dist/use-element-size.js +22 -0
- package/dist/use-filter-fields.js +18 -0
- package/dist/use-groupable.js +183 -0
- package/dist/use-items.js +231 -0
- package/dist/use-items.test.js +175 -0
- package/dist/use-layout.js +96 -0
- package/dist/use-size-class.js +33 -0
- package/dist/use-sync.js +11 -0
- package/dist/use-system.js +20 -0
- package/package.json +48 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './use-collection.js';
|
|
2
|
+
export * from './use-custom-selection.js';
|
|
3
|
+
export * from './use-element-size.js';
|
|
4
|
+
export * from './use-filter-fields.js';
|
|
5
|
+
export * from './use-groupable.js';
|
|
6
|
+
export * from './use-items.js';
|
|
7
|
+
export * from './use-layout.js';
|
|
8
|
+
export * from './use-size-class.js';
|
|
9
|
+
export * from './use-sync.js';
|
|
10
|
+
export * from './use-system.js';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { computed, ref } from 'vue';
|
|
2
|
+
import { useStores } from './use-system.js';
|
|
3
|
+
export function useCollection(collectionKey) {
|
|
4
|
+
const { useCollectionsStore, useFieldsStore } = useStores();
|
|
5
|
+
const collectionsStore = useCollectionsStore();
|
|
6
|
+
const fieldsStore = useFieldsStore();
|
|
7
|
+
const collection = typeof collectionKey === 'string' ? ref(collectionKey) : collectionKey;
|
|
8
|
+
const info = computed(() => {
|
|
9
|
+
return (collectionsStore.collections.find(({ collection: key }) => key === collection.value) || null);
|
|
10
|
+
});
|
|
11
|
+
const fields = computed(() => {
|
|
12
|
+
if (!collection.value)
|
|
13
|
+
return [];
|
|
14
|
+
return fieldsStore.getFieldsForCollectionSorted(collection.value);
|
|
15
|
+
});
|
|
16
|
+
const defaults = computed(() => {
|
|
17
|
+
if (!fields.value)
|
|
18
|
+
return {};
|
|
19
|
+
const defaults = {};
|
|
20
|
+
for (const field of fields.value) {
|
|
21
|
+
if (field.schema !== null && 'default_value' in field.schema) {
|
|
22
|
+
defaults[field.field] = field.schema.default_value;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return defaults;
|
|
26
|
+
});
|
|
27
|
+
const primaryKeyField = computed(() => {
|
|
28
|
+
return (fields.value.find((field) => field.collection === collection.value && field.schema?.is_primary_key === true) ||
|
|
29
|
+
null);
|
|
30
|
+
});
|
|
31
|
+
const userCreatedField = computed(() => {
|
|
32
|
+
return fields.value?.find((field) => (field.meta?.special || []).includes('user_created')) || null;
|
|
33
|
+
});
|
|
34
|
+
const sortField = computed(() => {
|
|
35
|
+
return info.value?.meta?.sort_field || null;
|
|
36
|
+
});
|
|
37
|
+
const isSingleton = computed(() => {
|
|
38
|
+
return info.value?.meta?.singleton === true;
|
|
39
|
+
});
|
|
40
|
+
const accountabilityScope = computed(() => {
|
|
41
|
+
if (!info.value)
|
|
42
|
+
return null;
|
|
43
|
+
if (!info.value.meta)
|
|
44
|
+
return null;
|
|
45
|
+
return info.value.meta.accountability;
|
|
46
|
+
});
|
|
47
|
+
return { info, fields, defaults, primaryKeyField, userCreatedField, sortField, isSingleton, accountabilityScope };
|
|
48
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid';
|
|
2
|
+
import { computed, ref, watch } from 'vue';
|
|
3
|
+
export function useCustomSelection(currentValue, items, emit) {
|
|
4
|
+
const localOtherValue = ref('');
|
|
5
|
+
const otherValue = computed({
|
|
6
|
+
get() {
|
|
7
|
+
return localOtherValue.value || (usesOtherValue.value ? currentValue.value : '');
|
|
8
|
+
},
|
|
9
|
+
set(newValue) {
|
|
10
|
+
if (newValue === null) {
|
|
11
|
+
localOtherValue.value = '';
|
|
12
|
+
emit(null);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
localOtherValue.value = newValue;
|
|
16
|
+
emit(newValue);
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
const usesOtherValue = computed(() => {
|
|
21
|
+
if (items.value === null)
|
|
22
|
+
return false;
|
|
23
|
+
// Check if set value is one of the existing keys
|
|
24
|
+
const values = items.value.map((item) => item.value);
|
|
25
|
+
return (currentValue.value !== null && currentValue.value.length > 0 && values.includes(currentValue.value) === false);
|
|
26
|
+
});
|
|
27
|
+
return { otherValue, usesOtherValue };
|
|
28
|
+
}
|
|
29
|
+
export function useCustomSelectionMultiple(currentValues, items, emit) {
|
|
30
|
+
const otherValues = ref([]);
|
|
31
|
+
watch(currentValues, (newValue) => {
|
|
32
|
+
if (newValue === null)
|
|
33
|
+
return;
|
|
34
|
+
if (Array.isArray(newValue) === false)
|
|
35
|
+
return;
|
|
36
|
+
if (items.value === null)
|
|
37
|
+
return;
|
|
38
|
+
newValue.forEach((value) => {
|
|
39
|
+
if (items.value === null)
|
|
40
|
+
return;
|
|
41
|
+
const values = items.value.map((item) => item.value);
|
|
42
|
+
const existsInValues = values.includes(value) === true;
|
|
43
|
+
if (existsInValues === false) {
|
|
44
|
+
const other = otherValues.value.map((o) => o.value);
|
|
45
|
+
const existsInOtherValues = other.includes(value) === true;
|
|
46
|
+
if (existsInOtherValues === false) {
|
|
47
|
+
addOtherValue(value);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}, { immediate: true });
|
|
52
|
+
return { otherValues, addOtherValue, setOtherValue };
|
|
53
|
+
function addOtherValue(value = '') {
|
|
54
|
+
otherValues.value = [
|
|
55
|
+
...otherValues.value,
|
|
56
|
+
{
|
|
57
|
+
key: nanoid(),
|
|
58
|
+
value: value,
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
function setOtherValue(key, newValue) {
|
|
63
|
+
const previousValue = otherValues.value.find((o) => o.key === key);
|
|
64
|
+
const valueWithoutPrevious = (currentValues.value || []).filter((val) => val !== previousValue?.value);
|
|
65
|
+
if (newValue === null) {
|
|
66
|
+
otherValues.value = otherValues.value.filter((o) => o.key !== key);
|
|
67
|
+
if (valueWithoutPrevious.length === 0) {
|
|
68
|
+
emit(null);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
emit(valueWithoutPrevious);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
otherValues.value = otherValues.value.map((otherValue) => {
|
|
76
|
+
if (otherValue.key === key)
|
|
77
|
+
otherValue.value = newValue;
|
|
78
|
+
return otherValue;
|
|
79
|
+
});
|
|
80
|
+
const newEmitValue = [...valueWithoutPrevious, newValue];
|
|
81
|
+
emit(newEmitValue);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { isNil } from 'lodash-es';
|
|
2
|
+
import { isRef, onMounted, onUnmounted, ref } from 'vue';
|
|
3
|
+
export function useElementSize(target) {
|
|
4
|
+
const width = ref(0);
|
|
5
|
+
const height = ref(0);
|
|
6
|
+
const resizeObserver = new ResizeObserver(([entry]) => {
|
|
7
|
+
if (entry === undefined)
|
|
8
|
+
return;
|
|
9
|
+
width.value = entry.contentRect.width;
|
|
10
|
+
height.value = entry.contentRect.height;
|
|
11
|
+
});
|
|
12
|
+
onMounted(() => {
|
|
13
|
+
const t = isRef(target) ? target.value : target;
|
|
14
|
+
if (!isNil(t)) {
|
|
15
|
+
resizeObserver.observe(t);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
onUnmounted(() => {
|
|
19
|
+
resizeObserver.disconnect();
|
|
20
|
+
});
|
|
21
|
+
return { width, height };
|
|
22
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { computed } from 'vue';
|
|
2
|
+
export function useFilterFields(fields, filters) {
|
|
3
|
+
const fieldGroups = computed(() => {
|
|
4
|
+
const acc = {};
|
|
5
|
+
for (const name in filters) {
|
|
6
|
+
acc[name] = [];
|
|
7
|
+
}
|
|
8
|
+
return fields.value.reduce((acc, field) => {
|
|
9
|
+
for (const name in filters) {
|
|
10
|
+
if (filters[name](field) === false)
|
|
11
|
+
continue;
|
|
12
|
+
acc[name].push(field);
|
|
13
|
+
}
|
|
14
|
+
return acc;
|
|
15
|
+
}, acc);
|
|
16
|
+
});
|
|
17
|
+
return { fieldGroups };
|
|
18
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { isEqual, isNil } from 'lodash-es';
|
|
2
|
+
import { computed, inject, nextTick, onBeforeUnmount, provide, ref, shallowRef, watch } from 'vue';
|
|
3
|
+
export function useGroupable(options) {
|
|
4
|
+
// Injects the registration / toggle functions from the parent scope
|
|
5
|
+
const parentFunctions = inject(options?.group || 'item-group', null);
|
|
6
|
+
if (isNil(parentFunctions)) {
|
|
7
|
+
return {
|
|
8
|
+
active: ref(false),
|
|
9
|
+
toggle: () => {
|
|
10
|
+
// Do nothing
|
|
11
|
+
},
|
|
12
|
+
activate: () => {
|
|
13
|
+
// Do nothing
|
|
14
|
+
},
|
|
15
|
+
deactivate: () => {
|
|
16
|
+
// Do nothing
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const { register, unregister, toggle, selection, } = parentFunctions;
|
|
21
|
+
let startActive = false;
|
|
22
|
+
if (options?.active?.value === true)
|
|
23
|
+
startActive = true;
|
|
24
|
+
if (options?.value && selection.value.includes(options.value))
|
|
25
|
+
startActive = true;
|
|
26
|
+
const active = ref(startActive);
|
|
27
|
+
const item = { active, value: options?.value };
|
|
28
|
+
register(item);
|
|
29
|
+
if (options?.active !== undefined && options.watch === true) {
|
|
30
|
+
watch(options.active, () => {
|
|
31
|
+
if (options.active === undefined)
|
|
32
|
+
return;
|
|
33
|
+
if (options.active.value === true) {
|
|
34
|
+
if (active.value === false)
|
|
35
|
+
toggle(item);
|
|
36
|
+
active.value = true;
|
|
37
|
+
}
|
|
38
|
+
if (options.active.value === false) {
|
|
39
|
+
if (active.value === true)
|
|
40
|
+
toggle(item);
|
|
41
|
+
active.value = false;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
onBeforeUnmount(() => unregister(item));
|
|
46
|
+
return {
|
|
47
|
+
active,
|
|
48
|
+
toggle: () => {
|
|
49
|
+
toggle(item);
|
|
50
|
+
},
|
|
51
|
+
activate: () => {
|
|
52
|
+
if (active.value === false)
|
|
53
|
+
toggle(item);
|
|
54
|
+
},
|
|
55
|
+
deactivate: () => {
|
|
56
|
+
if (active.value === true)
|
|
57
|
+
toggle(item);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Used to make a component a group parent component. Provides the registration / toggle functions
|
|
63
|
+
* to its group children
|
|
64
|
+
*/
|
|
65
|
+
export function useGroupableParent(state = {}, options = {}, group = 'item-group') {
|
|
66
|
+
// References to the active state and value of the individual child items
|
|
67
|
+
const items = shallowRef([]);
|
|
68
|
+
// Internal copy of the selection. This allows the composition to work without the state option
|
|
69
|
+
// being passed
|
|
70
|
+
const internalSelection = ref([]);
|
|
71
|
+
// Uses either the internal state, or the passed in state. Will call the onSelectionChange
|
|
72
|
+
// handler if it's passed
|
|
73
|
+
const selection = computed({
|
|
74
|
+
get() {
|
|
75
|
+
if (!isNil(state.selection) && !isNil(state.selection.value)) {
|
|
76
|
+
return state.selection.value;
|
|
77
|
+
}
|
|
78
|
+
return internalSelection.value;
|
|
79
|
+
},
|
|
80
|
+
set(newSelection) {
|
|
81
|
+
if (!isNil(state.onSelectionChange)) {
|
|
82
|
+
state.onSelectionChange(newSelection);
|
|
83
|
+
}
|
|
84
|
+
internalSelection.value = [...newSelection];
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
// Provide the needed functions to all children groupable components. Note: nested item groups
|
|
88
|
+
// will override the item-group namespace, making nested item groups possible.
|
|
89
|
+
provide(group, { register, unregister, toggle, selection });
|
|
90
|
+
// Whenever the value of the selection changes, we have to update all the children's internal
|
|
91
|
+
// states. If not, you can have an activated item that's not actually active.
|
|
92
|
+
watch(selection, updateChildren, { immediate: true });
|
|
93
|
+
// It takes a tick before all children are rendered, this will make sure the start state of the
|
|
94
|
+
// children matches the start selection
|
|
95
|
+
nextTick().then(updateChildren);
|
|
96
|
+
watch(() => options?.mandatory?.value, (newValue, oldValue) => {
|
|
97
|
+
if (isEqual(newValue, oldValue))
|
|
98
|
+
return;
|
|
99
|
+
// If you're required to select a value, make sure a value is selected on first render
|
|
100
|
+
if (!selection.value || (selection.value.length === 0 && options?.mandatory?.value === true)) {
|
|
101
|
+
if (items.value[0])
|
|
102
|
+
selection.value = [getValueForItem(items.value[0])];
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
// These aren't exported with any particular use in mind. It's mostly for testing purposes.
|
|
106
|
+
// Treat them as readonly.
|
|
107
|
+
return { items, selection, internalSelection, getValueForItem, updateChildren };
|
|
108
|
+
// Register a child within the context of this group
|
|
109
|
+
function register(item) {
|
|
110
|
+
items.value = [...items.value, item];
|
|
111
|
+
const value = getValueForItem(item);
|
|
112
|
+
// If you're required to select a value, make sure a value is selected on first render
|
|
113
|
+
if (selection.value.length === 0 && options?.mandatory?.value === true && items.value.length === 1) {
|
|
114
|
+
selection.value = [value];
|
|
115
|
+
}
|
|
116
|
+
if (item.active.value && selection.value.includes(value) === false) {
|
|
117
|
+
toggle(item);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Remove a child within the context of this group. Needed to avoid memory leaks.
|
|
121
|
+
function unregister(item) {
|
|
122
|
+
items.value = items.value.filter((existingItem) => {
|
|
123
|
+
return existingItem !== item;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// Toggle the active state for the given item
|
|
127
|
+
function toggle(item) {
|
|
128
|
+
if (options?.multiple?.value === true) {
|
|
129
|
+
toggleMultiple(item);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
toggleSingle(item);
|
|
133
|
+
}
|
|
134
|
+
if (!isNil(state.onToggle)) {
|
|
135
|
+
state.onToggle(item);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function toggleSingle(item) {
|
|
139
|
+
const itemValue = getValueForItem(item);
|
|
140
|
+
if (selection.value[0] === itemValue && options?.mandatory?.value !== true) {
|
|
141
|
+
selection.value = [];
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (selection.value[0] !== itemValue) {
|
|
145
|
+
selection.value = [itemValue];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function toggleMultiple(item) {
|
|
149
|
+
const itemValue = getValueForItem(item);
|
|
150
|
+
// Remove the item if it is already selected. Don't remove it if it's the last item and
|
|
151
|
+
// the mandatory option is set
|
|
152
|
+
if (selection.value.includes(itemValue)) {
|
|
153
|
+
if (options?.mandatory?.value === true && selection.value.length === 1) {
|
|
154
|
+
updateChildren();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
selection.value = selection.value.filter((value) => value !== itemValue);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// Don't add it if when we're already at the maximum number of selections
|
|
161
|
+
if (options?.max?.value && options.max.value !== -1 && selection.value.length >= options.max.value) {
|
|
162
|
+
// Even though we don't alter selection, we should flush the internal active state of
|
|
163
|
+
// the children to make sure we don't have any invalid internal active states
|
|
164
|
+
updateChildren();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// Add the selected item to the selection
|
|
168
|
+
selection.value = [...selection.value, itemValue];
|
|
169
|
+
}
|
|
170
|
+
// Converts the item reference into the value that's used in the selection. This value is either
|
|
171
|
+
// the index of the item in the items array (by default), or the custom value that's passed in
|
|
172
|
+
// the groupable composition
|
|
173
|
+
function getValueForItem(item) {
|
|
174
|
+
return item.value || items.value.findIndex((child) => item === child);
|
|
175
|
+
}
|
|
176
|
+
// Loop over all children and make sure their internal active state matches the selection array
|
|
177
|
+
// of the parent
|
|
178
|
+
function updateChildren() {
|
|
179
|
+
items.value.forEach((item) => {
|
|
180
|
+
item.active.value = selection.value.includes(getValueForItem(item));
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { getEndpoint, moveInArray } from '@directus/utils';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import { isEqual, throttle } from 'lodash-es';
|
|
4
|
+
import { computed, ref, unref, watch } from 'vue';
|
|
5
|
+
import { useCollection } from './use-collection.js';
|
|
6
|
+
import { useApi } from './use-system.js';
|
|
7
|
+
export function useItems(collection, query) {
|
|
8
|
+
const api = useApi();
|
|
9
|
+
const { primaryKeyField } = useCollection(collection);
|
|
10
|
+
const { fields, alias, limit, sort, search, filter, page } = query;
|
|
11
|
+
const endpoint = computed(() => {
|
|
12
|
+
if (!collection.value)
|
|
13
|
+
return null;
|
|
14
|
+
return getEndpoint(collection.value);
|
|
15
|
+
});
|
|
16
|
+
const items = ref([]);
|
|
17
|
+
const loading = ref(false);
|
|
18
|
+
const error = ref(null);
|
|
19
|
+
const itemCount = ref(null);
|
|
20
|
+
const totalCount = ref(null);
|
|
21
|
+
const totalPages = computed(() => {
|
|
22
|
+
if (itemCount.value === null)
|
|
23
|
+
return 1;
|
|
24
|
+
if (itemCount.value < (unref(limit) ?? 100))
|
|
25
|
+
return 1;
|
|
26
|
+
return Math.ceil(itemCount.value / (unref(limit) ?? 100));
|
|
27
|
+
});
|
|
28
|
+
const existingRequests = {
|
|
29
|
+
items: null,
|
|
30
|
+
total: null,
|
|
31
|
+
filter: null,
|
|
32
|
+
};
|
|
33
|
+
let loadingTimeout = null;
|
|
34
|
+
const fetchItems = throttle(getItems, 500);
|
|
35
|
+
watch([collection, limit, sort, search, filter, fields, page], async (after, before) => {
|
|
36
|
+
if (isEqual(after, before))
|
|
37
|
+
return;
|
|
38
|
+
const [newCollection, newLimit, newSort, newSearch, newFilter, _newFields, _newPage] = after;
|
|
39
|
+
const [oldCollection, oldLimit, oldSort, oldSearch, oldFilter, _oldFields, _oldPage] = before;
|
|
40
|
+
if (!newCollection || !query)
|
|
41
|
+
return;
|
|
42
|
+
if (newCollection !== oldCollection) {
|
|
43
|
+
reset();
|
|
44
|
+
}
|
|
45
|
+
if (!isEqual(newFilter, oldFilter) ||
|
|
46
|
+
!isEqual(newSort, oldSort) ||
|
|
47
|
+
newLimit !== oldLimit ||
|
|
48
|
+
newSearch !== oldSearch) {
|
|
49
|
+
if (oldCollection) {
|
|
50
|
+
page.value = 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (newCollection !== oldCollection || !isEqual(newFilter, oldFilter) || newSearch !== oldSearch) {
|
|
54
|
+
getItemCount();
|
|
55
|
+
}
|
|
56
|
+
fetchItems();
|
|
57
|
+
}, { deep: true, immediate: true });
|
|
58
|
+
return {
|
|
59
|
+
itemCount,
|
|
60
|
+
totalCount,
|
|
61
|
+
items,
|
|
62
|
+
totalPages,
|
|
63
|
+
loading,
|
|
64
|
+
error,
|
|
65
|
+
changeManualSort,
|
|
66
|
+
getItems,
|
|
67
|
+
getItemCount,
|
|
68
|
+
getTotalCount,
|
|
69
|
+
};
|
|
70
|
+
async function getItems() {
|
|
71
|
+
if (!endpoint.value)
|
|
72
|
+
return;
|
|
73
|
+
let isCurrentRequestCanceled = false;
|
|
74
|
+
if (existingRequests.items)
|
|
75
|
+
existingRequests.items.abort();
|
|
76
|
+
existingRequests.items = new AbortController();
|
|
77
|
+
error.value = null;
|
|
78
|
+
if (loadingTimeout) {
|
|
79
|
+
clearTimeout(loadingTimeout);
|
|
80
|
+
}
|
|
81
|
+
loadingTimeout = setTimeout(() => {
|
|
82
|
+
loading.value = true;
|
|
83
|
+
}, 150);
|
|
84
|
+
if (unref(totalCount) === null) {
|
|
85
|
+
getTotalCount();
|
|
86
|
+
}
|
|
87
|
+
let fieldsToFetch = [...(unref(fields) ?? [])];
|
|
88
|
+
// Make sure the primary key is always fetched
|
|
89
|
+
if (!unref(fields)?.includes('*') &&
|
|
90
|
+
primaryKeyField.value &&
|
|
91
|
+
fieldsToFetch.includes(primaryKeyField.value.field) === false) {
|
|
92
|
+
fieldsToFetch.push(primaryKeyField.value.field);
|
|
93
|
+
}
|
|
94
|
+
// Filter out fake internal columns. This is (among other things) for a fake $thumbnail m2o field
|
|
95
|
+
// on directus_files
|
|
96
|
+
fieldsToFetch = fieldsToFetch.filter((field) => field.startsWith('$') === false);
|
|
97
|
+
try {
|
|
98
|
+
const response = await api.get(endpoint.value, {
|
|
99
|
+
params: {
|
|
100
|
+
limit: unref(limit),
|
|
101
|
+
fields: fieldsToFetch,
|
|
102
|
+
...(alias ? { alias: unref(alias) } : {}),
|
|
103
|
+
sort: unref(sort),
|
|
104
|
+
page: unref(page),
|
|
105
|
+
search: unref(search),
|
|
106
|
+
filter: unref(filter),
|
|
107
|
+
},
|
|
108
|
+
signal: existingRequests.items.signal,
|
|
109
|
+
});
|
|
110
|
+
let fetchedItems = response.data.data;
|
|
111
|
+
existingRequests.items = null;
|
|
112
|
+
/**
|
|
113
|
+
* @NOTE
|
|
114
|
+
*
|
|
115
|
+
* This is used in conjunction with the fake field in /src/stores/fields/fields.ts to be
|
|
116
|
+
* able to render out the directus_files collection (file library) using regular layouts
|
|
117
|
+
*
|
|
118
|
+
* Layouts expect the file to be a m2o of a `file` type, however, directus_files is the
|
|
119
|
+
* only collection that doesn't have this (obviously). This fake $thumbnail field is used to
|
|
120
|
+
* pretend there is a file m2o, so we can use the regular layout logic for files as well
|
|
121
|
+
*/
|
|
122
|
+
if (collection.value === 'directus_files') {
|
|
123
|
+
fetchedItems = fetchedItems.map((file) => ({
|
|
124
|
+
...file,
|
|
125
|
+
$thumbnail: file,
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
items.value = fetchedItems;
|
|
129
|
+
if (page && fetchedItems.length === 0 && page?.value !== 1) {
|
|
130
|
+
page.value = 1;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
if (axios.isCancel(err)) {
|
|
135
|
+
isCurrentRequestCanceled = true;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
error.value = err;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
if (loadingTimeout && !isCurrentRequestCanceled) {
|
|
143
|
+
clearTimeout(loadingTimeout);
|
|
144
|
+
loadingTimeout = null;
|
|
145
|
+
}
|
|
146
|
+
if (!loadingTimeout)
|
|
147
|
+
loading.value = false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function reset() {
|
|
151
|
+
items.value = [];
|
|
152
|
+
totalCount.value = null;
|
|
153
|
+
itemCount.value = null;
|
|
154
|
+
}
|
|
155
|
+
async function changeManualSort({ item, to }) {
|
|
156
|
+
const pk = primaryKeyField.value?.field;
|
|
157
|
+
if (!pk)
|
|
158
|
+
return;
|
|
159
|
+
const fromIndex = items.value.findIndex((existing) => existing[pk] === item);
|
|
160
|
+
const toIndex = items.value.findIndex((existing) => existing[pk] === to);
|
|
161
|
+
items.value = moveInArray(items.value, fromIndex, toIndex);
|
|
162
|
+
const endpoint = computed(() => `/utils/sort/${collection.value}`);
|
|
163
|
+
await api.post(endpoint.value, { item, to });
|
|
164
|
+
}
|
|
165
|
+
async function getTotalCount() {
|
|
166
|
+
if (!endpoint.value)
|
|
167
|
+
return;
|
|
168
|
+
try {
|
|
169
|
+
if (existingRequests.total)
|
|
170
|
+
existingRequests.total.abort();
|
|
171
|
+
existingRequests.total = new AbortController();
|
|
172
|
+
const aggregate = primaryKeyField.value
|
|
173
|
+
? {
|
|
174
|
+
countDistinct: primaryKeyField.value.field,
|
|
175
|
+
}
|
|
176
|
+
: {
|
|
177
|
+
count: '*',
|
|
178
|
+
};
|
|
179
|
+
const response = await api.get(endpoint.value, {
|
|
180
|
+
params: {
|
|
181
|
+
aggregate,
|
|
182
|
+
},
|
|
183
|
+
signal: existingRequests.total.signal,
|
|
184
|
+
});
|
|
185
|
+
const count = primaryKeyField.value
|
|
186
|
+
? Number(response.data.data[0].countDistinct[primaryKeyField.value.field])
|
|
187
|
+
: Number(response.data.data[0].count);
|
|
188
|
+
existingRequests.total = null;
|
|
189
|
+
totalCount.value = count;
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
if (!axios.isCancel(err)) {
|
|
193
|
+
throw err;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function getItemCount() {
|
|
198
|
+
if (!endpoint.value)
|
|
199
|
+
return;
|
|
200
|
+
try {
|
|
201
|
+
if (existingRequests.filter)
|
|
202
|
+
existingRequests.filter.abort();
|
|
203
|
+
existingRequests.filter = new AbortController();
|
|
204
|
+
const aggregate = primaryKeyField.value
|
|
205
|
+
? {
|
|
206
|
+
countDistinct: primaryKeyField.value.field,
|
|
207
|
+
}
|
|
208
|
+
: {
|
|
209
|
+
count: '*',
|
|
210
|
+
};
|
|
211
|
+
const response = await api.get(endpoint.value, {
|
|
212
|
+
params: {
|
|
213
|
+
filter: unref(filter),
|
|
214
|
+
search: unref(search),
|
|
215
|
+
aggregate,
|
|
216
|
+
},
|
|
217
|
+
signal: existingRequests.filter.signal,
|
|
218
|
+
});
|
|
219
|
+
const count = primaryKeyField.value
|
|
220
|
+
? Number(response.data.data[0].countDistinct[primaryKeyField.value.field])
|
|
221
|
+
: Number(response.data.data[0].count);
|
|
222
|
+
existingRequests.filter = null;
|
|
223
|
+
itemCount.value = count;
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
if (!axios.isCancel(err)) {
|
|
227
|
+
throw err;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { flushPromises } from '@vue/test-utils';
|
|
2
|
+
import { isEqual } from 'lodash-es';
|
|
3
|
+
import { afterEach, expect, test, vi } from 'vitest';
|
|
4
|
+
import { computed, ref, unref } from 'vue';
|
|
5
|
+
import { useItems } from './use-items.js';
|
|
6
|
+
import { useCollection } from './use-collection.js';
|
|
7
|
+
const mockData = { id: 1 };
|
|
8
|
+
const mockCountData = { count: 2 };
|
|
9
|
+
const mockCountDistinctData = { countDistinct: { id: 3 } };
|
|
10
|
+
const mockPrimaryKeyField = {
|
|
11
|
+
collection: 'test_collection',
|
|
12
|
+
field: 'id',
|
|
13
|
+
name: 'id',
|
|
14
|
+
type: 'integer',
|
|
15
|
+
schema: null,
|
|
16
|
+
meta: null,
|
|
17
|
+
};
|
|
18
|
+
const mockApiGet = vi.fn();
|
|
19
|
+
const mockApiPost = vi.fn();
|
|
20
|
+
function isGetItemsRequest(config) {
|
|
21
|
+
if (!config.params)
|
|
22
|
+
return false;
|
|
23
|
+
return Object.keys(config.params).includes('fields');
|
|
24
|
+
}
|
|
25
|
+
function isTotalCountRequest(config) {
|
|
26
|
+
if (!config.params)
|
|
27
|
+
return false;
|
|
28
|
+
return isEqual(Object.keys(config.params), ['aggregate']);
|
|
29
|
+
}
|
|
30
|
+
function isFilterCountRequest(config) {
|
|
31
|
+
if (!config.params)
|
|
32
|
+
return false;
|
|
33
|
+
return isEqual(Object.keys(config.params), ['filter', 'search', 'aggregate']);
|
|
34
|
+
}
|
|
35
|
+
vi.mock('./use-system.js', () => ({
|
|
36
|
+
useApi: vi.fn().mockImplementation(() => ({
|
|
37
|
+
get: mockApiGet.mockImplementation((_path, config) => {
|
|
38
|
+
if (isTotalCountRequest(config) || isFilterCountRequest(config)) {
|
|
39
|
+
if (config.params.aggregate?.countDistinct)
|
|
40
|
+
return Promise.resolve({ data: { data: [mockCountDistinctData] } });
|
|
41
|
+
return Promise.resolve({ data: { data: [mockCountData] } });
|
|
42
|
+
}
|
|
43
|
+
return Promise.resolve({ data: { data: [mockData] } });
|
|
44
|
+
}),
|
|
45
|
+
post: mockApiPost,
|
|
46
|
+
})),
|
|
47
|
+
}));
|
|
48
|
+
vi.mock('./use-collection.js');
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
vi.clearAllMocks();
|
|
51
|
+
});
|
|
52
|
+
test('should fetch filter count and total count only once', async () => {
|
|
53
|
+
vi.mocked(useCollection).mockReturnValueOnce({ primaryKeyField: computed(() => null) });
|
|
54
|
+
const { totalCount, itemCount } = useItems(ref('test_collection'), {
|
|
55
|
+
fields: ref(['*']),
|
|
56
|
+
limit: ref(1),
|
|
57
|
+
sort: ref(null),
|
|
58
|
+
search: ref(null),
|
|
59
|
+
filter: ref(null),
|
|
60
|
+
page: ref(1),
|
|
61
|
+
});
|
|
62
|
+
// Wait until computed values are updated
|
|
63
|
+
await flushPromises();
|
|
64
|
+
expect(unref(totalCount)).toBe(mockCountData.count);
|
|
65
|
+
expect(unref(itemCount)).toBe(mockCountData.count);
|
|
66
|
+
expect(mockApiGet.mock.calls.filter((call) => isGetItemsRequest(call[1])).length).toBe(1);
|
|
67
|
+
expect(mockApiGet.mock.calls.filter((call) => isTotalCountRequest(call[1])).length).toBe(1);
|
|
68
|
+
expect(mockApiGet.mock.calls.filter((call) => isFilterCountRequest(call[1])).length).toBe(1);
|
|
69
|
+
});
|
|
70
|
+
test('should fetch distinct filter count and total count only once', async () => {
|
|
71
|
+
vi.mocked(useCollection).mockReturnValueOnce({ primaryKeyField: computed(() => mockPrimaryKeyField) });
|
|
72
|
+
const { totalCount, itemCount } = useItems(ref('test_collection'), {
|
|
73
|
+
fields: ref(['*']),
|
|
74
|
+
limit: ref(1),
|
|
75
|
+
sort: ref(null),
|
|
76
|
+
search: ref(null),
|
|
77
|
+
filter: ref(null),
|
|
78
|
+
page: ref(1),
|
|
79
|
+
});
|
|
80
|
+
// Wait until computed values are updated
|
|
81
|
+
await flushPromises();
|
|
82
|
+
expect(unref(totalCount)).toBe(mockCountDistinctData.countDistinct.id);
|
|
83
|
+
expect(unref(itemCount)).toBe(mockCountDistinctData.countDistinct.id);
|
|
84
|
+
expect(mockApiGet.mock.calls.filter((call) => isGetItemsRequest(call[1])).length).toBe(1);
|
|
85
|
+
expect(mockApiGet.mock.calls.filter((call) => isTotalCountRequest(call[1])).length).toBe(1);
|
|
86
|
+
expect(mockApiGet.mock.calls.filter((call) => isFilterCountRequest(call[1])).length).toBe(1);
|
|
87
|
+
});
|
|
88
|
+
test('should not re-fetch filter count when changing fields query', async () => {
|
|
89
|
+
vi.mocked(useCollection).mockReturnValueOnce({ primaryKeyField: computed(() => null) });
|
|
90
|
+
const fields = ref(['*']);
|
|
91
|
+
useItems(ref('test_collection'), {
|
|
92
|
+
fields,
|
|
93
|
+
limit: ref(1),
|
|
94
|
+
sort: ref(null),
|
|
95
|
+
search: ref(null),
|
|
96
|
+
filter: ref(null),
|
|
97
|
+
page: ref(1),
|
|
98
|
+
});
|
|
99
|
+
// update fields query
|
|
100
|
+
fields.value = ['id'];
|
|
101
|
+
// Wait until computed values are updated
|
|
102
|
+
await flushPromises();
|
|
103
|
+
expect(mockApiGet.mock.calls.filter((call) => isFilterCountRequest(call[1])).length).toBe(1);
|
|
104
|
+
});
|
|
105
|
+
test('should re-fetch filter count when changing filters query', async () => {
|
|
106
|
+
vi.mocked(useCollection).mockReturnValueOnce({ primaryKeyField: computed(() => null) });
|
|
107
|
+
const filter = ref(null);
|
|
108
|
+
useItems(ref('test_collection'), {
|
|
109
|
+
fields: ref(['*']),
|
|
110
|
+
limit: ref(1),
|
|
111
|
+
sort: ref(null),
|
|
112
|
+
search: ref(null),
|
|
113
|
+
filter,
|
|
114
|
+
page: ref(1),
|
|
115
|
+
});
|
|
116
|
+
// update filter query
|
|
117
|
+
filter.value = { id: { _eq: 1 } };
|
|
118
|
+
// Wait until computed values are updated
|
|
119
|
+
await flushPromises();
|
|
120
|
+
expect(mockApiGet.mock.calls.filter((call) => isTotalCountRequest(call[1])).length).toBe(1);
|
|
121
|
+
expect(mockApiGet.mock.calls.filter((call) => isFilterCountRequest(call[1])).length).toBe(2);
|
|
122
|
+
});
|
|
123
|
+
test('should re-fetch filter count when changing search query', async () => {
|
|
124
|
+
vi.mocked(useCollection).mockReturnValueOnce({ primaryKeyField: computed(() => null) });
|
|
125
|
+
const search = ref(null);
|
|
126
|
+
useItems(ref('test_collection'), {
|
|
127
|
+
fields: ref(['*']),
|
|
128
|
+
limit: ref(1),
|
|
129
|
+
sort: ref(null),
|
|
130
|
+
search,
|
|
131
|
+
filter: ref(null),
|
|
132
|
+
page: ref(1),
|
|
133
|
+
});
|
|
134
|
+
// update search query
|
|
135
|
+
search.value = 'test';
|
|
136
|
+
// Wait until computed values are updated
|
|
137
|
+
await flushPromises();
|
|
138
|
+
expect(mockApiGet.mock.calls.filter((call) => isTotalCountRequest(call[1])).length).toBe(1);
|
|
139
|
+
expect(mockApiGet.mock.calls.filter((call) => isFilterCountRequest(call[1])).length).toBe(2);
|
|
140
|
+
});
|
|
141
|
+
test('should reset when collection changes', async () => {
|
|
142
|
+
vi.mocked(useCollection).mockReturnValueOnce({ primaryKeyField: computed(() => null) });
|
|
143
|
+
const collection = ref('old_collection');
|
|
144
|
+
const { items } = useItems(collection, {
|
|
145
|
+
fields: ref(['*']),
|
|
146
|
+
limit: ref(1),
|
|
147
|
+
sort: ref(null),
|
|
148
|
+
search: ref(null),
|
|
149
|
+
filter: ref(null),
|
|
150
|
+
page: ref(1),
|
|
151
|
+
});
|
|
152
|
+
// Wait until computed values are updated
|
|
153
|
+
await flushPromises();
|
|
154
|
+
expect(unref(items)).toEqual([mockData]);
|
|
155
|
+
// update collection ref
|
|
156
|
+
collection.value = 'new_collection';
|
|
157
|
+
// Wait until computed values are updated again
|
|
158
|
+
await flushPromises();
|
|
159
|
+
expect(unref(items)).toEqual([]);
|
|
160
|
+
});
|
|
161
|
+
test('should append $thumbnail to fetched items when collection is directus_files', async () => {
|
|
162
|
+
vi.mocked(useCollection).mockReturnValueOnce({ primaryKeyField: computed(() => null) });
|
|
163
|
+
const collection = ref('directus_files');
|
|
164
|
+
const { items } = useItems(collection, {
|
|
165
|
+
fields: ref(['*']),
|
|
166
|
+
limit: ref(1),
|
|
167
|
+
sort: ref(null),
|
|
168
|
+
search: ref(null),
|
|
169
|
+
filter: ref(null),
|
|
170
|
+
page: ref(1),
|
|
171
|
+
});
|
|
172
|
+
// Wait until computed values are updated
|
|
173
|
+
await flushPromises();
|
|
174
|
+
expect(unref(items)).toEqual([{ id: mockData.id, $thumbnail: mockData }]);
|
|
175
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { computed, defineComponent, reactive, toRefs } from 'vue';
|
|
2
|
+
import { useExtensions } from './use-system.js';
|
|
3
|
+
const NAME_SUFFIX = 'wrapper';
|
|
4
|
+
const WRITABLE_PROPS = ['selection', 'layoutOptions', 'layoutQuery'];
|
|
5
|
+
function isWritableProp(prop) {
|
|
6
|
+
return WRITABLE_PROPS.includes(prop);
|
|
7
|
+
}
|
|
8
|
+
function createLayoutWrapper(layout) {
|
|
9
|
+
return defineComponent({
|
|
10
|
+
name: `${layout.id}-${NAME_SUFFIX}`,
|
|
11
|
+
props: {
|
|
12
|
+
collection: {
|
|
13
|
+
type: String,
|
|
14
|
+
required: true,
|
|
15
|
+
},
|
|
16
|
+
selection: {
|
|
17
|
+
type: Array,
|
|
18
|
+
default: () => [],
|
|
19
|
+
},
|
|
20
|
+
layoutOptions: {
|
|
21
|
+
type: Object,
|
|
22
|
+
default: () => ({}),
|
|
23
|
+
},
|
|
24
|
+
layoutQuery: {
|
|
25
|
+
type: Object,
|
|
26
|
+
default: () => ({}),
|
|
27
|
+
},
|
|
28
|
+
filter: {
|
|
29
|
+
type: Object,
|
|
30
|
+
default: null,
|
|
31
|
+
},
|
|
32
|
+
filterUser: {
|
|
33
|
+
type: Object,
|
|
34
|
+
default: null,
|
|
35
|
+
},
|
|
36
|
+
filterSystem: {
|
|
37
|
+
type: Object,
|
|
38
|
+
default: null,
|
|
39
|
+
},
|
|
40
|
+
search: {
|
|
41
|
+
type: String,
|
|
42
|
+
default: null,
|
|
43
|
+
},
|
|
44
|
+
showSelect: {
|
|
45
|
+
type: String,
|
|
46
|
+
default: 'multiple',
|
|
47
|
+
},
|
|
48
|
+
selectMode: {
|
|
49
|
+
type: Boolean,
|
|
50
|
+
default: false,
|
|
51
|
+
},
|
|
52
|
+
readonly: {
|
|
53
|
+
type: Boolean,
|
|
54
|
+
default: false,
|
|
55
|
+
},
|
|
56
|
+
resetPreset: {
|
|
57
|
+
type: Function,
|
|
58
|
+
default: null,
|
|
59
|
+
},
|
|
60
|
+
clearFilters: {
|
|
61
|
+
type: Function,
|
|
62
|
+
default: null,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
emits: WRITABLE_PROPS.map((prop) => `update:${prop}`),
|
|
66
|
+
setup(props, { emit }) {
|
|
67
|
+
const state = reactive({ ...layout.setup(props, { emit }), ...toRefs(props) });
|
|
68
|
+
for (const key in state) {
|
|
69
|
+
state[`onUpdate:${key}`] = (value) => {
|
|
70
|
+
if (isWritableProp(key)) {
|
|
71
|
+
emit(`update:${key}`, value);
|
|
72
|
+
}
|
|
73
|
+
else if (!Object.keys(props).includes(key)) {
|
|
74
|
+
state[key] = value;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return { state };
|
|
79
|
+
},
|
|
80
|
+
render(ctx) {
|
|
81
|
+
return ctx.$slots.default !== undefined ? ctx.$slots.default({ layoutState: ctx.state }) : null;
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
export function useLayout(layoutId) {
|
|
86
|
+
const { layouts } = useExtensions();
|
|
87
|
+
const layoutWrappers = computed(() => layouts.value.map((layout) => createLayoutWrapper(layout)));
|
|
88
|
+
const layoutWrapper = computed(() => {
|
|
89
|
+
const layout = layoutWrappers.value.find((layout) => layout.name === `${layoutId.value}-${NAME_SUFFIX}`);
|
|
90
|
+
if (layout === undefined) {
|
|
91
|
+
return layoutWrappers.value.find((layout) => layout.name === `tabular-${NAME_SUFFIX}`);
|
|
92
|
+
}
|
|
93
|
+
return layout;
|
|
94
|
+
});
|
|
95
|
+
return { layoutWrapper };
|
|
96
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { computed } from 'vue';
|
|
2
|
+
export const sizeProps = {
|
|
3
|
+
xSmall: {
|
|
4
|
+
type: Boolean,
|
|
5
|
+
default: false,
|
|
6
|
+
},
|
|
7
|
+
small: {
|
|
8
|
+
type: Boolean,
|
|
9
|
+
default: false,
|
|
10
|
+
},
|
|
11
|
+
large: {
|
|
12
|
+
type: Boolean,
|
|
13
|
+
default: false,
|
|
14
|
+
},
|
|
15
|
+
xLarge: {
|
|
16
|
+
type: Boolean,
|
|
17
|
+
default: false,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
export function useSizeClass(props) {
|
|
21
|
+
const sizeClass = computed(() => {
|
|
22
|
+
if (props.xSmall)
|
|
23
|
+
return 'x-small';
|
|
24
|
+
if (props.small)
|
|
25
|
+
return 'small';
|
|
26
|
+
if (props.large)
|
|
27
|
+
return 'large';
|
|
28
|
+
if (props.xLarge)
|
|
29
|
+
return 'x-large';
|
|
30
|
+
return null;
|
|
31
|
+
});
|
|
32
|
+
return sizeClass;
|
|
33
|
+
}
|
package/dist/use-sync.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { inject } from 'vue';
|
|
2
|
+
import { API_INJECT, EXTENSIONS_INJECT, STORES_INJECT } from '@directus/constants';
|
|
3
|
+
export function useStores() {
|
|
4
|
+
const stores = inject(STORES_INJECT);
|
|
5
|
+
if (!stores)
|
|
6
|
+
throw new Error('[useStores]: The stores could not be found.');
|
|
7
|
+
return stores;
|
|
8
|
+
}
|
|
9
|
+
export function useApi() {
|
|
10
|
+
const api = inject(API_INJECT);
|
|
11
|
+
if (!api)
|
|
12
|
+
throw new Error('[useApi]: The api could not be found.');
|
|
13
|
+
return api;
|
|
14
|
+
}
|
|
15
|
+
export function useExtensions() {
|
|
16
|
+
const extensions = inject(EXTENSIONS_INJECT);
|
|
17
|
+
if (!extensions)
|
|
18
|
+
throw new Error('[useExtensions]: The extensions could not be found.');
|
|
19
|
+
return extensions;
|
|
20
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@directus/composables",
|
|
3
|
+
"version": "9.25.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Shared Vue composables for Directus use",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/directus/directus.git",
|
|
9
|
+
"directory": "packages/composables"
|
|
10
|
+
},
|
|
11
|
+
"funding": "https://github.com/directus/directus?sponsor=1",
|
|
12
|
+
"license": "GPL-3.0",
|
|
13
|
+
"author": "Rijk van Zanten <rijkvanzanten@me.com>",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./dist/index.js",
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"main": "dist/index.js",
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"!**/*.d.ts?(.map)"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@directus/tsconfig": "0.0.6",
|
|
28
|
+
"@types/lodash-es": "4.17.7",
|
|
29
|
+
"@vitest/coverage-c8": "0.29.3",
|
|
30
|
+
"@vue/test-utils": "2.3.1",
|
|
31
|
+
"axios": "1.3.4",
|
|
32
|
+
"typescript": "4.9.5",
|
|
33
|
+
"vitest": "0.29.3",
|
|
34
|
+
"@directus/types": "9.25.0"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"lodash-es": "4.17.21",
|
|
38
|
+
"nanoid": "4.0.2",
|
|
39
|
+
"vue": "3.2.47",
|
|
40
|
+
"@directus/constants": "9.25.0",
|
|
41
|
+
"@directus/utils": "9.25.0"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc --build",
|
|
45
|
+
"dev": "tsc --watch",
|
|
46
|
+
"test": "vitest --watch=false"
|
|
47
|
+
}
|
|
48
|
+
}
|