@whatworks/payload-select-search-field 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 CHANGED
@@ -3,6 +3,11 @@
3
3
  Server-backed search select field and plugin for Payload. The client component queries a shared endpoint
4
4
  and passes the current search query plus any currently selected values.
5
5
 
6
+ ## Demo
7
+
8
+ https://github.com/user-attachments/assets/0f49d3f9-8473-4d77-8e20-ee07a1276a8e
9
+
10
+
6
11
  ## Usage
7
12
 
8
13
  Add the plugin:
@@ -15,7 +20,7 @@ export default buildConfig({
15
20
  })
16
21
  ```
17
22
 
18
- Add a field with `selectSearch` (recommended):c
23
+ Add a field with `selectSearch` (recommended):
19
24
 
20
25
  ```ts
21
26
  import { selectSearch } from '@whatworks/payload-select-search-field'
@@ -23,6 +28,8 @@ import { selectSearch } from '@whatworks/payload-select-search-field'
23
28
  selectSearch({
24
29
  name: 'stripeCustomer',
25
30
  hasMany: true,
31
+ passDataToSearchFunction: true,
32
+ passSiblingDataToSearchFunction: true,
26
33
  searchFunction: async ({ query, selectedValues }) => {
27
34
  return [
28
35
  { value: 'cus_123', label: `Result for ${query}` },
@@ -32,17 +39,14 @@ selectSearch({
32
39
  })),
33
40
  ]
34
41
  },
35
- admin: {
36
- components: {
37
- Field: '@whatworks/payload-select-search-field/client#SelectSearchField',
38
- },
39
- },
40
42
  })
41
43
  ```
42
44
 
43
45
  `searchFunction` receives:
44
46
  - `query`: the current input text.
45
47
  - `selectedValues`: an array of currently selected values (empty array when nothing is selected).
48
+ - `siblingData`: sibling field values for the current field path when `passSiblingDataToSearchFunction: true`.
49
+ - `data`: full form data when `passDataToSearchFunction: true`.
46
50
  - `req`, `field`, and `collection`/`global` context.
47
51
 
48
52
  The client component calls the shared endpoint path from `selectSearchEndpoint`.
@@ -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,3 @@
1
+ export const selectSearchEndpoint = '/select-search';
2
+
3
+ //# sourceMappingURL=endpointName.js.map
@@ -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,3 @@
1
+ export { SelectSearchField } from '../ui/SelectSearchField.js';
2
+
3
+ //# sourceMappingURL=client.js.map
@@ -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"}
@@ -1,10 +1,11 @@
1
1
  import type { Field, TextField } from 'payload';
2
2
  import type { SelectSearchFunction } from './types.js';
3
- export type SelectSearchFieldArgs = Omit<TextField, 'admin' | 'custom' | 'type' | 'hasMany'> & {
3
+ export type SelectSearchFieldArgs = {
4
+ admin?: TextField['admin'];
5
+ custom?: Record<string, unknown>;
4
6
  hasMany?: boolean;
5
- type?: 'text';
7
+ passDataToSearchFunction?: boolean;
6
8
  searchFunction: SelectSearchFunction;
7
- custom?: Record<string, unknown>;
8
- admin?: TextField['admin'];
9
- };
9
+ type?: 'text';
10
+ } & Omit<TextField, 'admin' | 'custom' | 'hasMany' | 'type'>;
10
11
  export declare const selectSearchField: (args: SelectSearchFieldArgs) => Field;
@@ -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"}
@@ -1,24 +1,28 @@
1
- import type { FlattenedField, PayloadRequest, SanitizedCollectionConfig, SanitizedGlobalConfig } from 'payload';
1
+ import type { Data, FlattenedField, PayloadRequest, SanitizedCollectionConfig, SanitizedGlobalConfig } from 'payload';
2
2
  export type SelectSearchOption = {
3
+ [key: string]: unknown;
3
4
  label: string;
4
5
  value: string;
5
- [key: string]: unknown;
6
6
  };
7
7
  export type SelectSearchFunctionArgs = {
8
- req: PayloadRequest;
9
- query: string;
10
- selectedValues: string[];
11
- field: FlattenedField;
12
8
  collection?: SanitizedCollectionConfig;
9
+ data?: Data;
10
+ field: FlattenedField;
13
11
  global?: SanitizedGlobalConfig;
12
+ query: string;
13
+ req: PayloadRequest;
14
+ selectedValues: string[];
15
+ siblingData?: Data;
14
16
  };
15
17
  export type SelectSearchFunction = (args: SelectSearchFunctionArgs) => Promise<SelectSearchOption[]> | SelectSearchOption[];
16
18
  export type SelectSearchRequest = {
19
+ data?: Data;
17
20
  entityType: 'collection' | 'global';
18
- slug: string;
19
- schemaPath: string;
20
21
  query?: string;
22
+ schemaPath: string;
21
23
  selectedValues?: string[];
24
+ siblingData?: Data;
25
+ slug: string;
22
26
  };
23
27
  export type SelectSearchResponse = {
24
28
  options: SelectSearchOption[];
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ export { };
2
+
3
+ //# sourceMappingURL=types.js.map
@@ -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.0",
3
+ "version": "1.0.2",
4
4
  "description": "Payload plugin and field component for server-backed search select fields.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -11,6 +11,11 @@
11
11
  "LICENSE.md",
12
12
  "README.md"
13
13
  ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/what-works-global/payload-packages",
17
+ "directory": "packages/select-search-field"
18
+ },
14
19
  "publishConfig": {
15
20
  "access": "public",
16
21
  "registry": "https://registry.npmjs.org/"
@@ -37,9 +42,15 @@
37
42
  "@payloadcms/db-mongodb": "^3.0.0",
38
43
  "@payloadcms/eslint-config": "^3.0.0",
39
44
  "@payloadcms/next": "^3.0.0",
40
- "@types/node": "^22.5.4",
41
- "@types/react": "^19.1.0",
42
- "@types/react-dom": "^19.1.2",
45
+ "@swc-node/register": "1.10.9",
46
+ "@swc/cli": "0.6.0",
47
+ "@types/node": "22.19.9",
48
+ "@types/react": "19.2.9",
49
+ "@types/react-dom": "19.2.3",
50
+ "react": "19.2.1",
51
+ "react-dom": "19.2.1",
52
+ "eslint": "^9.23.0",
53
+ "eslint-config-next": "15.4.11",
43
54
  "copyfiles": "^2.4.1",
44
55
  "cross-env": "^7.0.3",
45
56
  "next": "^16.1.6",
@@ -49,15 +60,19 @@
49
60
  "typescript": "5.5.3"
50
61
  },
51
62
  "scripts": {
52
- "build": "tsc --project tsconfig.json",
53
- "clean": "rimraf {dist,*.tsbuildinfo,package}",
54
- "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/",
55
- "dev": "next dev dev",
56
- "dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
63
+ "build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
64
+ "build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
65
+ "build:types": "tsc --outDir dist --rootDir ./src",
66
+ "clean": "rimraf {dist,*.tsbuildinfo}",
67
+ "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
68
+ "dev": "next dev dev --turbo",
57
69
  "dev:generate-importmap": "pnpm dev:payload generate:importmap",
58
70
  "dev:generate-types": "pnpm dev:payload generate:types",
59
- "lint": "eslint .",
60
- "lint:fix": "eslint . --fix",
61
- "typecheck": "tsc --noEmit"
71
+ "dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
72
+ "generate:importmap": "pnpm dev:generate-importmap",
73
+ "generate:types": "pnpm dev:generate-types",
74
+ "lint": "eslint \"src/**/*.{ts,tsx}\"",
75
+ "lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix",
76
+ "typecheck": "tsc --outDir dist --rootDir ./src"
62
77
  }
63
78
  }