directus-extension-duplicate-value-from-relation 1.0.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/README.md +51 -0
- package/dist/index.js +407 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# duplicate-value-from-relation
|
|
2
|
+
|
|
3
|
+
Directus interface that copies a value from a related item into the current field.
|
|
4
|
+
|
|
5
|
+
Useful when you have a relation (e.g. `bank_account`) and want another field on the same item to stay in sync with something on the related record (e.g. `currency`).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Build the extension and make sure Directus can load it from your `extensions` folder:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install
|
|
13
|
+
npm run build
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
For local development:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm run dev
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Requires Directus `^10.10.0`.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
1. Add a field to your collection.
|
|
27
|
+
2. Set the interface to **Duplicate Value From Relation**.
|
|
28
|
+
3. Pick the **Relation Field** — the M2O field pointing at the other collection.
|
|
29
|
+
4. Pick the **Source Field** — the field on that related collection to copy from.
|
|
30
|
+
|
|
31
|
+
The field is read-only in the item editor. When the relation changes, the value updates automatically.
|
|
32
|
+
|
|
33
|
+
Match the field type to the source field (`string`, `uuid`, `integer`, etc.).
|
|
34
|
+
|
|
35
|
+
## Example
|
|
36
|
+
|
|
37
|
+
Collection: `invoices`
|
|
38
|
+
|
|
39
|
+
- Relation field: `bank_account` → `currency`
|
|
40
|
+
- Synced field: `currency`
|
|
41
|
+
- Interface options:
|
|
42
|
+
- Relation Field: `bank_account`
|
|
43
|
+
- Source Field: `currency`
|
|
44
|
+
|
|
45
|
+
Selecting an account on an invoice will fill `currency` with that account's currency.
|
|
46
|
+
|
|
47
|
+
## Supported types
|
|
48
|
+
|
|
49
|
+
`string`, `text`, `integer`, `bigInteger`, `float`, `decimal`, `boolean`, `uuid`, `date`, `time`, `datetime`, `timestamp`, `json`
|
|
50
|
+
|
|
51
|
+
Only many-to-one relations are supported for the relation field picker.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { useApi, useStores, getRelationType, defineInterface } from '@directus/extensions-sdk';
|
|
2
|
+
import { inject, ref, computed, watch, resolveComponent, openBlock, createElementBlock, createBlock, withCtx, createTextVNode, toDisplayString, Fragment, createVNode, createCommentVNode } from 'vue';
|
|
3
|
+
|
|
4
|
+
var e=[],t=[];function n(n,r){if(n&&"undefined"!=typeof document){var a,s=true===r.prepend?"prepend":"append",d=true===r.singleTag,i="string"==typeof r.container?document.querySelector(r.container):document.getElementsByTagName("head")[0];if(d){var u=e.indexOf(i);-1===u&&(u=e.push(i)-1,t[u]={}),a=t[u]&&t[u][s]?t[u][s]:t[u][s]=c();}else a=c();65279===n.charCodeAt(0)&&(n=n.substring(1)),a.styleSheet?a.styleSheet.cssText+=n:a.appendChild(document.createTextNode(n));}function c(){var e=document.createElement("style");if(e.setAttribute("type","text/css"),r.attributes)for(var t=Object.keys(r.attributes),n=0;n<t.length;n++)e.setAttribute(t[n],r.attributes[t[n]]);var a="prepend"===s?"afterbegin":"beforeend";return i.insertAdjacentElement(a,e),e}}
|
|
5
|
+
|
|
6
|
+
var css$1 = "\n.duplicate-value-from-relation[data-v-9d64cc82] {\n\twidth: 100%;\n}\n";
|
|
7
|
+
n(css$1,{});
|
|
8
|
+
|
|
9
|
+
var _export_sfc = (sfc, props) => {
|
|
10
|
+
const target = sfc.__vccOpts || sfc;
|
|
11
|
+
for (const [key, val] of props) {
|
|
12
|
+
target[key] = val;
|
|
13
|
+
}
|
|
14
|
+
return target;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const _hoisted_1$1 = { class: "duplicate-value-from-relation" };
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
const _sfc_main$1 = {
|
|
21
|
+
__name: 'interface',
|
|
22
|
+
props: {
|
|
23
|
+
value: {
|
|
24
|
+
default: null,
|
|
25
|
+
},
|
|
26
|
+
collection: {
|
|
27
|
+
type: String,
|
|
28
|
+
default: null,
|
|
29
|
+
},
|
|
30
|
+
type: {
|
|
31
|
+
type: String,
|
|
32
|
+
default: 'string',
|
|
33
|
+
},
|
|
34
|
+
relationField: {
|
|
35
|
+
type: String,
|
|
36
|
+
default: null,
|
|
37
|
+
},
|
|
38
|
+
sourceField: {
|
|
39
|
+
type: String,
|
|
40
|
+
default: null,
|
|
41
|
+
},
|
|
42
|
+
disabled: {
|
|
43
|
+
type: Boolean,
|
|
44
|
+
default: false,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
emits: ['input'],
|
|
48
|
+
setup(__props, { emit: __emit }) {
|
|
49
|
+
|
|
50
|
+
const props = __props;
|
|
51
|
+
|
|
52
|
+
const emit = __emit;
|
|
53
|
+
|
|
54
|
+
const api = useApi();
|
|
55
|
+
const { useRelationsStore } = useStores();
|
|
56
|
+
const relationsStore = useRelationsStore();
|
|
57
|
+
const values = inject('values');
|
|
58
|
+
|
|
59
|
+
const loading = ref(false);
|
|
60
|
+
|
|
61
|
+
const isConfigured = computed(() => Boolean(props.relationField && props.sourceField));
|
|
62
|
+
|
|
63
|
+
const relationValue = computed(() => {
|
|
64
|
+
if (!values?.value || !props.relationField) return null;
|
|
65
|
+
return values.value[props.relationField];
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const hasRelationValue = computed(() => {
|
|
69
|
+
const current = relationValue.value;
|
|
70
|
+
return current !== null && current !== undefined && current !== '';
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const displayValue = computed(() => {
|
|
74
|
+
if (props.value === null || props.value === undefined) return '';
|
|
75
|
+
|
|
76
|
+
if (props.type === 'json' && typeof props.value === 'object') {
|
|
77
|
+
return JSON.stringify(props.value, null, 2);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return String(props.value);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const placeholder = computed(() => {
|
|
84
|
+
if (!props.sourceField) return '';
|
|
85
|
+
return `Synced from ${props.sourceField}`;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
watch(
|
|
89
|
+
[relationValue, () => props.relationField, () => props.sourceField, () => props.collection],
|
|
90
|
+
() => {
|
|
91
|
+
if (!isConfigured.value || props.disabled) return;
|
|
92
|
+
syncValue();
|
|
93
|
+
},
|
|
94
|
+
{ deep: true, immediate: true },
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
async function syncValue() {
|
|
98
|
+
const currentRelationValue = relationValue.value;
|
|
99
|
+
|
|
100
|
+
if (currentRelationValue === null || currentRelationValue === undefined || currentRelationValue === '') {
|
|
101
|
+
if (props.value !== null && props.value !== undefined) {
|
|
102
|
+
emit('input', null);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const syncedValue = await resolveSourceValue(currentRelationValue);
|
|
109
|
+
|
|
110
|
+
if (!valuesEqual(syncedValue, props.value)) {
|
|
111
|
+
emit('input', syncedValue);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function resolveSourceValue(relationItem) {
|
|
116
|
+
if (typeof relationItem === 'object' && relationItem !== null && props.sourceField in relationItem) {
|
|
117
|
+
return relationItem[props.sourceField];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const relation = relationsStore.getRelationForField(props.collection, props.relationField);
|
|
121
|
+
const relatedCollection = relation?.related_collection;
|
|
122
|
+
|
|
123
|
+
if (!relatedCollection) return null;
|
|
124
|
+
|
|
125
|
+
const primaryKey =
|
|
126
|
+
typeof relationItem === 'object' && relationItem !== null
|
|
127
|
+
? relationItem[relation.meta?.related_field || 'id']
|
|
128
|
+
: relationItem;
|
|
129
|
+
|
|
130
|
+
if (primaryKey === null || primaryKey === undefined || primaryKey === '') {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
loading.value = true;
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const response = await api.get(`/items/${relatedCollection}/${primaryKey}`, {
|
|
138
|
+
params: {
|
|
139
|
+
fields: [props.sourceField],
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return response.data.data?.[props.sourceField] ?? null;
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
} finally {
|
|
147
|
+
loading.value = false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function valuesEqual(a, b) {
|
|
152
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return (_ctx, _cache) => {
|
|
156
|
+
const _component_v_notice = resolveComponent("v-notice");
|
|
157
|
+
const _component_v_skeleton_loader = resolveComponent("v-skeleton-loader");
|
|
158
|
+
const _component_v_checkbox = resolveComponent("v-checkbox");
|
|
159
|
+
const _component_v_textarea = resolveComponent("v-textarea");
|
|
160
|
+
const _component_v_input = resolveComponent("v-input");
|
|
161
|
+
|
|
162
|
+
return (openBlock(), createElementBlock("div", _hoisted_1$1, [
|
|
163
|
+
(!isConfigured.value)
|
|
164
|
+
? (openBlock(), createBlock(_component_v_notice, {
|
|
165
|
+
key: 0,
|
|
166
|
+
type: "warning"
|
|
167
|
+
}, {
|
|
168
|
+
default: withCtx(() => [...(_cache[0] || (_cache[0] = [
|
|
169
|
+
createTextVNode(" Configure the relation field and source field in the interface settings. ", -1 /* CACHED */)
|
|
170
|
+
]))]),
|
|
171
|
+
_: 1 /* STABLE */
|
|
172
|
+
}))
|
|
173
|
+
: (!hasRelationValue.value)
|
|
174
|
+
? (openBlock(), createBlock(_component_v_notice, {
|
|
175
|
+
key: 1,
|
|
176
|
+
type: "info"
|
|
177
|
+
}, {
|
|
178
|
+
default: withCtx(() => [
|
|
179
|
+
createTextVNode(" Select a related item in \"" + toDisplayString(__props.relationField) + "\" to sync this field. ", 1 /* TEXT */)
|
|
180
|
+
]),
|
|
181
|
+
_: 1 /* STABLE */
|
|
182
|
+
}))
|
|
183
|
+
: (openBlock(), createElementBlock(Fragment, { key: 2 }, [
|
|
184
|
+
(loading.value)
|
|
185
|
+
? (openBlock(), createBlock(_component_v_skeleton_loader, {
|
|
186
|
+
key: 0,
|
|
187
|
+
type: "input"
|
|
188
|
+
}))
|
|
189
|
+
: (__props.type === 'boolean')
|
|
190
|
+
? (openBlock(), createBlock(_component_v_checkbox, {
|
|
191
|
+
key: 1,
|
|
192
|
+
"model-value": Boolean(__props.value),
|
|
193
|
+
disabled: ""
|
|
194
|
+
}, {
|
|
195
|
+
label: withCtx(() => [...(_cache[1] || (_cache[1] = [
|
|
196
|
+
createTextVNode("Synced value", -1 /* CACHED */)
|
|
197
|
+
]))]),
|
|
198
|
+
_: 1 /* STABLE */
|
|
199
|
+
}, 8 /* PROPS */, ["model-value"]))
|
|
200
|
+
: (__props.type === 'json' || __props.type === 'text')
|
|
201
|
+
? (openBlock(), createBlock(_component_v_textarea, {
|
|
202
|
+
key: 2,
|
|
203
|
+
"model-value": displayValue.value,
|
|
204
|
+
disabled: "",
|
|
205
|
+
placeholder: placeholder.value
|
|
206
|
+
}, null, 8 /* PROPS */, ["model-value", "placeholder"]))
|
|
207
|
+
: (openBlock(), createBlock(_component_v_input, {
|
|
208
|
+
key: 3,
|
|
209
|
+
"model-value": displayValue.value,
|
|
210
|
+
disabled: "",
|
|
211
|
+
placeholder: placeholder.value
|
|
212
|
+
}, null, 8 /* PROPS */, ["model-value", "placeholder"]))
|
|
213
|
+
], 64 /* STABLE_FRAGMENT */))
|
|
214
|
+
]))
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
};
|
|
219
|
+
var InterfaceComponent = /*#__PURE__*/_export_sfc(_sfc_main$1, [['__scopeId',"data-v-9d64cc82"]]);
|
|
220
|
+
|
|
221
|
+
var css = "\n.options[data-v-36ad6c04] {\n\tdisplay: grid;\n\tgap: 20px;\n}\n";
|
|
222
|
+
n(css,{});
|
|
223
|
+
|
|
224
|
+
const _hoisted_1 = { class: "options" };
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
const _sfc_main = {
|
|
228
|
+
__name: 'options',
|
|
229
|
+
props: {
|
|
230
|
+
value: {
|
|
231
|
+
type: Object,
|
|
232
|
+
default: () => ({}),
|
|
233
|
+
},
|
|
234
|
+
collection: {
|
|
235
|
+
type: String,
|
|
236
|
+
default: null,
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
emits: ['input'],
|
|
240
|
+
setup(__props, { emit: __emit }) {
|
|
241
|
+
|
|
242
|
+
const props = __props;
|
|
243
|
+
|
|
244
|
+
const emit = __emit;
|
|
245
|
+
|
|
246
|
+
const { useFieldsStore, useRelationsStore } = useStores();
|
|
247
|
+
const fieldsStore = useFieldsStore();
|
|
248
|
+
const relationsStore = useRelationsStore();
|
|
249
|
+
|
|
250
|
+
const relationField = computed(() => props.value?.relationField ?? null);
|
|
251
|
+
const sourceField = computed(() => props.value?.sourceField ?? null);
|
|
252
|
+
|
|
253
|
+
const relatedCollection = computed(() => {
|
|
254
|
+
if (!props.collection || !relationField.value) return null;
|
|
255
|
+
|
|
256
|
+
const relation = relationsStore.getRelationForField(props.collection, relationField.value);
|
|
257
|
+
if (!relation) return null;
|
|
258
|
+
|
|
259
|
+
const type = getRelationType({
|
|
260
|
+
relation,
|
|
261
|
+
collection: props.collection,
|
|
262
|
+
field: relationField.value,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
return type === 'm2o' ? relation.related_collection : null;
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const relationFieldChoices = computed(() => {
|
|
269
|
+
if (!props.collection) return [];
|
|
270
|
+
|
|
271
|
+
return fieldsStore
|
|
272
|
+
.getFieldsForCollection(props.collection)
|
|
273
|
+
.filter((field) => {
|
|
274
|
+
if (field.meta?.hidden) return false;
|
|
275
|
+
|
|
276
|
+
const relation = relationsStore.getRelationForField(props.collection, field.field);
|
|
277
|
+
if (!relation) return false;
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
getRelationType({
|
|
281
|
+
relation,
|
|
282
|
+
collection: props.collection,
|
|
283
|
+
field: field.field,
|
|
284
|
+
}) === 'm2o'
|
|
285
|
+
);
|
|
286
|
+
})
|
|
287
|
+
.map((field) => ({
|
|
288
|
+
text: field.name || field.field,
|
|
289
|
+
value: field.field,
|
|
290
|
+
}));
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const sourceFieldChoices = computed(() => {
|
|
294
|
+
if (!relatedCollection.value) return [];
|
|
295
|
+
|
|
296
|
+
return fieldsStore
|
|
297
|
+
.getFieldsForCollection(relatedCollection.value)
|
|
298
|
+
.filter((field) => {
|
|
299
|
+
if (field.meta?.hidden) return false;
|
|
300
|
+
if (field.type === 'alias') return false;
|
|
301
|
+
if (field.meta?.special?.includes('group')) return false;
|
|
302
|
+
return true;
|
|
303
|
+
})
|
|
304
|
+
.map((field) => ({
|
|
305
|
+
text: field.name || field.field,
|
|
306
|
+
value: field.field,
|
|
307
|
+
}));
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
function setRelationField(field) {
|
|
311
|
+
emit('input', {
|
|
312
|
+
...(props.value || {}),
|
|
313
|
+
relationField: field,
|
|
314
|
+
sourceField: null,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function setSourceField(field) {
|
|
319
|
+
emit('input', {
|
|
320
|
+
...(props.value || {}),
|
|
321
|
+
sourceField: field,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return (_ctx, _cache) => {
|
|
326
|
+
const _component_v_select = resolveComponent("v-select");
|
|
327
|
+
const _component_v_notice = resolveComponent("v-notice");
|
|
328
|
+
|
|
329
|
+
return (openBlock(), createElementBlock("div", _hoisted_1, [
|
|
330
|
+
createVNode(_component_v_select, {
|
|
331
|
+
"model-value": relationField.value,
|
|
332
|
+
items: relationFieldChoices.value,
|
|
333
|
+
disabled: !__props.collection,
|
|
334
|
+
placeholder: "Select a relation field",
|
|
335
|
+
"onUpdate:modelValue": setRelationField
|
|
336
|
+
}, {
|
|
337
|
+
label: withCtx(() => [...(_cache[0] || (_cache[0] = [
|
|
338
|
+
createTextVNode("Relation Field", -1 /* CACHED */)
|
|
339
|
+
]))]),
|
|
340
|
+
_: 1 /* STABLE */
|
|
341
|
+
}, 8 /* PROPS */, ["model-value", "items", "disabled"]),
|
|
342
|
+
createVNode(_component_v_select, {
|
|
343
|
+
"model-value": sourceField.value,
|
|
344
|
+
items: sourceFieldChoices.value,
|
|
345
|
+
disabled: !relatedCollection.value,
|
|
346
|
+
placeholder: "Select a source field",
|
|
347
|
+
"onUpdate:modelValue": setSourceField
|
|
348
|
+
}, {
|
|
349
|
+
label: withCtx(() => [...(_cache[1] || (_cache[1] = [
|
|
350
|
+
createTextVNode("Source Field", -1 /* CACHED */)
|
|
351
|
+
]))]),
|
|
352
|
+
_: 1 /* STABLE */
|
|
353
|
+
}, 8 /* PROPS */, ["model-value", "items", "disabled"]),
|
|
354
|
+
(!__props.collection)
|
|
355
|
+
? (openBlock(), createBlock(_component_v_notice, {
|
|
356
|
+
key: 0,
|
|
357
|
+
type: "warning"
|
|
358
|
+
}, {
|
|
359
|
+
default: withCtx(() => [...(_cache[2] || (_cache[2] = [
|
|
360
|
+
createTextVNode(" Save the field first to configure relation syncing. ", -1 /* CACHED */)
|
|
361
|
+
]))]),
|
|
362
|
+
_: 1 /* STABLE */
|
|
363
|
+
}))
|
|
364
|
+
: (relationField.value && !relatedCollection.value)
|
|
365
|
+
? (openBlock(), createBlock(_component_v_notice, {
|
|
366
|
+
key: 1,
|
|
367
|
+
type: "warning"
|
|
368
|
+
}, {
|
|
369
|
+
default: withCtx(() => [...(_cache[3] || (_cache[3] = [
|
|
370
|
+
createTextVNode(" The selected field is not a many-to-one relation. ", -1 /* CACHED */)
|
|
371
|
+
]))]),
|
|
372
|
+
_: 1 /* STABLE */
|
|
373
|
+
}))
|
|
374
|
+
: createCommentVNode("v-if", true)
|
|
375
|
+
]))
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
};
|
|
380
|
+
var OptionsComponent = /*#__PURE__*/_export_sfc(_sfc_main, [['__scopeId',"data-v-36ad6c04"]]);
|
|
381
|
+
|
|
382
|
+
var index = defineInterface({
|
|
383
|
+
id: 'duplicate-value-from-relation',
|
|
384
|
+
name: 'Duplicate Value From Relation',
|
|
385
|
+
icon: 'content_copy',
|
|
386
|
+
description: 'Automatically sync this field from a field on a related item.',
|
|
387
|
+
component: InterfaceComponent,
|
|
388
|
+
options: OptionsComponent,
|
|
389
|
+
types: [
|
|
390
|
+
'string',
|
|
391
|
+
'text',
|
|
392
|
+
'integer',
|
|
393
|
+
'bigInteger',
|
|
394
|
+
'float',
|
|
395
|
+
'decimal',
|
|
396
|
+
'boolean',
|
|
397
|
+
'uuid',
|
|
398
|
+
'date',
|
|
399
|
+
'time',
|
|
400
|
+
'datetime',
|
|
401
|
+
'timestamp',
|
|
402
|
+
'json',
|
|
403
|
+
],
|
|
404
|
+
group: 'standard',
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
export { index as default };
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "directus-extension-duplicate-value-from-relation",
|
|
3
|
+
"description": "Sync a field value from a related collection item",
|
|
4
|
+
"icon": "extension",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"directus",
|
|
8
|
+
"directus-extension",
|
|
9
|
+
"directus-extension-interface"
|
|
10
|
+
],
|
|
11
|
+
"type": "module",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/Abdallah-Awwad/directus-extension-duplicate-value-from-relation-interface",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/Abdallah-Awwad/directus-extension-duplicate-value-from-relation-interface"
|
|
19
|
+
},
|
|
20
|
+
"author": {
|
|
21
|
+
"name": "Abdallah Awwad",
|
|
22
|
+
"email": "abdallahhawwad@gmail.com"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"directus:extension": {
|
|
26
|
+
"type": "interface",
|
|
27
|
+
"path": "dist/index.js",
|
|
28
|
+
"source": "src/index.js",
|
|
29
|
+
"host": "^10.10.0"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "directus-extension build",
|
|
33
|
+
"dev": "directus-extension build -w --no-minify",
|
|
34
|
+
"link": "directus-extension link",
|
|
35
|
+
"validate": "directus-extension validate"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@directus/extensions-sdk": "18.0.1",
|
|
39
|
+
"vue": "^3.5.39"
|
|
40
|
+
}
|
|
41
|
+
}
|