@meeovi/directus-client 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 +0 -0
- package/package.json +23 -0
- package/src/client/createClient.ts +45 -0
- package/src/generators/form-engine.ts +89 -0
- package/src/generators/table-engine.ts +11 -0
- package/src/generators/validation-engine.ts +29 -0
- package/src/generators/widget-registry.ts +7 -0
- package/src/index.ts +21 -0
- package/src/react/DirectusProvider.tsx +18 -0
- package/src/react/useDirectus.ts +13 -0
- package/src/schema/introspect.ts +47 -0
- package/src/schema/types.ts +59 -0
- package/src/utils/collections.ts +18 -0
- package/src/utils/fields.ts +52 -0
- package/src/utils/useDirectusField.ts +144 -0
- package/src/utils/useDirectusRequest.ts +32 -0
- package/src/utils/useDirectusSchema.js +9 -0
- package/src/utils/useLivePreview.ts +17 -0
- package/src/utils/useVisualEditing.ts +38 -0
- package/src/vue/DirectusProvider.ts +15 -0
- package/src/vue/useDirectus.ts +13 -0
- package/tsconfig.json +0 -0
package/README.md
ADDED
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@meeovi/directus-client",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [],
|
|
10
|
+
"author": "",
|
|
11
|
+
"license": "ISC",
|
|
12
|
+
"type": "commonjs",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@directus/sdk": "^18.0.3",
|
|
15
|
+
"@directus/types": "^14.0.0",
|
|
16
|
+
"@directus/visual-editing": "^1.1.0",
|
|
17
|
+
"react": "^19.2.3",
|
|
18
|
+
"vue": "^3.5.24"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/react": "^19.2.9"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// src/client/createClient.ts
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createDirectus,
|
|
5
|
+
rest,
|
|
6
|
+
authentication,
|
|
7
|
+
readItem,
|
|
8
|
+
readItems,
|
|
9
|
+
createItem,
|
|
10
|
+
updateItem,
|
|
11
|
+
deleteItem,
|
|
12
|
+
uploadFiles,
|
|
13
|
+
readSingleton,
|
|
14
|
+
readFieldsByCollection
|
|
15
|
+
} from '@directus/sdk';
|
|
16
|
+
|
|
17
|
+
export interface MeeoviDirectusClient<Schema> {
|
|
18
|
+
client: ReturnType<typeof createDirectus<Schema>>;
|
|
19
|
+
readItem: typeof readItem;
|
|
20
|
+
readItems: typeof readItems;
|
|
21
|
+
createItem: typeof createItem;
|
|
22
|
+
updateItem: typeof updateItem;
|
|
23
|
+
deleteItem: typeof deleteItem;
|
|
24
|
+
uploadFiles: typeof uploadFiles;
|
|
25
|
+
readSingleton: typeof readSingleton;
|
|
26
|
+
readFieldsByCollection: typeof readFieldsByCollection;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createMeeoviDirectusClient<Schema>(url: string): MeeoviDirectusClient<Schema> {
|
|
30
|
+
const client = createDirectus<Schema>(url)
|
|
31
|
+
.with(rest())
|
|
32
|
+
.with(authentication());
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
client,
|
|
36
|
+
readItem,
|
|
37
|
+
readItems,
|
|
38
|
+
createItem,
|
|
39
|
+
updateItem,
|
|
40
|
+
deleteItem,
|
|
41
|
+
uploadFiles,
|
|
42
|
+
readSingleton,
|
|
43
|
+
readFieldsByCollection
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { DirectusField } from '../schema/types';
|
|
2
|
+
|
|
3
|
+
export interface FormEngineOptions {
|
|
4
|
+
clearOnSuccess?: boolean;
|
|
5
|
+
onSuccess?: () => void;
|
|
6
|
+
onError?: (msg: string) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createFormEngine(
|
|
10
|
+
collectionName: string,
|
|
11
|
+
fields: DirectusField[],
|
|
12
|
+
directusClient: any,
|
|
13
|
+
opts?: FormEngineOptions
|
|
14
|
+
) {
|
|
15
|
+
const form: Record<string, any> = {};
|
|
16
|
+
let error: string | null = null;
|
|
17
|
+
let success: string | null = null;
|
|
18
|
+
|
|
19
|
+
const validate = () => {
|
|
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;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const submit = async () => {
|
|
57
|
+
if (!validate()) return { error, success };
|
|
58
|
+
|
|
59
|
+
const result = await directusClient.request(
|
|
60
|
+
directusClient.createItem(collectionName, form)
|
|
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
|
+
};
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
form,
|
|
81
|
+
submit,
|
|
82
|
+
get error() {
|
|
83
|
+
return error;
|
|
84
|
+
},
|
|
85
|
+
get success() {
|
|
86
|
+
return success;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DirectusField } from '../schema/types';
|
|
2
|
+
|
|
3
|
+
export function generateTableSchema(fields: DirectusField[]) {
|
|
4
|
+
return fields
|
|
5
|
+
.filter(f => !f.hidden)
|
|
6
|
+
.map(f => ({
|
|
7
|
+
key: f.field,
|
|
8
|
+
label: f.field.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
|
|
9
|
+
type: f.type
|
|
10
|
+
}));
|
|
11
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { DirectusField } from '../schema/types';
|
|
2
|
+
|
|
3
|
+
export function validateField(field: DirectusField, value: any): string | null {
|
|
4
|
+
if (!field.validation) return null;
|
|
5
|
+
|
|
6
|
+
try {
|
|
7
|
+
const validation = field.validation;
|
|
8
|
+
|
|
9
|
+
if (validation._and) {
|
|
10
|
+
for (const rule of validation._and) {
|
|
11
|
+
const fieldName = Object.keys(rule)[0];
|
|
12
|
+
if (!fieldName) continue;
|
|
13
|
+
|
|
14
|
+
const ruleDef = (rule as any)[fieldName];
|
|
15
|
+
|
|
16
|
+
if (ruleDef?._regex) {
|
|
17
|
+
const regex = new RegExp(ruleDef._regex);
|
|
18
|
+
if (!regex.test(String(value ?? ''))) {
|
|
19
|
+
return field.validation_message || `${field.field} failed validation`;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
return `Validation error for ${field.field}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return null;
|
|
29
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
|
|
3
|
+
export * from './client/createClient';
|
|
4
|
+
|
|
5
|
+
// Vue bindings
|
|
6
|
+
export { default as DirectusVueProvider } from './vue/DirectusProvider';
|
|
7
|
+
export * from './vue/useDirectus';
|
|
8
|
+
|
|
9
|
+
// React bindings
|
|
10
|
+
export { DirectusProvider as DirectusReactProvider } from './react/DirectusProvider';
|
|
11
|
+
export * from './react/useDirectus';
|
|
12
|
+
|
|
13
|
+
// Schema + generators + utils
|
|
14
|
+
export * from './schema/types';
|
|
15
|
+
export * from './schema/introspect';
|
|
16
|
+
export * from './utils/collections';
|
|
17
|
+
export * from './utils/fields';
|
|
18
|
+
export * from './generators/form-engine';
|
|
19
|
+
export * from './generators/table-engine';
|
|
20
|
+
export * from './generators/validation-engine';
|
|
21
|
+
export * from './generators/widget-registry';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React, { createContext } from 'react';
|
|
2
|
+
import type { MeeoviDirectusClient } from '../client/createClient';
|
|
3
|
+
|
|
4
|
+
export const DirectusContext = createContext<MeeoviDirectusClient<any> | null>(null);
|
|
5
|
+
|
|
6
|
+
export function DirectusProvider({
|
|
7
|
+
client,
|
|
8
|
+
children
|
|
9
|
+
}: {
|
|
10
|
+
client: MeeoviDirectusClient<any>;
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
}) {
|
|
13
|
+
return (
|
|
14
|
+
<DirectusContext.Provider value={client}>
|
|
15
|
+
{children}
|
|
16
|
+
</DirectusContext.Provider>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { DirectusContext } from './DirectusProvider';
|
|
3
|
+
import type { MeeoviDirectusClient } from '../client/createClient';
|
|
4
|
+
|
|
5
|
+
export function useRDirectus<Schema>() {
|
|
6
|
+
const client = useContext(DirectusContext) as MeeoviDirectusClient<Schema> | null;
|
|
7
|
+
|
|
8
|
+
if (!client) {
|
|
9
|
+
throw new Error('Directus client not provided. Wrap your app in <DirectusProvider>.');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return client;
|
|
13
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// schema/introspect.ts
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
readFieldsByCollection,
|
|
5
|
+
readItems
|
|
6
|
+
} from '@directus/sdk';
|
|
7
|
+
import type {
|
|
8
|
+
DirectusSchema,
|
|
9
|
+
DirectusField,
|
|
10
|
+
DirectusCollection,
|
|
11
|
+
DirectusRelation,
|
|
12
|
+
} from './types';
|
|
13
|
+
|
|
14
|
+
export async function introspectCollections(client: any): Promise<DirectusCollection[]> {
|
|
15
|
+
return await client.request((readItems as any)('directus_collection'));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function introspectFields(client: any, collection: string): Promise<DirectusField[]> {
|
|
19
|
+
return await client.request((readFieldsByCollection as any)(collection));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function introspectRelations(client: any): Promise<DirectusRelation[]> {
|
|
23
|
+
return await client.request((readItems as any)('directus_relations'));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function introspectSchema(client: any): Promise<DirectusSchema> {
|
|
27
|
+
const [collections, relations] = await Promise.all([
|
|
28
|
+
introspectCollections(client),
|
|
29
|
+
introspectRelations(client),
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const fields: DirectusField[] = [];
|
|
33
|
+
|
|
34
|
+
for (const col of collections) {
|
|
35
|
+
const colFields = await introspectFields(client, col.collection);
|
|
36
|
+
fields.push(...colFields);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
collections,
|
|
41
|
+
fields,
|
|
42
|
+
relations,
|
|
43
|
+
directus_collections: collections,
|
|
44
|
+
directus_fields: fields,
|
|
45
|
+
directus_relations: relations,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface DirectusField {
|
|
2
|
+
collection: string;
|
|
3
|
+
field: string;
|
|
4
|
+
type: string;
|
|
5
|
+
interface ? : string;
|
|
6
|
+
options ? : Record < string,
|
|
7
|
+
any > ;
|
|
8
|
+
required ? : boolean;
|
|
9
|
+
readonly ? : boolean;
|
|
10
|
+
hidden ? : boolean;
|
|
11
|
+
sort ? : number;
|
|
12
|
+
special ? : string[];
|
|
13
|
+
validation ? : Record < string,
|
|
14
|
+
any > ;
|
|
15
|
+
validation_message ? : string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DirectusRelation {
|
|
19
|
+
collection: string;
|
|
20
|
+
field: string;
|
|
21
|
+
related_collection: string | null;
|
|
22
|
+
meta ? : Record < string,
|
|
23
|
+
any > ;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DirectusCollection {
|
|
27
|
+
collection: string;
|
|
28
|
+
meta ? : Record < string,
|
|
29
|
+
any > ;
|
|
30
|
+
schema ? : Record < string,
|
|
31
|
+
any > ;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DirectusSchema {
|
|
35
|
+
directus_collections: DirectusCollection[];
|
|
36
|
+
directus_relations: DirectusRelation[];
|
|
37
|
+
directus_fields: DirectusField[];
|
|
38
|
+
collections: DirectusCollection[];
|
|
39
|
+
fields: DirectusField[];
|
|
40
|
+
relations: DirectusRelation[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface GeneratedFieldSchema {
|
|
44
|
+
key: string;
|
|
45
|
+
type: string;
|
|
46
|
+
widget: string;
|
|
47
|
+
required: boolean;
|
|
48
|
+
readonly: boolean;
|
|
49
|
+
hidden: boolean;
|
|
50
|
+
options ? : Record < string,
|
|
51
|
+
any > ;
|
|
52
|
+
validation ? : Record < string,
|
|
53
|
+
any > ;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface GeneratedCollectionSchema {
|
|
57
|
+
collection: string;
|
|
58
|
+
fields: GeneratedFieldSchema[];
|
|
59
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DirectusSchema, DirectusCollection, DirectusField } from '../schema/types';
|
|
2
|
+
|
|
3
|
+
export function getCollection(schema: DirectusSchema, name: string): DirectusCollection | undefined {
|
|
4
|
+
return schema.collections.find(c => c.collection === name);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getCollectionFields(schema: DirectusSchema, name: string): DirectusField[] {
|
|
8
|
+
return schema.fields.filter(f => f.collection === name);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function listCollections(schema: DirectusSchema): string[] {
|
|
12
|
+
return schema.collections.map(c => c.collection);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isSingleton(schema: DirectusSchema, name: string): boolean {
|
|
16
|
+
const col = getCollection(schema, name);
|
|
17
|
+
return col?.meta?.singleton === true;
|
|
18
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DirectusField,
|
|
3
|
+
GeneratedFieldSchema,
|
|
4
|
+
} from '../schema/types';
|
|
5
|
+
|
|
6
|
+
export function mapFieldToWidget(field: DirectusField): string {
|
|
7
|
+
if (field.interface) return field.interface;
|
|
8
|
+
|
|
9
|
+
switch (field.type) {
|
|
10
|
+
case 'string':
|
|
11
|
+
case 'text':
|
|
12
|
+
return 'text';
|
|
13
|
+
|
|
14
|
+
case 'integer':
|
|
15
|
+
case 'bigInteger':
|
|
16
|
+
case 'float':
|
|
17
|
+
case 'decimal':
|
|
18
|
+
return 'number';
|
|
19
|
+
|
|
20
|
+
case 'boolean':
|
|
21
|
+
return 'checkbox';
|
|
22
|
+
|
|
23
|
+
case 'date':
|
|
24
|
+
case 'dateTime':
|
|
25
|
+
return 'date';
|
|
26
|
+
|
|
27
|
+
case 'json':
|
|
28
|
+
return 'json';
|
|
29
|
+
|
|
30
|
+
default:
|
|
31
|
+
return 'text';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function generateFieldSchema(field: DirectusField): GeneratedFieldSchema {
|
|
36
|
+
return {
|
|
37
|
+
key: field.field,
|
|
38
|
+
type: field.type,
|
|
39
|
+
widget: mapFieldToWidget(field),
|
|
40
|
+
required: field.required ?? false,
|
|
41
|
+
readonly: field.readonly ?? false,
|
|
42
|
+
hidden: field.hidden ?? false,
|
|
43
|
+
options: field.options ?? {},
|
|
44
|
+
validation: field.validation ?? {},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function generateFieldsSchema(fields: DirectusField[]): GeneratedFieldSchema[] {
|
|
49
|
+
return fields
|
|
50
|
+
.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0))
|
|
51
|
+
.map(generateFieldSchema);
|
|
52
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { ref, computed, watch } from "vue"
|
|
2
|
+
import Input from "~/components/ui/forms/TextInput.vue"
|
|
3
|
+
import TextArea from "~/components/ui/forms/TextArea.vue"
|
|
4
|
+
import Select from "~/components/ui/forms/SelectInput.vue"
|
|
5
|
+
import DateTime from "~/components/ui/forms/DateTime.vue"
|
|
6
|
+
import FileInput from "~/components/ui/forms/FileInput.vue"
|
|
7
|
+
import BooleanInput from "~/components/ui/forms/BooleanInput.vue"
|
|
8
|
+
import RelationSelect from "~/components/ui/forms/RelationSelect.vue"
|
|
9
|
+
import RepeaterInput from "~/components/ui/forms/RepeaterInput.vue"
|
|
10
|
+
import TiptapEditor from "~/components/ui/forms/TiptapEditor.vue"
|
|
11
|
+
|
|
12
|
+
export function useDirectusField(field: any, modelValue: any, emit: any, formContext?: any) {
|
|
13
|
+
const internalValue = computed({
|
|
14
|
+
get: () => modelValue,
|
|
15
|
+
set: (v) => emit("update:modelValue", v),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const previousAuto = ref<string | null>(null)
|
|
19
|
+
const internalAuto = computed(() => {
|
|
20
|
+
return previousAuto.value != null && internalValue.value === previousAuto.value
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const slugify = (val: string) =>
|
|
24
|
+
String(val || "")
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.normalize("NFKD")
|
|
27
|
+
.replace(/\s+/g, "-")
|
|
28
|
+
.replace(/[^a-z0-9\-]/g, "")
|
|
29
|
+
.replace(/-+/g, "-")
|
|
30
|
+
.replace(/^-|-$/g, "")
|
|
31
|
+
|
|
32
|
+
const isAliasField = computed(() => {
|
|
33
|
+
const meta = field?.meta ?? {}
|
|
34
|
+
return (
|
|
35
|
+
meta.interface === "alias" ||
|
|
36
|
+
(meta.options && (meta.options.source || meta.options.from))
|
|
37
|
+
)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
if (isAliasField.value && formContext && formContext.form) {
|
|
41
|
+
const meta = field?.meta ?? {}
|
|
42
|
+
let sourceField: string | null = meta?.options?.source ?? meta?.options?.from ?? null
|
|
43
|
+
|
|
44
|
+
if (!sourceField && formContext.fields) {
|
|
45
|
+
const candidates = ["name", "title"]
|
|
46
|
+
sourceField =
|
|
47
|
+
candidates.find((c: string) =>
|
|
48
|
+
formContext.fields.value.some((f: any) => f.field === c)
|
|
49
|
+
) ?? null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (sourceField) {
|
|
53
|
+
watch(
|
|
54
|
+
() => formContext.form.value?.[sourceField as string],
|
|
55
|
+
(newVal) => {
|
|
56
|
+
const target = field.field
|
|
57
|
+
const current = formContext.form.value?.[target] ?? ""
|
|
58
|
+
const generated = slugify(String(newVal ?? ""))
|
|
59
|
+
|
|
60
|
+
if (!generated) return
|
|
61
|
+
|
|
62
|
+
// Case 1: Empty or previously auto‑generated → regenerate
|
|
63
|
+
if (!current || current === previousAuto.value) {
|
|
64
|
+
formContext.form.value[target] = generated
|
|
65
|
+
internalValue.value = generated
|
|
66
|
+
previousAuto.value = generated
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{ immediate: true }
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
// Watch the target itself: if cleared, regenerate
|
|
73
|
+
watch(
|
|
74
|
+
() => formContext.form.value?.[field.field],
|
|
75
|
+
(newVal) => {
|
|
76
|
+
if (!newVal) {
|
|
77
|
+
const sourceVal = formContext.form.value?.[sourceField as string] ?? ""
|
|
78
|
+
const regenerated = slugify(String(sourceVal))
|
|
79
|
+
if (regenerated) {
|
|
80
|
+
formContext.form.value[field.field] = regenerated
|
|
81
|
+
internalValue.value = regenerated
|
|
82
|
+
previousAuto.value = regenerated
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// --- Label prettifier ---
|
|
91
|
+
const prettify = (s: string) =>
|
|
92
|
+
s.replace(/_/g, " ")
|
|
93
|
+
.split(" ")
|
|
94
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
95
|
+
.join(" ")
|
|
96
|
+
|
|
97
|
+
const label = computed(() => {
|
|
98
|
+
const raw = field?.meta?.field ?? field?.field ?? ""
|
|
99
|
+
return prettify(raw)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// --- Field lookup ---
|
|
103
|
+
const fieldLookup = computed(() => {
|
|
104
|
+
const iface = field?.meta?.interface
|
|
105
|
+
const name = String(iface ?? "").toLowerCase()
|
|
106
|
+
|
|
107
|
+
if (["input", "string"].includes(name)) return Input
|
|
108
|
+
if (
|
|
109
|
+
["textarea", "wysiwyg", "rich-text", "input-multiline", "input-rich-text", "input-rich-text-md"].includes(name)
|
|
110
|
+
)
|
|
111
|
+
return TiptapEditor
|
|
112
|
+
if (["select", "select-dropdown", "select-multiple"].includes(name)) return Select
|
|
113
|
+
if (name === "datetime") return DateTime
|
|
114
|
+
if (["file", "files", "file-image"].includes(name)) return FileInput
|
|
115
|
+
if (name === "boolean") return BooleanInput
|
|
116
|
+
if (name.includes("many") || name.includes("one") || name.includes("to")) return RelationSelect
|
|
117
|
+
return Input
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const required = computed(() => {
|
|
121
|
+
const meta = field?.meta ?? {}
|
|
122
|
+
return Boolean(meta?.validation?.required ?? meta?.required ?? false)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const safeOptions = computed(() => field?.meta?.options ?? null)
|
|
126
|
+
const defaultValue = computed(() => field?.schema?.default_value ?? null)
|
|
127
|
+
const isMultiple = computed(() => {
|
|
128
|
+
const name = String(field?.meta?.interface ?? "").toLowerCase()
|
|
129
|
+
if (name === "select-multiple") return true
|
|
130
|
+
if (name.includes("many")) return true
|
|
131
|
+
return Boolean(field?.meta?.options?.multiple || false)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
internalValue,
|
|
136
|
+
internalAuto,
|
|
137
|
+
label,
|
|
138
|
+
fieldLookup,
|
|
139
|
+
required,
|
|
140
|
+
safeOptions,
|
|
141
|
+
defaultValue,
|
|
142
|
+
isMultiple,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useNuxtApp } from '#imports'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Centralized safe wrapper around `$directus.request`.
|
|
5
|
+
* Shows a toast error when the Directus client or `.request` is not available.
|
|
6
|
+
*/
|
|
7
|
+
export default function useDirectusRequest() {
|
|
8
|
+
const nuxt = useNuxtApp() as any
|
|
9
|
+
|
|
10
|
+
async function request(config: any) {
|
|
11
|
+
try {
|
|
12
|
+
const client = nuxt?.$directus
|
|
13
|
+
if (!client || typeof client.request !== 'function') {
|
|
14
|
+
try {
|
|
15
|
+
const toast = nuxt?.$toast
|
|
16
|
+
if (toast && typeof toast.error === 'function') toast.error('Directus client unavailable')
|
|
17
|
+
} catch (_) {}
|
|
18
|
+
throw new Error('Directus client.request is not available')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return await client.request(config)
|
|
22
|
+
} catch (e) {
|
|
23
|
+
try {
|
|
24
|
+
const toast = nuxt?.$toast
|
|
25
|
+
if (toast && typeof toast.error === 'function') toast.error('Request failed')
|
|
26
|
+
} catch (_) {}
|
|
27
|
+
throw e
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { request }
|
|
32
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
|
|
9
|
+
// Store the token from the URL for use in API calls
|
|
10
|
+
getState: (currentState) => {
|
|
11
|
+
const route = useRoute();
|
|
12
|
+
return {
|
|
13
|
+
token: route.query.token || currentState.token,
|
|
14
|
+
};
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { apply as applyVisualEditing, setAttr } from '@directus/visual-editing';
|
|
2
|
+
import type { PrimaryKey } from '@directus/types';
|
|
3
|
+
|
|
4
|
+
interface ApplyOptions {
|
|
5
|
+
directusUrl: string;
|
|
6
|
+
elements?: HTMLElement[] | HTMLElement;
|
|
7
|
+
onSaved?: (data: { collection?: string; item?: PrimaryKey | null; payload?: Record<string, any> }) => void;
|
|
8
|
+
customClass?: string;
|
|
9
|
+
}
|
|
10
|
+
export default function useVisualEditing() {
|
|
11
|
+
// Use useState for state that persists across navigation
|
|
12
|
+
const isVisualEditingEnabled = useState('visual-editing-enabled', () => false);
|
|
13
|
+
const route = useRoute();
|
|
14
|
+
const {
|
|
15
|
+
public: { enableVisualEditing, directusUrl },
|
|
16
|
+
} = useRuntimeConfig();
|
|
17
|
+
|
|
18
|
+
// Check query param on composable initialization.
|
|
19
|
+
if (route.query['visual-editing'] === 'true' && enableVisualEditing) {
|
|
20
|
+
isVisualEditingEnabled.value = true;
|
|
21
|
+
} else if (route.query['visual-editing'] === 'false') {
|
|
22
|
+
isVisualEditingEnabled.value = false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const apply = (options: Pick<ApplyOptions, 'elements' | 'onSaved' | 'customClass'>) => {
|
|
26
|
+
if (!isVisualEditingEnabled.value) return;
|
|
27
|
+
applyVisualEditing({
|
|
28
|
+
...options,
|
|
29
|
+
directusUrl,
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
isVisualEditingEnabled,
|
|
35
|
+
apply,
|
|
36
|
+
setAttr,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineComponent, provide } from 'vue';
|
|
2
|
+
import type { MeeoviDirectusClient } from '../client/createClient';
|
|
3
|
+
|
|
4
|
+
export const DirectusKey = Symbol('DirectusClient');
|
|
5
|
+
|
|
6
|
+
export default defineComponent({
|
|
7
|
+
name: 'DirectusProvider',
|
|
8
|
+
props: {
|
|
9
|
+
client: { type: Object, required: true }
|
|
10
|
+
},
|
|
11
|
+
setup(props, { slots }) {
|
|
12
|
+
provide(DirectusKey, props.client as MeeoviDirectusClient<any>);
|
|
13
|
+
return () => slots.default?.();
|
|
14
|
+
}
|
|
15
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { inject } from 'vue';
|
|
2
|
+
import { DirectusKey } from './DirectusProvider';
|
|
3
|
+
import type { MeeoviDirectusClient } from '../client/createClient';
|
|
4
|
+
|
|
5
|
+
export function useVDirectus<Schema>() {
|
|
6
|
+
const client = inject<MeeoviDirectusClient<Schema>>(DirectusKey);
|
|
7
|
+
|
|
8
|
+
if (!client) {
|
|
9
|
+
throw new Error('Directus client not provided. Wrap your app in <DirectusProvider>.');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return client;
|
|
13
|
+
}
|
package/tsconfig.json
ADDED
|
File without changes
|