@whatworks/payload-select-search-field 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -6
- package/dist/endpoint.js +111 -0
- package/dist/endpoint.js.map +1 -0
- package/dist/endpointName.js +3 -0
- package/dist/endpointName.js.map +1 -0
- package/dist/exports/client.js +3 -0
- package/dist/exports/client.js.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.js +12 -0
- package/dist/plugin.js.map +1 -0
- package/dist/selectSearchField.js +20 -0
- package/dist/selectSearchField.js.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/SelectSearchField.js +154 -0
- package/dist/ui/SelectSearchField.js.map +1 -0
- package/package.json +21 -11
- package/dist/tsconfig.tsbuildinfo +0 -1
- /package/dist/{src/endpoint.d.ts → endpoint.d.ts} +0 -0
- /package/dist/{src/endpointName.d.ts → endpointName.d.ts} +0 -0
- /package/dist/{src/exports → exports}/client.d.ts +0 -0
- /package/dist/{src/index.d.ts → index.d.ts} +0 -0
- /package/dist/{src/plugin.d.ts → plugin.d.ts} +0 -0
- /package/dist/{src/selectSearchField.d.ts → selectSearchField.d.ts} +0 -0
- /package/dist/{src/types.d.ts → types.d.ts} +0 -0
- /package/dist/{src/ui → ui}/SelectSearchField.d.ts +0 -0
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ export default buildConfig({
|
|
|
15
15
|
})
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
Add a field with `selectSearch` (recommended):
|
|
18
|
+
Add a field with `selectSearch` (recommended):
|
|
19
19
|
|
|
20
20
|
```ts
|
|
21
21
|
import { selectSearch } from '@whatworks/payload-select-search-field'
|
|
@@ -32,11 +32,6 @@ selectSearch({
|
|
|
32
32
|
})),
|
|
33
33
|
]
|
|
34
34
|
},
|
|
35
|
-
admin: {
|
|
36
|
-
components: {
|
|
37
|
-
Field: '@whatworks/payload-select-search-field/client#SelectSearchField',
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
35
|
})
|
|
41
36
|
```
|
|
42
37
|
|
package/dist/endpoint.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { getFieldByPath } from 'payload';
|
|
2
|
+
import { selectSearchEndpoint } from './endpointName.js';
|
|
3
|
+
const maxQueryLength = 200;
|
|
4
|
+
const parseBody = async (req)=>{
|
|
5
|
+
if (typeof req.json === 'function') {
|
|
6
|
+
return await req.json();
|
|
7
|
+
}
|
|
8
|
+
if (req.body && typeof req.body === 'object') {
|
|
9
|
+
return req.body;
|
|
10
|
+
}
|
|
11
|
+
return {};
|
|
12
|
+
};
|
|
13
|
+
export const selectSearchEndpointHandler = ()=>({
|
|
14
|
+
method: 'post',
|
|
15
|
+
path: selectSearchEndpoint,
|
|
16
|
+
handler: async (req)=>{
|
|
17
|
+
if (!req.user) {
|
|
18
|
+
return Response.json({
|
|
19
|
+
error: 'Unauthorized'
|
|
20
|
+
}, {
|
|
21
|
+
status: 401
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
let body;
|
|
25
|
+
try {
|
|
26
|
+
body = await parseBody(req);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return Response.json({
|
|
29
|
+
error: 'Invalid JSON body'
|
|
30
|
+
}, {
|
|
31
|
+
status: 400
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const { entityType, slug, schemaPath } = body;
|
|
35
|
+
if (entityType !== 'collection' && entityType !== 'global') {
|
|
36
|
+
return Response.json({
|
|
37
|
+
error: 'Invalid entityType'
|
|
38
|
+
}, {
|
|
39
|
+
status: 400
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (!slug || !schemaPath) {
|
|
43
|
+
return Response.json({
|
|
44
|
+
error: 'Missing slug or schemaPath'
|
|
45
|
+
}, {
|
|
46
|
+
status: 400
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const safeQuery = String(body.query || '').slice(0, maxQueryLength);
|
|
50
|
+
const selectedValues = Array.isArray(body.selectedValues) ? body.selectedValues.map((value)=>String(value)) : [];
|
|
51
|
+
const config = req.payload.config;
|
|
52
|
+
const entityConfig = entityType === 'collection' ? config.collections?.find((collection)=>collection.slug === slug) : config.globals?.find((global)=>global.slug === slug);
|
|
53
|
+
if (!entityConfig) {
|
|
54
|
+
return Response.json({
|
|
55
|
+
error: 'Unknown entity'
|
|
56
|
+
}, {
|
|
57
|
+
status: 404
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const fields = entityConfig.flattenedFields;
|
|
61
|
+
if (!Array.isArray(fields)) {
|
|
62
|
+
return Response.json({
|
|
63
|
+
error: 'Fields not searchable'
|
|
64
|
+
}, {
|
|
65
|
+
status: 400
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const fieldResult = getFieldByPath({
|
|
69
|
+
fields,
|
|
70
|
+
path: schemaPath.split('.').slice(1).join('.')
|
|
71
|
+
});
|
|
72
|
+
if (!fieldResult) {
|
|
73
|
+
return Response.json({
|
|
74
|
+
error: 'Field not found'
|
|
75
|
+
}, {
|
|
76
|
+
status: 400
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
const searchFunction = fieldResult?.field?.custom?.searchFunction;
|
|
80
|
+
if (typeof searchFunction !== 'function') {
|
|
81
|
+
return Response.json({
|
|
82
|
+
error: 'Field not searchable'
|
|
83
|
+
}, {
|
|
84
|
+
status: 400
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const collectionConfig = entityType === 'collection' ? entityConfig : undefined;
|
|
88
|
+
const globalConfig = entityType === 'global' ? entityConfig : undefined;
|
|
89
|
+
const options = await searchFunction({
|
|
90
|
+
req,
|
|
91
|
+
query: safeQuery,
|
|
92
|
+
selectedValues,
|
|
93
|
+
field: fieldResult.field,
|
|
94
|
+
collection: collectionConfig,
|
|
95
|
+
global: globalConfig
|
|
96
|
+
});
|
|
97
|
+
if (!Array.isArray(options)) {
|
|
98
|
+
return Response.json({
|
|
99
|
+
error: 'Invalid searchFunction response'
|
|
100
|
+
}, {
|
|
101
|
+
status: 500
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const res = {
|
|
105
|
+
options
|
|
106
|
+
};
|
|
107
|
+
return Response.json(res);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
//# sourceMappingURL=endpoint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/endpoint.ts"],"sourcesContent":["import type {\n Endpoint,\n PayloadRequest,\n SanitizedCollectionConfig,\n SanitizedGlobalConfig,\n} from 'payload'\nimport { getFieldByPath } from 'payload'\nimport type { SelectSearchRequest, SelectSearchResponse, SelectSearchFunction } from './types.js'\nimport { selectSearchEndpoint } from './endpointName.js'\n\nconst maxQueryLength = 200\n\nconst parseBody = async (req: PayloadRequest): Promise<Partial<SelectSearchRequest>> => {\n if (typeof req.json === 'function') {\n return (await req.json()) as Partial<SelectSearchRequest>\n }\n\n if (req.body && typeof req.body === 'object') {\n return req.body as Partial<SelectSearchRequest>\n }\n\n return {}\n}\n\nexport const selectSearchEndpointHandler = (): Endpoint => ({\n method: 'post',\n path: selectSearchEndpoint,\n handler: async (req: PayloadRequest) => {\n if (!req.user) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let body: Partial<SelectSearchRequest>\n try {\n body = await parseBody(req)\n } catch (error) {\n return Response.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n\n const { entityType, slug, schemaPath } = body\n if (entityType !== 'collection' && entityType !== 'global') {\n return Response.json({ error: 'Invalid entityType' }, { status: 400 })\n }\n\n if (!slug || !schemaPath) {\n return Response.json({ error: 'Missing slug or schemaPath' }, { status: 400 })\n }\n\n const safeQuery = String(body.query || '').slice(0, maxQueryLength)\n const selectedValues = Array.isArray(body.selectedValues)\n ? body.selectedValues.map((value) => String(value))\n : []\n\n const config = req.payload.config\n const entityConfig =\n entityType === 'collection'\n ? config.collections?.find((collection) => collection.slug === slug)\n : config.globals?.find((global) => global.slug === slug)\n\n if (!entityConfig) {\n return Response.json({ error: 'Unknown entity' }, { status: 404 })\n }\n\n const fields = entityConfig.flattenedFields\n if (!Array.isArray(fields)) {\n return Response.json({ error: 'Fields not searchable' }, { status: 400 })\n }\n\n const fieldResult = getFieldByPath({\n fields,\n path: schemaPath.split('.').slice(1).join('.'),\n })\n\n if (!fieldResult) {\n return Response.json({ error: 'Field not found' }, { status: 400 })\n }\n\n const searchFunction = fieldResult?.field?.custom?.searchFunction as\n | SelectSearchFunction\n | undefined\n\n if (typeof searchFunction !== 'function') {\n return Response.json({ error: 'Field not searchable' }, { status: 400 })\n }\n\n const collectionConfig =\n entityType === 'collection' ? (entityConfig as SanitizedCollectionConfig) : undefined\n const globalConfig =\n entityType === 'global' ? (entityConfig as SanitizedGlobalConfig) : undefined\n\n const options = await searchFunction({\n req,\n query: safeQuery,\n selectedValues,\n field: fieldResult.field,\n collection: collectionConfig,\n global: globalConfig,\n })\n\n if (!Array.isArray(options)) {\n return Response.json({ error: 'Invalid searchFunction response' }, { status: 500 })\n }\n\n const res: SelectSearchResponse = {\n options,\n }\n\n return Response.json(res)\n },\n})\n"],"names":["getFieldByPath","selectSearchEndpoint","maxQueryLength","parseBody","req","json","body","selectSearchEndpointHandler","method","path","handler","user","Response","error","status","entityType","slug","schemaPath","safeQuery","String","query","slice","selectedValues","Array","isArray","map","value","config","payload","entityConfig","collections","find","collection","globals","global","fields","flattenedFields","fieldResult","split","join","searchFunction","field","custom","collectionConfig","undefined","globalConfig","options","res"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/endpointName.ts"],"sourcesContent":["export const selectSearchEndpoint = '/select-search'\n"],"names":["selectSearchEndpoint"],"mappings":"AAAA,OAAO,MAAMA,uBAAuB,iBAAgB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["export { SelectSearchField } from '../ui/SelectSearchField.js'\n"],"names":["SelectSearchField"],"mappings":"AAAA,SAASA,iBAAiB,QAAQ,6BAA4B"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { selectSearchPlugin as selectSearchPlugin } from './plugin.js';
|
|
2
|
+
export { selectSearchEndpoint } from './endpointName.js';
|
|
3
|
+
export { selectSearchEndpointHandler } from './endpoint.js';
|
|
4
|
+
export { selectSearchField } from './selectSearchField.js';
|
|
5
|
+
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { selectSearchPlugin as selectSearchPlugin } from './plugin.js'\nexport { selectSearchEndpoint } from './endpointName.js'\nexport { selectSearchEndpointHandler } from './endpoint.js'\nexport { selectSearchField } from './selectSearchField.js'\nexport type {\n SelectSearchFunction,\n SelectSearchFunctionArgs,\n SelectSearchOption,\n SelectSearchRequest,\n SelectSearchResponse,\n} from './types.js'\n"],"names":["selectSearchPlugin","selectSearchEndpoint","selectSearchEndpointHandler","selectSearchField"],"mappings":"AAAA,SAASA,sBAAsBA,kBAAkB,QAAQ,cAAa;AACtE,SAASC,oBAAoB,QAAQ,oBAAmB;AACxD,SAASC,2BAA2B,QAAQ,gBAAe;AAC3D,SAASC,iBAAiB,QAAQ,yBAAwB"}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { selectSearchEndpointHandler } from './endpoint.js';
|
|
2
|
+
export const selectSearchPlugin = ()=>{
|
|
3
|
+
return async (config)=>{
|
|
4
|
+
config.endpoints = [
|
|
5
|
+
...config.endpoints || [],
|
|
6
|
+
selectSearchEndpointHandler()
|
|
7
|
+
];
|
|
8
|
+
return config;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { Plugin } from 'payload'\nimport { selectSearchEndpointHandler } from './endpoint.js'\n\nexport const selectSearchPlugin = (): Plugin => {\n return async (config) => {\n config.endpoints = [...(config.endpoints || []), selectSearchEndpointHandler()]\n return config\n }\n}\n"],"names":["selectSearchEndpointHandler","selectSearchPlugin","config","endpoints"],"mappings":"AACA,SAASA,2BAA2B,QAAQ,gBAAe;AAE3D,OAAO,MAAMC,qBAAqB;IAChC,OAAO,OAAOC;QACZA,OAAOC,SAAS,GAAG;eAAKD,OAAOC,SAAS,IAAI,EAAE;YAAGH;SAA8B;QAC/E,OAAOE;IACT;AACF,EAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const selectSearchField = (args)=>{
|
|
2
|
+
const { searchFunction, ...rest } = args;
|
|
3
|
+
return {
|
|
4
|
+
...rest,
|
|
5
|
+
type: 'text',
|
|
6
|
+
custom: {
|
|
7
|
+
...args.custom,
|
|
8
|
+
searchFunction: args.searchFunction
|
|
9
|
+
},
|
|
10
|
+
admin: {
|
|
11
|
+
...args.admin,
|
|
12
|
+
components: {
|
|
13
|
+
...args.admin?.components,
|
|
14
|
+
Field: args.admin?.components?.Field ?? '@whatworks/payload-select-search-field/client#SelectSearchField'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
//# sourceMappingURL=selectSearchField.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/selectSearchField.ts"],"sourcesContent":["import type { Field, TextField } from 'payload'\nimport type { SelectSearchFunction } from './types.js'\n\nexport type SelectSearchFieldArgs = Omit<TextField, 'admin' | 'custom' | 'type' | 'hasMany'> & {\n hasMany?: boolean\n type?: 'text'\n searchFunction: SelectSearchFunction\n custom?: Record<string, unknown>\n admin?: TextField['admin']\n}\n\nexport const selectSearchField = (args: SelectSearchFieldArgs): Field => {\n const { searchFunction, ...rest } = args\n return {\n ...rest,\n type: 'text',\n custom: {\n ...args.custom,\n searchFunction: args.searchFunction,\n },\n admin: {\n ...args.admin,\n components: {\n ...args.admin?.components,\n Field:\n args.admin?.components?.Field ??\n '@whatworks/payload-select-search-field/client#SelectSearchField',\n },\n },\n } as Field\n}\n"],"names":["selectSearchField","args","searchFunction","rest","type","custom","admin","components","Field"],"mappings":"AAWA,OAAO,MAAMA,oBAAoB,CAACC;IAChC,MAAM,EAAEC,cAAc,EAAE,GAAGC,MAAM,GAAGF;IACpC,OAAO;QACL,GAAGE,IAAI;QACPC,MAAM;QACNC,QAAQ;YACN,GAAGJ,KAAKI,MAAM;YACdH,gBAAgBD,KAAKC,cAAc;QACrC;QACAI,OAAO;YACL,GAAGL,KAAKK,KAAK;YACbC,YAAY;gBACV,GAAGN,KAAKK,KAAK,EAAEC,UAAU;gBACzBC,OACEP,KAAKK,KAAK,EAAEC,YAAYC,SACxB;YACJ;QACF;IACF;AACF,EAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type {\n FlattenedField,\n PayloadRequest,\n SanitizedCollectionConfig,\n SanitizedGlobalConfig,\n} from 'payload'\n\nexport type SelectSearchOption = {\n label: string\n value: string\n [key: string]: unknown\n}\n\nexport type SelectSearchFunctionArgs = {\n req: PayloadRequest\n query: string\n selectedValues: string[]\n field: FlattenedField\n collection?: SanitizedCollectionConfig\n global?: SanitizedGlobalConfig\n}\n\nexport type SelectSearchFunction = (\n args: SelectSearchFunctionArgs,\n) => Promise<SelectSearchOption[]> | SelectSearchOption[]\n\nexport type SelectSearchRequest = {\n entityType: 'collection' | 'global'\n slug: string\n schemaPath: string\n query?: string\n selectedValues?: string[]\n}\n\nexport type SelectSearchResponse = {\n options: SelectSearchOption[]\n}\n"],"names":[],"mappings":"AAkCA,WAEC"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { SelectInput, useConfig, useDocumentInfo, useField } from '@payloadcms/ui';
|
|
4
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
|
+
import { selectSearchEndpoint } from '../endpointName.js';
|
|
6
|
+
const debounceMs = 300;
|
|
7
|
+
export const SelectSearchField = (props)=>{
|
|
8
|
+
const { field, path, schemaPath: schemaPathProp } = props;
|
|
9
|
+
const { value, setValue, showError } = useField({
|
|
10
|
+
path
|
|
11
|
+
});
|
|
12
|
+
const { collectionSlug, globalSlug } = useDocumentInfo();
|
|
13
|
+
const { config } = useConfig();
|
|
14
|
+
const [options, setOptions] = useState([]);
|
|
15
|
+
const [inputValue, setInputValue] = useState('');
|
|
16
|
+
const [remoteError, setRemoteError] = useState(null);
|
|
17
|
+
const abortRef = useRef(null);
|
|
18
|
+
const entityType = globalSlug ? 'global' : 'collection';
|
|
19
|
+
const slug = globalSlug || collectionSlug;
|
|
20
|
+
const schemaPath = schemaPathProp ?? field.name;
|
|
21
|
+
const hasMany = field.hasMany ?? false;
|
|
22
|
+
const apiPath = config.routes?.api || '/api';
|
|
23
|
+
const apiRoute = apiPath.startsWith('/') ? apiPath : `/${apiPath}`;
|
|
24
|
+
const baseURL = config.serverURL || '';
|
|
25
|
+
const endpointURL = `${baseURL}${apiRoute}${selectSearchEndpoint}`;
|
|
26
|
+
const selectedValues = useMemo(()=>{
|
|
27
|
+
if (hasMany) {
|
|
28
|
+
return Array.isArray(value) ? value.map((entry)=>String(entry)) : [];
|
|
29
|
+
}
|
|
30
|
+
if (Array.isArray(value) || value === null || value === undefined) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
return [
|
|
34
|
+
String(value)
|
|
35
|
+
];
|
|
36
|
+
}, [
|
|
37
|
+
hasMany,
|
|
38
|
+
value
|
|
39
|
+
]);
|
|
40
|
+
const fetchOptions = useCallback(async (query)=>{
|
|
41
|
+
if (!slug || !schemaPath) {
|
|
42
|
+
setOptions([]);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (abortRef.current) {
|
|
46
|
+
abortRef.current.abort();
|
|
47
|
+
}
|
|
48
|
+
const controller = new AbortController();
|
|
49
|
+
abortRef.current = controller;
|
|
50
|
+
setRemoteError(null);
|
|
51
|
+
const res = await fetch(endpointURL, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: {
|
|
54
|
+
'Content-Type': 'application/json'
|
|
55
|
+
},
|
|
56
|
+
credentials: 'include',
|
|
57
|
+
signal: controller.signal,
|
|
58
|
+
body: JSON.stringify({
|
|
59
|
+
entityType,
|
|
60
|
+
slug,
|
|
61
|
+
schemaPath,
|
|
62
|
+
query,
|
|
63
|
+
selectedValues
|
|
64
|
+
})
|
|
65
|
+
});
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
const errorBody = await res.json().catch(()=>null);
|
|
68
|
+
const message = errorBody?.error || 'Failed to fetch options';
|
|
69
|
+
setRemoteError(message);
|
|
70
|
+
setOptions([]);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const data = await res.json();
|
|
74
|
+
setOptions(Array.isArray(data.options) ? data.options : []);
|
|
75
|
+
}, [
|
|
76
|
+
endpointURL,
|
|
77
|
+
entityType,
|
|
78
|
+
schemaPath,
|
|
79
|
+
selectedValues,
|
|
80
|
+
slug
|
|
81
|
+
]);
|
|
82
|
+
useEffect(()=>{
|
|
83
|
+
if (!slug || !schemaPath) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const timeout = setTimeout(()=>{
|
|
87
|
+
void fetchOptions(inputValue);
|
|
88
|
+
}, debounceMs);
|
|
89
|
+
return ()=>{
|
|
90
|
+
clearTimeout(timeout);
|
|
91
|
+
};
|
|
92
|
+
}, [
|
|
93
|
+
fetchOptions,
|
|
94
|
+
inputValue,
|
|
95
|
+
schemaPath,
|
|
96
|
+
slug
|
|
97
|
+
]);
|
|
98
|
+
useEffect(()=>{
|
|
99
|
+
return ()=>{
|
|
100
|
+
if (abortRef.current) {
|
|
101
|
+
abortRef.current.abort();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}, []);
|
|
105
|
+
const handleChange = useCallback((option)=>{
|
|
106
|
+
if (Array.isArray(option)) {
|
|
107
|
+
const values = option.map((entry)=>String(entry.value));
|
|
108
|
+
setValue(values);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (!option) {
|
|
112
|
+
setValue(hasMany ? [] : null);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
setValue(String(option.value));
|
|
116
|
+
}, [
|
|
117
|
+
hasMany,
|
|
118
|
+
setValue
|
|
119
|
+
]);
|
|
120
|
+
const description = useMemo(()=>{
|
|
121
|
+
if (remoteError) {
|
|
122
|
+
return remoteError;
|
|
123
|
+
}
|
|
124
|
+
return field.admin?.description;
|
|
125
|
+
}, [
|
|
126
|
+
field.admin?.description,
|
|
127
|
+
remoteError
|
|
128
|
+
]);
|
|
129
|
+
const selectValue = useMemo(()=>{
|
|
130
|
+
if (hasMany) {
|
|
131
|
+
return Array.isArray(value) ? value : [];
|
|
132
|
+
}
|
|
133
|
+
return Array.isArray(value) ? '' : value ?? '';
|
|
134
|
+
}, [
|
|
135
|
+
hasMany,
|
|
136
|
+
value
|
|
137
|
+
]);
|
|
138
|
+
return /*#__PURE__*/ _jsx(SelectInput, {
|
|
139
|
+
description: description,
|
|
140
|
+
hasMany: hasMany,
|
|
141
|
+
label: field.label,
|
|
142
|
+
localized: field.localized,
|
|
143
|
+
name: field.name,
|
|
144
|
+
onChange: handleChange,
|
|
145
|
+
onInputChange: setInputValue,
|
|
146
|
+
options: options,
|
|
147
|
+
path: path,
|
|
148
|
+
required: field.required,
|
|
149
|
+
showError: showError,
|
|
150
|
+
value: selectValue
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
//# sourceMappingURL=SelectSearchField.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/ui/SelectSearchField.tsx"],"sourcesContent":["'use client'\n\nimport type { OptionObject, TextFieldClientComponent } from 'payload'\nimport type { ReactSelectOption } from '@payloadcms/ui'\nimport { SelectInput, useConfig, useDocumentInfo, useField } from '@payloadcms/ui'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { selectSearchEndpoint } from '../endpointName.js'\n\nconst debounceMs = 300\n\nexport const SelectSearchField: TextFieldClientComponent = (props) => {\n const { field, path, schemaPath: schemaPathProp } = props\n\n const { value, setValue, showError } = useField<string | string[]>({\n path,\n })\n\n const { collectionSlug, globalSlug } = useDocumentInfo()\n const { config } = useConfig()\n\n const [options, setOptions] = useState<OptionObject[]>([])\n\n const [inputValue, setInputValue] = useState('')\n const [remoteError, setRemoteError] = useState<string | null>(null)\n\n const abortRef = useRef<AbortController | null>(null)\n\n const entityType = globalSlug ? 'global' : 'collection'\n const slug = globalSlug || collectionSlug\n\n const schemaPath = schemaPathProp ?? field.name\n const hasMany = field.hasMany ?? false\n\n const apiPath = config.routes?.api || '/api'\n const apiRoute = apiPath.startsWith('/') ? apiPath : `/${apiPath}`\n const baseURL = config.serverURL || ''\n const endpointURL = `${baseURL}${apiRoute}${selectSearchEndpoint}`\n\n const selectedValues = useMemo(() => {\n if (hasMany) {\n return Array.isArray(value) ? value.map((entry) => String(entry)) : []\n }\n\n if (Array.isArray(value) || value === null || value === undefined) {\n return []\n }\n\n return [String(value)]\n }, [hasMany, value])\n\n const fetchOptions = useCallback(\n async (query: string) => {\n if (!slug || !schemaPath) {\n setOptions([])\n return\n }\n\n if (abortRef.current) {\n abortRef.current.abort()\n }\n\n const controller = new AbortController()\n abortRef.current = controller\n\n setRemoteError(null)\n\n const res = await fetch(endpointURL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n credentials: 'include',\n signal: controller.signal,\n body: JSON.stringify({\n entityType,\n slug,\n schemaPath,\n query,\n selectedValues,\n }),\n })\n\n if (!res.ok) {\n const errorBody = await res.json().catch(() => null)\n const message = errorBody?.error || 'Failed to fetch options'\n setRemoteError(message)\n setOptions([])\n return\n }\n\n const data = (await res.json()) as { options?: OptionObject[] }\n setOptions(Array.isArray(data.options) ? data.options : [])\n },\n [endpointURL, entityType, schemaPath, selectedValues, slug],\n )\n\n useEffect(() => {\n if (!slug || !schemaPath) {\n return\n }\n\n const timeout = setTimeout(() => {\n void fetchOptions(inputValue)\n }, debounceMs)\n\n return () => {\n clearTimeout(timeout)\n }\n }, [fetchOptions, inputValue, schemaPath, slug])\n\n useEffect(() => {\n return () => {\n if (abortRef.current) {\n abortRef.current.abort()\n }\n }\n }, [])\n\n const handleChange = useCallback(\n (option: ReactSelectOption | ReactSelectOption[] | null) => {\n if (Array.isArray(option)) {\n const values = option.map((entry) => String(entry.value))\n setValue(values)\n return\n }\n\n if (!option) {\n setValue(hasMany ? [] : null)\n return\n }\n\n setValue(String(option.value))\n },\n [hasMany, setValue],\n )\n\n const description = useMemo(() => {\n if (remoteError) {\n return remoteError\n }\n\n return field.admin?.description\n }, [field.admin?.description, remoteError])\n\n const selectValue = useMemo(() => {\n if (hasMany) {\n return Array.isArray(value) ? value : []\n }\n\n return Array.isArray(value) ? '' : (value ?? '')\n }, [hasMany, value])\n\n return (\n <SelectInput\n description={description}\n hasMany={hasMany}\n label={field.label as string}\n localized={field.localized}\n name={field.name}\n onChange={handleChange as (value: ReactSelectOption | ReactSelectOption[]) => void}\n onInputChange={setInputValue}\n options={options}\n path={path}\n required={field.required}\n showError={showError}\n value={selectValue}\n />\n )\n}\n"],"names":["SelectInput","useConfig","useDocumentInfo","useField","useCallback","useEffect","useMemo","useRef","useState","selectSearchEndpoint","debounceMs","SelectSearchField","props","field","path","schemaPath","schemaPathProp","value","setValue","showError","collectionSlug","globalSlug","config","options","setOptions","inputValue","setInputValue","remoteError","setRemoteError","abortRef","entityType","slug","name","hasMany","apiPath","routes","api","apiRoute","startsWith","baseURL","serverURL","endpointURL","selectedValues","Array","isArray","map","entry","String","undefined","fetchOptions","query","current","abort","controller","AbortController","res","fetch","method","headers","credentials","signal","body","JSON","stringify","ok","errorBody","json","catch","message","error","data","timeout","setTimeout","clearTimeout","handleChange","option","values","description","admin","selectValue","label","localized","onChange","onInputChange","required"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whatworks/payload-select-search-field",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Payload plugin and field component for server-backed search select fields.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,9 +37,15 @@
|
|
|
37
37
|
"@payloadcms/db-mongodb": "^3.0.0",
|
|
38
38
|
"@payloadcms/eslint-config": "^3.0.0",
|
|
39
39
|
"@payloadcms/next": "^3.0.0",
|
|
40
|
-
"@
|
|
41
|
-
"@
|
|
42
|
-
"@types/
|
|
40
|
+
"@swc-node/register": "1.10.9",
|
|
41
|
+
"@swc/cli": "0.6.0",
|
|
42
|
+
"@types/node": "22.19.9",
|
|
43
|
+
"@types/react": "19.2.9",
|
|
44
|
+
"@types/react-dom": "19.2.3",
|
|
45
|
+
"react": "19.2.1",
|
|
46
|
+
"react-dom": "19.2.1",
|
|
47
|
+
"eslint": "^9.23.0",
|
|
48
|
+
"eslint-config-next": "15.4.11",
|
|
43
49
|
"copyfiles": "^2.4.1",
|
|
44
50
|
"cross-env": "^7.0.3",
|
|
45
51
|
"next": "^16.1.6",
|
|
@@ -49,15 +55,19 @@
|
|
|
49
55
|
"typescript": "5.5.3"
|
|
50
56
|
},
|
|
51
57
|
"scripts": {
|
|
52
|
-
"build": "
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
58
|
+
"build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
|
|
59
|
+
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
|
|
60
|
+
"build:types": "tsc --outDir dist --rootDir ./src",
|
|
61
|
+
"clean": "rimraf {dist,*.tsbuildinfo}",
|
|
62
|
+
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
|
63
|
+
"dev": "next dev dev --turbo",
|
|
57
64
|
"dev:generate-importmap": "pnpm dev:payload generate:importmap",
|
|
58
65
|
"dev:generate-types": "pnpm dev:payload generate:types",
|
|
59
|
-
"
|
|
60
|
-
"
|
|
66
|
+
"dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
|
|
67
|
+
"generate:importmap": "pnpm dev:generate-importmap",
|
|
68
|
+
"generate:types": "pnpm dev:generate-types",
|
|
69
|
+
"lint": "eslint",
|
|
70
|
+
"lint:fix": "eslint ./src --fix",
|
|
61
71
|
"typecheck": "tsc --noEmit"
|
|
62
72
|
}
|
|
63
73
|
}
|