@meeovi/directus-client 1.0.0 → 1.0.2
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 +238 -0
- package/dist/client/createClient.d.ts +14 -0
- package/dist/client/createClient.js +18 -0
- package/dist/generators/form-engine.d.ts +13 -0
- package/dist/generators/form-engine.js +22 -0
- package/dist/generators/table-engine.d.ts +6 -0
- package/dist/generators/table-engine.js +9 -0
- package/dist/generators/validation-engine.d.ts +2 -0
- package/dist/generators/validation-engine.js +25 -0
- package/dist/generators/widget-registry.d.ts +9 -0
- package/dist/generators/widget-registry.js +54 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +17 -0
- package/dist/react/DirectusProvider.d.ts +7 -0
- package/dist/react/DirectusProvider.js +6 -0
- package/dist/react/useDirectus.d.ts +2 -0
- package/dist/react/useDirectus.js +9 -0
- package/dist/schema/introspect.d.ts +5 -0
- package/dist/schema/introspect.js +30 -0
- package/dist/schema/types.d.ts +47 -0
- package/dist/schema/types.js +1 -0
- package/dist/utils/collections.d.ts +5 -0
- package/dist/utils/collections.js +13 -0
- package/dist/utils/fields.d.ts +4 -0
- package/dist/utils/fields.js +40 -0
- package/dist/utils/livePreview.d.ts +11 -0
- package/dist/utils/livePreview.js +11 -0
- package/dist/utils/useDirectusField.d.ts +10 -0
- package/dist/utils/useDirectusField.js +119 -0
- package/dist/utils/useDirectusRequest.d.ts +7 -0
- package/dist/utils/useDirectusRequest.js +33 -0
- package/dist/utils/useLivePreview.d.ts +1 -0
- package/dist/utils/useLivePreview.js +16 -0
- package/dist/utils/useVisualEditing.d.ts +18 -0
- package/dist/utils/useVisualEditing.js +27 -0
- package/dist/utils/visualEditing.d.ts +21 -0
- package/dist/utils/visualEditing.js +25 -0
- package/dist/vue/DirectusProvider.d.ts +15 -0
- package/dist/vue/DirectusProvider.js +12 -0
- package/dist/vue/useDirectus.d.ts +2 -0
- package/dist/vue/useDirectus.js +9 -0
- package/package.json +16 -5
- package/src/client/createClient.ts +5 -4
- package/src/generators/form-engine.ts +31 -81
- package/src/generators/widget-registry.ts +71 -4
- package/src/react/useDirectus.ts +1 -1
- package/src/utils/livePreview.ts +24 -0
- package/src/utils/visualEditing.ts +46 -0
- package/src/vue/useDirectus.ts +1 -1
- package/tsconfig.json +15 -0
- package/src/utils/useDirectusField.ts +0 -144
- package/src/utils/useDirectusRequest.ts +0 -32
- package/src/utils/useDirectusSchema.js +0 -9
- package/src/utils/useLivePreview.ts +0 -17
- package/src/utils/useVisualEditing.ts +0 -38
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function useDirectusField(field: any, modelValue: any, emit: any, formContext?: any): {
|
|
2
|
+
internalValue: import("vue").WritableComputedRef<any, any>;
|
|
3
|
+
internalAuto: import("vue").ComputedRef<boolean>;
|
|
4
|
+
label: import("vue").ComputedRef<string>;
|
|
5
|
+
fieldLookup: import("vue").ComputedRef<any>;
|
|
6
|
+
required: import("vue").ComputedRef<boolean>;
|
|
7
|
+
safeOptions: import("vue").ComputedRef<any>;
|
|
8
|
+
defaultValue: import("vue").ComputedRef<any>;
|
|
9
|
+
isMultiple: import("vue").ComputedRef<boolean>;
|
|
10
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { ref, computed, watch } from "vue";
|
|
2
|
+
import Input from "~/components/ui/forms/TextInput.vue";
|
|
3
|
+
import Select from "~/components/ui/forms/SelectInput.vue";
|
|
4
|
+
import DateTime from "~/components/ui/forms/DateTime.vue";
|
|
5
|
+
import FileInput from "~/components/ui/forms/FileInput.vue";
|
|
6
|
+
import BooleanInput from "~/components/ui/forms/BooleanInput.vue";
|
|
7
|
+
import RelationSelect from "~/components/ui/forms/RelationSelect.vue";
|
|
8
|
+
import TiptapEditor from "~/components/ui/forms/TiptapEditor.vue";
|
|
9
|
+
export function useDirectusField(field, modelValue, emit, formContext) {
|
|
10
|
+
const internalValue = computed({
|
|
11
|
+
get: () => modelValue,
|
|
12
|
+
set: (v) => emit("update:modelValue", v),
|
|
13
|
+
});
|
|
14
|
+
const previousAuto = ref(null);
|
|
15
|
+
const internalAuto = computed(() => {
|
|
16
|
+
return previousAuto.value != null && internalValue.value === previousAuto.value;
|
|
17
|
+
});
|
|
18
|
+
const slugify = (val) => String(val || "")
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.normalize("NFKD")
|
|
21
|
+
.replace(/\s+/g, "-")
|
|
22
|
+
.replace(/[^a-z0-9\-]/g, "")
|
|
23
|
+
.replace(/-+/g, "-")
|
|
24
|
+
.replace(/^-|-$/g, "");
|
|
25
|
+
const isAliasField = computed(() => {
|
|
26
|
+
const meta = field?.meta ?? {};
|
|
27
|
+
return (meta.interface === "alias" ||
|
|
28
|
+
(meta.options && (meta.options.source || meta.options.from)));
|
|
29
|
+
});
|
|
30
|
+
if (isAliasField.value && formContext && formContext.form) {
|
|
31
|
+
const meta = field?.meta ?? {};
|
|
32
|
+
let sourceField = meta?.options?.source ?? meta?.options?.from ?? null;
|
|
33
|
+
if (!sourceField && formContext.fields) {
|
|
34
|
+
const candidates = ["name", "title"];
|
|
35
|
+
sourceField =
|
|
36
|
+
candidates.find((c) => formContext.fields.value.some((f) => f.field === c)) ?? null;
|
|
37
|
+
}
|
|
38
|
+
if (sourceField) {
|
|
39
|
+
watch(() => formContext.form.value?.[sourceField], (newVal) => {
|
|
40
|
+
const target = field.field;
|
|
41
|
+
const current = formContext.form.value?.[target] ?? "";
|
|
42
|
+
const generated = slugify(String(newVal ?? ""));
|
|
43
|
+
if (!generated)
|
|
44
|
+
return;
|
|
45
|
+
// Case 1: Empty or previously auto‑generated → regenerate
|
|
46
|
+
if (!current || current === previousAuto.value) {
|
|
47
|
+
formContext.form.value[target] = generated;
|
|
48
|
+
internalValue.value = generated;
|
|
49
|
+
previousAuto.value = generated;
|
|
50
|
+
}
|
|
51
|
+
}, { immediate: true });
|
|
52
|
+
// Watch the target itself: if cleared, regenerate
|
|
53
|
+
watch(() => formContext.form.value?.[field.field], (newVal) => {
|
|
54
|
+
if (!newVal) {
|
|
55
|
+
const sourceVal = formContext.form.value?.[sourceField] ?? "";
|
|
56
|
+
const regenerated = slugify(String(sourceVal));
|
|
57
|
+
if (regenerated) {
|
|
58
|
+
formContext.form.value[field.field] = regenerated;
|
|
59
|
+
internalValue.value = regenerated;
|
|
60
|
+
previousAuto.value = regenerated;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// --- Label prettifier ---
|
|
67
|
+
const prettify = (s) => s.replace(/_/g, " ")
|
|
68
|
+
.split(" ")
|
|
69
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
70
|
+
.join(" ");
|
|
71
|
+
const label = computed(() => {
|
|
72
|
+
const raw = field?.meta?.field ?? field?.field ?? "";
|
|
73
|
+
return prettify(raw);
|
|
74
|
+
});
|
|
75
|
+
// --- Field lookup ---
|
|
76
|
+
const fieldLookup = computed(() => {
|
|
77
|
+
const iface = field?.meta?.interface;
|
|
78
|
+
const name = String(iface ?? "").toLowerCase();
|
|
79
|
+
if (["input", "string"].includes(name))
|
|
80
|
+
return Input;
|
|
81
|
+
if (["textarea", "wysiwyg", "rich-text", "input-multiline", "input-rich-text", "input-rich-text-md"].includes(name))
|
|
82
|
+
return TiptapEditor;
|
|
83
|
+
if (["select", "select-dropdown", "select-multiple"].includes(name))
|
|
84
|
+
return Select;
|
|
85
|
+
if (name === "datetime")
|
|
86
|
+
return DateTime;
|
|
87
|
+
if (["file", "files", "file-image"].includes(name))
|
|
88
|
+
return FileInput;
|
|
89
|
+
if (name === "boolean")
|
|
90
|
+
return BooleanInput;
|
|
91
|
+
if (name.includes("many") || name.includes("one") || name.includes("to"))
|
|
92
|
+
return RelationSelect;
|
|
93
|
+
return Input;
|
|
94
|
+
});
|
|
95
|
+
const required = computed(() => {
|
|
96
|
+
const meta = field?.meta ?? {};
|
|
97
|
+
return Boolean(meta?.validation?.required ?? meta?.required ?? false);
|
|
98
|
+
});
|
|
99
|
+
const safeOptions = computed(() => field?.meta?.options ?? null);
|
|
100
|
+
const defaultValue = computed(() => field?.schema?.default_value ?? null);
|
|
101
|
+
const isMultiple = computed(() => {
|
|
102
|
+
const name = String(field?.meta?.interface ?? "").toLowerCase();
|
|
103
|
+
if (name === "select-multiple")
|
|
104
|
+
return true;
|
|
105
|
+
if (name.includes("many"))
|
|
106
|
+
return true;
|
|
107
|
+
return Boolean(field?.meta?.options?.multiple || false);
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
internalValue,
|
|
111
|
+
internalAuto,
|
|
112
|
+
label,
|
|
113
|
+
fieldLookup,
|
|
114
|
+
required,
|
|
115
|
+
safeOptions,
|
|
116
|
+
defaultValue,
|
|
117
|
+
isMultiple,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useNuxtApp } from '#imports';
|
|
2
|
+
/**
|
|
3
|
+
* Centralized safe wrapper around `$directus.request`.
|
|
4
|
+
* Shows a toast error when the Directus client or `.request` is not available.
|
|
5
|
+
*/
|
|
6
|
+
export default function useDirectusRequest() {
|
|
7
|
+
const nuxt = useNuxtApp();
|
|
8
|
+
async function request(config) {
|
|
9
|
+
try {
|
|
10
|
+
const client = nuxt?.$directus;
|
|
11
|
+
if (!client || typeof client.request !== 'function') {
|
|
12
|
+
try {
|
|
13
|
+
const toast = nuxt?.$toast;
|
|
14
|
+
if (toast && typeof toast.error === 'function')
|
|
15
|
+
toast.error('Directus client unavailable');
|
|
16
|
+
}
|
|
17
|
+
catch (_) { }
|
|
18
|
+
throw new Error('Directus client.request is not available');
|
|
19
|
+
}
|
|
20
|
+
return await client.request(config);
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
try {
|
|
24
|
+
const toast = nuxt?.$toast;
|
|
25
|
+
if (toast && typeof toast.error === 'function')
|
|
26
|
+
toast.error('Request failed');
|
|
27
|
+
}
|
|
28
|
+
catch (_) { }
|
|
29
|
+
throw e;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { request };
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useLivePreview(): any;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function useLivePreview() {
|
|
2
|
+
return usePreviewMode({
|
|
3
|
+
// Enable preview mode when both preview and token params exist in URL
|
|
4
|
+
shouldEnable: () => {
|
|
5
|
+
const route = useRoute();
|
|
6
|
+
return !!route.query.preview && !!route.query.token;
|
|
7
|
+
},
|
|
8
|
+
// Store the token from the URL for use in API calls
|
|
9
|
+
getState: (currentState) => {
|
|
10
|
+
const route = useRoute();
|
|
11
|
+
return {
|
|
12
|
+
token: route.query.token || currentState.token,
|
|
13
|
+
};
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { setAttr } from '@directus/visual-editing';
|
|
2
|
+
import type { PrimaryKey } from '@directus/types';
|
|
3
|
+
interface ApplyOptions {
|
|
4
|
+
directusUrl: string;
|
|
5
|
+
elements?: HTMLElement[] | HTMLElement;
|
|
6
|
+
onSaved?: (data: {
|
|
7
|
+
collection?: string;
|
|
8
|
+
item?: PrimaryKey | null;
|
|
9
|
+
payload?: Record<string, any>;
|
|
10
|
+
}) => void;
|
|
11
|
+
customClass?: string;
|
|
12
|
+
}
|
|
13
|
+
export default function useVisualEditing(): {
|
|
14
|
+
isVisualEditingEnabled: any;
|
|
15
|
+
apply: (options: Pick<ApplyOptions, "elements" | "onSaved" | "customClass">) => void;
|
|
16
|
+
setAttr: typeof setAttr;
|
|
17
|
+
};
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { apply as applyVisualEditing, setAttr } from '@directus/visual-editing';
|
|
2
|
+
export default function useVisualEditing() {
|
|
3
|
+
// Use useState for state that persists across navigation
|
|
4
|
+
const isVisualEditingEnabled = useState('visual-editing-enabled', () => false);
|
|
5
|
+
const route = useRoute();
|
|
6
|
+
const { public: { enableVisualEditing, directusUrl }, } = useRuntimeConfig();
|
|
7
|
+
// Check query param on composable initialization.
|
|
8
|
+
if (route.query['visual-editing'] === 'true' && enableVisualEditing) {
|
|
9
|
+
isVisualEditingEnabled.value = true;
|
|
10
|
+
}
|
|
11
|
+
else if (route.query['visual-editing'] === 'false') {
|
|
12
|
+
isVisualEditingEnabled.value = false;
|
|
13
|
+
}
|
|
14
|
+
const apply = (options) => {
|
|
15
|
+
if (!isVisualEditingEnabled.value)
|
|
16
|
+
return;
|
|
17
|
+
applyVisualEditing({
|
|
18
|
+
...options,
|
|
19
|
+
directusUrl,
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
isVisualEditingEnabled,
|
|
24
|
+
apply,
|
|
25
|
+
setAttr,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { setAttr } from '@directus/visual-editing';
|
|
2
|
+
import type { PrimaryKey } from '@directus/types';
|
|
3
|
+
export interface VisualEditingOptions {
|
|
4
|
+
enableVisualEditing?: boolean;
|
|
5
|
+
directusUrl: string;
|
|
6
|
+
query?: Record<string, string | undefined>;
|
|
7
|
+
}
|
|
8
|
+
export interface ApplyOptions {
|
|
9
|
+
elements?: HTMLElement[] | HTMLElement;
|
|
10
|
+
onSaved?: (data: {
|
|
11
|
+
collection?: string;
|
|
12
|
+
item?: PrimaryKey | null;
|
|
13
|
+
payload?: Record<string, any>;
|
|
14
|
+
}) => void;
|
|
15
|
+
customClass?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function createVisualEditing(options: VisualEditingOptions): {
|
|
18
|
+
isEnabled: boolean;
|
|
19
|
+
apply: (opts: ApplyOptions) => void;
|
|
20
|
+
setAttr: typeof setAttr;
|
|
21
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { apply as applyVisualEditing, setAttr } from '@directus/visual-editing';
|
|
2
|
+
export function createVisualEditing(options) {
|
|
3
|
+
const { enableVisualEditing = false, directusUrl, query = {} } = options;
|
|
4
|
+
let isEnabled = false;
|
|
5
|
+
// Determine if visual editing should be active
|
|
6
|
+
if (query['visual-editing'] === 'true' && enableVisualEditing) {
|
|
7
|
+
isEnabled = true;
|
|
8
|
+
}
|
|
9
|
+
else if (query['visual-editing'] === 'false') {
|
|
10
|
+
isEnabled = false;
|
|
11
|
+
}
|
|
12
|
+
const apply = (opts) => {
|
|
13
|
+
if (!isEnabled)
|
|
14
|
+
return;
|
|
15
|
+
applyVisualEditing({
|
|
16
|
+
...opts,
|
|
17
|
+
directusUrl,
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
isEnabled,
|
|
22
|
+
apply,
|
|
23
|
+
setAttr,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const DirectusKey: unique symbol;
|
|
2
|
+
declare const _default: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
3
|
+
client: {
|
|
4
|
+
type: ObjectConstructor;
|
|
5
|
+
required: true;
|
|
6
|
+
};
|
|
7
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
8
|
+
[key: string]: any;
|
|
9
|
+
}>[] | undefined, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
10
|
+
client: {
|
|
11
|
+
type: ObjectConstructor;
|
|
12
|
+
required: true;
|
|
13
|
+
};
|
|
14
|
+
}>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
15
|
+
export default _default;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineComponent, provide } from 'vue';
|
|
2
|
+
export const DirectusKey = Symbol('DirectusClient');
|
|
3
|
+
export default defineComponent({
|
|
4
|
+
name: 'DirectusProvider',
|
|
5
|
+
props: {
|
|
6
|
+
client: { type: Object, required: true }
|
|
7
|
+
},
|
|
8
|
+
setup(props, { slots }) {
|
|
9
|
+
provide(DirectusKey, props.client);
|
|
10
|
+
return () => slots.default?.();
|
|
11
|
+
}
|
|
12
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { inject } from 'vue';
|
|
2
|
+
import { DirectusKey } from './DirectusProvider';
|
|
3
|
+
export function useVueDirectus() {
|
|
4
|
+
const client = inject(DirectusKey);
|
|
5
|
+
if (!client) {
|
|
6
|
+
throw new Error('Directus client not provided. Wrap your app in <DirectusProvider>.');
|
|
7
|
+
}
|
|
8
|
+
return client;
|
|
9
|
+
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meeovi/directus-client",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "",
|
|
5
|
-
"main": "index.js",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Directus Client Library with auto generating forms and tables, Directus SDK and Types, and vue/react components for Directus Visual Editing.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
6
14
|
"scripts": {
|
|
7
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
15
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
16
|
+
"build": "tsc -p tsconfig.json"
|
|
8
17
|
},
|
|
9
18
|
"keywords": [],
|
|
10
19
|
"author": "",
|
|
11
20
|
"license": "ISC",
|
|
12
|
-
"type": "
|
|
21
|
+
"type": "module",
|
|
13
22
|
"dependencies": {
|
|
14
23
|
"@directus/sdk": "^18.0.3",
|
|
15
24
|
"@directus/types": "^14.0.0",
|
|
16
25
|
"@directus/visual-editing": "^1.1.0",
|
|
17
26
|
"react": "^19.2.3",
|
|
27
|
+
"tsc": "^2.0.4",
|
|
28
|
+
"typescript": "^5.9.3",
|
|
18
29
|
"vue": "^3.5.24"
|
|
19
30
|
},
|
|
20
31
|
"devDependencies": {
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// src/client/createClient.ts
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
createDirectus,
|
|
5
3
|
rest,
|
|
@@ -11,11 +9,13 @@ import {
|
|
|
11
9
|
deleteItem,
|
|
12
10
|
uploadFiles,
|
|
13
11
|
readSingleton,
|
|
14
|
-
readFieldsByCollection
|
|
12
|
+
readFieldsByCollection,
|
|
13
|
+
type DirectusClient
|
|
15
14
|
} from '@directus/sdk';
|
|
16
15
|
|
|
17
16
|
export interface MeeoviDirectusClient<Schema> {
|
|
18
|
-
client:
|
|
17
|
+
client: DirectusClient<Schema>;
|
|
18
|
+
request: any;
|
|
19
19
|
readItem: typeof readItem;
|
|
20
20
|
readItems: typeof readItems;
|
|
21
21
|
createItem: typeof createItem;
|
|
@@ -33,6 +33,7 @@ export function createMeeoviDirectusClient<Schema>(url: string): MeeoviDirectusC
|
|
|
33
33
|
|
|
34
34
|
return {
|
|
35
35
|
client,
|
|
36
|
+
request: client.request,
|
|
36
37
|
readItem,
|
|
37
38
|
readItems,
|
|
38
39
|
createItem,
|
|
@@ -1,89 +1,39 @@
|
|
|
1
1
|
import type { DirectusField } from '../schema/types';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
import { widgetRegistry } from './widget-registry';
|
|
3
|
+
|
|
4
|
+
export interface GeneratedFormField {
|
|
5
|
+
key: string;
|
|
6
|
+
widget: string;
|
|
7
|
+
type: string;
|
|
8
|
+
options?: Record<string, any>;
|
|
9
|
+
fields?: GeneratedFormField[];
|
|
10
|
+
isRepeatable?: boolean;
|
|
11
|
+
isFile?: boolean;
|
|
12
|
+
isRelational?: boolean;
|
|
7
13
|
}
|
|
8
14
|
|
|
9
|
-
export function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
error = null;
|
|
21
|
-
|
|
22
|
-
for (const field of fields) {
|
|
23
|
-
const meta = field;
|
|
24
|
-
if (!meta?.validation) continue;
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
const validation = meta.validation;
|
|
28
|
-
|
|
29
|
-
if (validation._and) {
|
|
30
|
-
for (const rule of validation._and) {
|
|
31
|
-
const fieldName = Object.keys(rule)[0];
|
|
32
|
-
if (!fieldName) continue;
|
|
33
|
-
|
|
34
|
-
const ruleDef = (rule as any)[fieldName];
|
|
35
|
-
|
|
36
|
-
if (ruleDef?._regex) {
|
|
37
|
-
const regex = new RegExp(ruleDef._regex);
|
|
38
|
-
const value = String(form[field.field] ?? '');
|
|
39
|
-
|
|
40
|
-
if (!regex.test(value)) {
|
|
41
|
-
error = meta.validation_message || `${field.field} failed validation`;
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
} catch {
|
|
48
|
-
error = `Validation error for ${field.field}`;
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return true;
|
|
15
|
+
export function generateFormField(field: DirectusField): GeneratedFormField {
|
|
16
|
+
const widget = widgetRegistry[field.interface || 'input'];
|
|
17
|
+
|
|
18
|
+
const base: GeneratedFormField = {
|
|
19
|
+
key: field.field,
|
|
20
|
+
widget: widget.component,
|
|
21
|
+
type: field.type,
|
|
22
|
+
options: field.options || {},
|
|
23
|
+
isRepeatable: widget.isRepeatable,
|
|
24
|
+
isFile: widget.isFile,
|
|
25
|
+
isRelational: widget.isRelational
|
|
54
26
|
};
|
|
55
27
|
|
|
56
|
-
|
|
57
|
-
|
|
28
|
+
if ((field.interface === 'repeater' || field.interface === 'group') && field.options?.fields) {
|
|
29
|
+
base.fields = field.options.fields.map((sub: any) => generateFormField(sub));
|
|
30
|
+
}
|
|
58
31
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
if (result?.error) {
|
|
64
|
-
error = result.error.message;
|
|
65
|
-
return { error, success };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
success = `${collectionName} created successfully`;
|
|
69
|
-
|
|
70
|
-
if (opts?.clearOnSuccess) {
|
|
71
|
-
for (const key of Object.keys(form)) delete form[key];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
opts?.onSuccess?.();
|
|
75
|
-
|
|
76
|
-
return { error, success };
|
|
77
|
-
};
|
|
32
|
+
return base;
|
|
33
|
+
}
|
|
78
34
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return error;
|
|
84
|
-
},
|
|
85
|
-
get success() {
|
|
86
|
-
return success;
|
|
87
|
-
}
|
|
88
|
-
};
|
|
35
|
+
export function generateFormSchema(fields: DirectusField[]): GeneratedFormField[] {
|
|
36
|
+
return fields
|
|
37
|
+
.filter(f => f.interface !== 'presentation' && f.interface !== 'divider')
|
|
38
|
+
.map(generateFormField);
|
|
89
39
|
}
|
|
@@ -1,7 +1,74 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { readItems } from "@directus/sdk";
|
|
2
|
+
|
|
3
|
+
export interface WidgetDefinition {
|
|
4
|
+
component: string;
|
|
5
|
+
props?: Record<string, any>;
|
|
6
|
+
isRepeatable?: boolean;
|
|
7
|
+
isRelational?: boolean;
|
|
8
|
+
isFile?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const widgetRegistry: Record<string, WidgetDefinition> = {
|
|
12
|
+
// Basic inputs
|
|
13
|
+
input: { component: 'TextInput' },
|
|
14
|
+
textarea: { component: 'TextareaInput' },
|
|
15
|
+
boolean: { component: 'ToggleInput' },
|
|
16
|
+
slider: { component: 'SliderInput' },
|
|
17
|
+
color: { component: 'ColorPicker' },
|
|
18
|
+
rating: { component: 'RatingInput' },
|
|
19
|
+
|
|
20
|
+
// Selects
|
|
21
|
+
'select-dropdown': { component: 'SelectInput' },
|
|
22
|
+
'select-multiple-dropdown': { component: 'MultiSelectInput' },
|
|
23
|
+
tags: { component: 'TagInput' },
|
|
4
24
|
checkbox: { component: 'CheckboxInput' },
|
|
25
|
+
radio: { component: 'RadioInput' },
|
|
26
|
+
|
|
27
|
+
// Date/time
|
|
28
|
+
datetime: { component: 'DateTimeInput' },
|
|
5
29
|
date: { component: 'DateInput' },
|
|
6
|
-
|
|
30
|
+
time: { component: 'TimeInput' },
|
|
31
|
+
|
|
32
|
+
// Files
|
|
33
|
+
file: { component: 'FileInput', isFile: true },
|
|
34
|
+
files: { component: 'FilesInput', isFile: true },
|
|
35
|
+
image: { component: 'ImageInput', isFile: true },
|
|
36
|
+
images: { component: 'ImagesInput', isFile: true },
|
|
37
|
+
|
|
38
|
+
// Complex
|
|
39
|
+
repeater: { component: 'RepeaterInput', isRepeatable: true },
|
|
40
|
+
group: { component: 'GroupInput' },
|
|
41
|
+
json: { component: 'JsonEditor' },
|
|
42
|
+
code: { component: 'CodeEditor' },
|
|
43
|
+
wysiwyg: { component: 'WysiwygEditor' },
|
|
44
|
+
markdown: { component: 'MarkdownEditor' },
|
|
45
|
+
|
|
46
|
+
// Directus-specific
|
|
47
|
+
icon: { component: 'IconPicker' },
|
|
48
|
+
user: { component: 'UserSelect' },
|
|
49
|
+
role: { component: 'RoleSelect' },
|
|
50
|
+
translation: { component: 'TranslationInput' },
|
|
51
|
+
|
|
52
|
+
// Presentation (ignored in forms)
|
|
53
|
+
presentation: { component: 'PresentationBlock' },
|
|
54
|
+
divider: { component: 'DividerBlock' }
|
|
7
55
|
};
|
|
56
|
+
|
|
57
|
+
export async function extendWidgetRegistryFromDirectus(client: any) {
|
|
58
|
+
const extensions = await client.request(
|
|
59
|
+
readItems<any, any, any>('directus_extensions')
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
for (const ext of extensions) {
|
|
63
|
+
if (ext.type !== 'interface') continue;
|
|
64
|
+
|
|
65
|
+
const name = ext.name;
|
|
66
|
+
|
|
67
|
+
if (!widgetRegistry[name]) {
|
|
68
|
+
widgetRegistry[name] = {
|
|
69
|
+
component: 'CustomInterfaceRenderer',
|
|
70
|
+
props: { interfaceName: name }
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/react/useDirectus.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { useContext } from 'react';
|
|
|
2
2
|
import { DirectusContext } from './DirectusProvider';
|
|
3
3
|
import type { MeeoviDirectusClient } from '../client/createClient';
|
|
4
4
|
|
|
5
|
-
export function
|
|
5
|
+
export function useReactDirectus<Schema>() {
|
|
6
6
|
const client = useContext(DirectusContext) as MeeoviDirectusClient<Schema> | null;
|
|
7
7
|
|
|
8
8
|
if (!client) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface LivePreviewState {
|
|
2
|
+
token?: string;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface LivePreviewOptions {
|
|
6
|
+
query?: Record<string, string | undefined>;
|
|
7
|
+
initialState?: LivePreviewState;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createLivePreview(options: LivePreviewOptions) {
|
|
11
|
+
const { query = {}, initialState = {} } = options;
|
|
12
|
+
|
|
13
|
+
const shouldEnable =
|
|
14
|
+
Boolean(query.preview) && Boolean(query.token);
|
|
15
|
+
|
|
16
|
+
const state: LivePreviewState = {
|
|
17
|
+
token: query.token || initialState.token,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
enabled: shouldEnable,
|
|
22
|
+
state,
|
|
23
|
+
};
|
|
24
|
+
}
|