@srothgan/sanity-plugin-autocomplete-input 3.0.2 → 3.1.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 +85 -54
- package/dist/index.cjs +79 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +81 -0
- package/dist/index.js.map +1 -0
- package/package.json +81 -49
- package/sanity.json +8 -0
- package/src/AutoCompleteInput.tsx +121 -0
- package/src/index.ts +10 -0
- package/src/schemas/autocompleteString.ts +31 -0
- package/src/types/InputOptions.ts +12 -0
- package/{lib/types/Option.d.ts → src/types/Option.ts} +2 -2
- package/src/types/index.ts +2 -0
- package/lib/AutoCompleteInput.d.ts +0 -8
- package/lib/AutoCompleteInput.js +0 -90
- package/lib/index.d.ts +0 -1
- package/lib/index.js +0 -10
- package/lib/schemas/autocompleteString.d.ts +0 -20
- package/lib/schemas/autocompleteString.js +0 -11
- package/lib/types/InputOptions.d.ts +0 -11
- package/lib/types/InputOptions.js +0 -1
- package/lib/types/Option.js +0 -1
- package/lib/types/index.d.ts +0 -2
- package/lib/types/index.js +0 -1
package/README.md
CHANGED
|
@@ -39,45 +39,69 @@ yarn add @srothgan/sanity-plugin-autocomplete-input
|
|
|
39
39
|
Add it as a plugin in sanity.config.ts (or .js):
|
|
40
40
|
|
|
41
41
|
```js
|
|
42
|
-
import {
|
|
42
|
+
import {autocompletInput} from '@srothgan/sanity-plugin-autocomplete-input'
|
|
43
43
|
|
|
44
44
|
export default defineConfig({
|
|
45
45
|
// ...
|
|
46
46
|
plugins: [autocompletInput()],
|
|
47
|
-
})
|
|
47
|
+
})
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
### Configuration Options
|
|
51
|
+
|
|
52
|
+
You can configure the autocomplete behavior using one of three approaches:
|
|
53
|
+
|
|
54
|
+
#### 1. Auto-aggregate from Existing Documents
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
2. Manually specify options in the schema option
|
|
54
|
-
3. Specify your own GROQ query returning a `[{ "value": "foobar" }]` format (you can use a `transform` function if this is not achievable using GROQ only)
|
|
56
|
+
The plugin automatically collects unique values from documents with the same field:
|
|
55
57
|
|
|
56
58
|
```javascript
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
59
|
+
{
|
|
60
|
+
name: "category",
|
|
61
|
+
type: "autocomplete",
|
|
62
|
+
options: {
|
|
63
|
+
autocompleteFieldPath: "category", // aggregates from all documents with this field
|
|
64
|
+
disableNew: false, // optional: prevent users from creating new values
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### 2. Manual Options List
|
|
70
|
+
|
|
71
|
+
Provide a predefined list of options:
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
{
|
|
75
|
+
name: "status",
|
|
76
|
+
type: "autocomplete",
|
|
77
|
+
options: {
|
|
78
|
+
options: [
|
|
79
|
+
{ value: "Draft" },
|
|
80
|
+
{ value: "Published" },
|
|
81
|
+
{ value: "Archived" }
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### 3. Custom GROQ Query
|
|
88
|
+
|
|
89
|
+
Define your own query for maximum flexibility:
|
|
90
|
+
|
|
91
|
+
```javascript
|
|
92
|
+
{
|
|
93
|
+
name: "author",
|
|
94
|
+
type: "autocomplete",
|
|
95
|
+
options: {
|
|
96
|
+
groq: {
|
|
97
|
+
query: '*[_type == $type] { "value": title }',
|
|
98
|
+
params: {
|
|
99
|
+
type: "author",
|
|
77
100
|
},
|
|
101
|
+
transform: (values) => values, // optional: transform results
|
|
78
102
|
},
|
|
79
|
-
|
|
80
|
-
}
|
|
103
|
+
},
|
|
104
|
+
}
|
|
81
105
|
```
|
|
82
106
|
|
|
83
107
|
### Advanced GROQ parameters
|
|
@@ -88,11 +112,11 @@ It is also possible to refer to the current parent value (for a top-level field
|
|
|
88
112
|
export default {
|
|
89
113
|
fields: [
|
|
90
114
|
{
|
|
91
|
-
name:
|
|
92
|
-
type:
|
|
115
|
+
name: 'autocomplete-input',
|
|
116
|
+
type: 'autocomplete',
|
|
93
117
|
options: {
|
|
94
118
|
groq: {
|
|
95
|
-
query:
|
|
119
|
+
query: '*[_id != $docId]',
|
|
96
120
|
params: (parent) => ({
|
|
97
121
|
docId: parent?._id,
|
|
98
122
|
}),
|
|
@@ -100,34 +124,17 @@ export default {
|
|
|
100
124
|
},
|
|
101
125
|
},
|
|
102
126
|
],
|
|
103
|
-
}
|
|
127
|
+
}
|
|
104
128
|
```
|
|
105
129
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
This maintained fork includes the following enhancements:
|
|
109
|
-
|
|
110
|
-
### Updated Dependencies
|
|
111
|
-
- **Sanity v4 & v5 Support**: Fully compatible with both Sanity Studio v4 and v5
|
|
112
|
-
- **React 18 & 19 Support**: Works with React 18 (Sanity v4) and React 19 (Sanity v5)
|
|
113
|
-
- **Modern Build Tooling**: Migrated from Babel to SWC for faster builds
|
|
114
|
-
- **TypeScript 5.3**: Updated to latest TypeScript with improved type safety
|
|
115
|
-
|
|
116
|
-
### Maintained & Active
|
|
117
|
-
- Regular dependency updates for security and compatibility
|
|
118
|
-
- Active issue tracking and bug fixes
|
|
119
|
-
- Community-driven improvements
|
|
120
|
-
|
|
121
|
-
All original functionality and API remain unchanged for seamless migration.
|
|
122
|
-
|
|
123
|
-
## TypeScript Usage
|
|
130
|
+
### TypeScript Usage
|
|
124
131
|
|
|
125
132
|
The plugin is written in TypeScript and exports all necessary types:
|
|
126
133
|
|
|
127
134
|
```typescript
|
|
128
|
-
import {
|
|
129
|
-
import {
|
|
130
|
-
import type {
|
|
135
|
+
import {defineConfig} from 'sanity'
|
|
136
|
+
import {autocompletInput} from '@srothgan/sanity-plugin-autocomplete-input'
|
|
137
|
+
import type {InputOptions} from '@srothgan/sanity-plugin-autocomplete-input'
|
|
131
138
|
|
|
132
139
|
export default defineConfig({
|
|
133
140
|
// ...
|
|
@@ -153,12 +160,33 @@ export default defineConfig({
|
|
|
153
160
|
})
|
|
154
161
|
```
|
|
155
162
|
|
|
163
|
+
## Differences from Original
|
|
164
|
+
|
|
165
|
+
This maintained fork includes the following enhancements:
|
|
166
|
+
|
|
167
|
+
### Updated Dependencies
|
|
168
|
+
|
|
169
|
+
- **Sanity v4 & v5 Support**: Fully compatible with both Sanity Studio v4 and v5
|
|
170
|
+
- **React 18 & 19 Support**: Works with React 18 (Sanity v4) and React 19 (Sanity v5)
|
|
171
|
+
- **Modern Build Tooling**: Migrated from Babel to SWC for faster builds
|
|
172
|
+
- **TypeScript 5.3**: Updated to latest TypeScript with improved type safety
|
|
173
|
+
|
|
174
|
+
### Maintained & Active
|
|
175
|
+
|
|
176
|
+
- Regular dependency updates for security and compatibility
|
|
177
|
+
- Active issue tracking and bug fixes
|
|
178
|
+
- Community-driven improvements
|
|
179
|
+
|
|
180
|
+
All original functionality and API remain unchanged for seamless migration.
|
|
181
|
+
|
|
156
182
|
## Troubleshooting
|
|
157
183
|
|
|
158
184
|
### Plugin not appearing in Studio
|
|
185
|
+
|
|
159
186
|
Make sure you've added the plugin to your `sanity.config.ts`:
|
|
187
|
+
|
|
160
188
|
```typescript
|
|
161
|
-
import {
|
|
189
|
+
import {autocompletInput} from '@srothgan/sanity-plugin-autocomplete-input'
|
|
162
190
|
|
|
163
191
|
export default defineConfig({
|
|
164
192
|
plugins: [autocompletInput()],
|
|
@@ -166,12 +194,15 @@ export default defineConfig({
|
|
|
166
194
|
```
|
|
167
195
|
|
|
168
196
|
### Autocomplete options not loading
|
|
197
|
+
|
|
169
198
|
- Verify your GROQ query returns data in the format `[{ "value": "..." }]`
|
|
170
199
|
- Check the browser console for any query errors
|
|
171
200
|
- Ensure the `autocompleteFieldPath` matches an existing field in your documents
|
|
172
201
|
|
|
173
202
|
### TypeScript errors
|
|
203
|
+
|
|
174
204
|
Make sure your `tsconfig.json` includes:
|
|
205
|
+
|
|
175
206
|
```json
|
|
176
207
|
{
|
|
177
208
|
"compilerOptions": {
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: !0 });
|
|
3
|
+
var sanity = require("sanity"), jsxRuntime = require("react/jsx-runtime"), ui = require("@sanity/ui"), compact = require("just-compact"), get = require("just-safe-get"), unique = require("just-unique"), React = require("react");
|
|
4
|
+
function _interopDefaultCompat(e) {
|
|
5
|
+
return e && typeof e == "object" && "default" in e ? e : { default: e };
|
|
6
|
+
}
|
|
7
|
+
var compact__default = /* @__PURE__ */ _interopDefaultCompat(compact), get__default = /* @__PURE__ */ _interopDefaultCompat(get), unique__default = /* @__PURE__ */ _interopDefaultCompat(unique), React__default = /* @__PURE__ */ _interopDefaultCompat(React);
|
|
8
|
+
const AutoCompleteInput = (props) => {
|
|
9
|
+
const { id, schemaType, value, validationError, readOnly, onChange } = props, sanityClient = sanity.useClient(), documentValue = sanity.useFormValue([]), [loading, setLoading] = React.useState(!1), [query, setQuery] = React__default.default.useState(""), [options, setOptions] = React__default.default.useState([]), canCreateNew = schemaType.options?.disableNew !== !0, optionsList = React.useMemo(() => {
|
|
10
|
+
const uniqueOptions = unique__default.default(
|
|
11
|
+
options.map(({ value: optionValue }) => optionValue),
|
|
12
|
+
!1,
|
|
13
|
+
!0
|
|
14
|
+
);
|
|
15
|
+
return !uniqueOptions.find((optionValue) => optionValue === query) && canCreateNew ? [
|
|
16
|
+
...uniqueOptions.map((optionValue) => ({ value: optionValue })),
|
|
17
|
+
{ value: query, isNew: !0 }
|
|
18
|
+
] : uniqueOptions.map((optionValue) => ({ value: optionValue }));
|
|
19
|
+
}, [query, options, canCreateNew]), handleQueryChange = React.useCallback((queryValue) => {
|
|
20
|
+
setQuery(queryValue ?? "");
|
|
21
|
+
}, []), handleChange = React.useCallback(
|
|
22
|
+
(newValue) => {
|
|
23
|
+
onChange(sanity.PatchEvent.from(newValue ? sanity.set(newValue) : sanity.unset()));
|
|
24
|
+
},
|
|
25
|
+
[onChange]
|
|
26
|
+
), renderOption = React.useCallback(
|
|
27
|
+
(option) => /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { as: "button", padding: 3, tone: option.isNew ? "primary" : "default", shadow: 1, children: option.isNew ? canCreateNew && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { children: [
|
|
28
|
+
'Create new option "',
|
|
29
|
+
option.value,
|
|
30
|
+
'"'
|
|
31
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: option.value }) }),
|
|
32
|
+
[canCreateNew]
|
|
33
|
+
);
|
|
34
|
+
return React.useEffect(() => {
|
|
35
|
+
if (schemaType.options?.options) {
|
|
36
|
+
setOptions(schemaType.options.options), setLoading(!1);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const path = schemaType.options?.autocompleteFieldPath ?? "title", {
|
|
40
|
+
query: groqQuery,
|
|
41
|
+
transform,
|
|
42
|
+
params = {}
|
|
43
|
+
} = schemaType.options?.groq || {
|
|
44
|
+
query: `*[defined(${path})] { "value": ${path} }`
|
|
45
|
+
}, resolvedParams = typeof params == "function" ? params(documentValue) : params;
|
|
46
|
+
setLoading(!0), sanityClient.fetch(groqQuery, resolvedParams).then((results) => {
|
|
47
|
+
if (Array.isArray(results)) {
|
|
48
|
+
const transformedResults = transform ? transform(results) : results, compactedValues = compact__default.default(transformedResults.map((doc) => get__default.default(doc, "value")));
|
|
49
|
+
setOptions(compactedValues.map((optionValue) => ({ value: String(optionValue) }))), setLoading(!1);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}, [schemaType.options, documentValue, sanityClient]), /* @__PURE__ */ jsxRuntime.jsx(
|
|
53
|
+
ui.Autocomplete,
|
|
54
|
+
{
|
|
55
|
+
id,
|
|
56
|
+
readOnly: readOnly ?? !1,
|
|
57
|
+
customValidity: validationError,
|
|
58
|
+
loading,
|
|
59
|
+
disabled: loading,
|
|
60
|
+
options: optionsList,
|
|
61
|
+
value: value ?? "",
|
|
62
|
+
onChange: handleChange,
|
|
63
|
+
onQueryChange: handleQueryChange,
|
|
64
|
+
renderOption
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
}, typeName = "autocomplete", autocompleteString = sanity.defineType({
|
|
68
|
+
name: typeName,
|
|
69
|
+
type: "string",
|
|
70
|
+
title: "Autocomplete",
|
|
71
|
+
components: { input: AutoCompleteInput }
|
|
72
|
+
}), autocompletInput = sanity.definePlugin({
|
|
73
|
+
name: "sanity-plugin-autocomplete-input",
|
|
74
|
+
schema: {
|
|
75
|
+
types: [autocompleteString]
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
exports.autocompletInput = autocompletInput;
|
|
79
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/AutoCompleteInput.tsx","../src/schemas/autocompleteString.ts","../src/index.ts"],"sourcesContent":["import {Autocomplete, Card, Text} from '@sanity/ui'\nimport compact from 'just-compact'\nimport get from 'just-safe-get'\nimport unique from 'just-unique'\nimport React, {useCallback, useEffect, useMemo, useState} from 'react'\nimport {\n PatchEvent,\n set,\n StringInputProps,\n StringSchemaType,\n unset,\n useClient,\n useFormValue,\n} from 'sanity'\n\nimport type {InputOptions, Option} from './types/index.js'\n\nexport type AutocompleteSchemaType = Omit<StringSchemaType, 'options'> & {\n options?: StringSchemaType['options'] & InputOptions\n}\nexport type InputProps = StringInputProps<AutocompleteSchemaType>\n\nexport const AutoCompleteInput = (props: InputProps): React.ReactElement => {\n const {id, schemaType, value, validationError, readOnly, onChange} = props\n\n const sanityClient = useClient()\n const documentValue = useFormValue([])\n const [loading, setLoading] = useState(false)\n const [query, setQuery] = React.useState('')\n const [options, setOptions] = React.useState<Option[]>([])\n const canCreateNew = schemaType.options?.disableNew !== true\n\n const optionsList = useMemo<(Option & {isNew?: boolean})[]>(() => {\n const uniqueOptions = unique(\n options.map(({value: optionValue}) => optionValue),\n false,\n true,\n )\n const queryInOptions = uniqueOptions.find((optionValue) => optionValue === query)\n if (!queryInOptions && canCreateNew) {\n return [\n ...uniqueOptions.map((optionValue) => ({value: optionValue})),\n {value: query, isNew: true},\n ]\n }\n\n return uniqueOptions.map((optionValue) => ({value: optionValue}))\n }, [query, options, canCreateNew])\n\n const handleQueryChange = useCallback((queryValue: string | null) => {\n setQuery(queryValue ?? '')\n }, [])\n\n const handleChange = useCallback(\n (newValue: string) => {\n onChange(PatchEvent.from(newValue ? set(newValue) : unset()))\n },\n [onChange],\n )\n\n const renderOption = useCallback(\n (option: Option & {isNew?: boolean}) => (\n <Card as=\"button\" padding={3} tone={option.isNew ? 'primary' : 'default'} shadow={1}>\n {option.isNew ? (\n canCreateNew && <Text>Create new option "{option.value}"</Text>\n ) : (\n <Text>{option.value}</Text>\n )}\n </Card>\n ),\n [canCreateNew],\n )\n\n useEffect(() => {\n if (schemaType.options?.options) {\n // eslint-disable-next-line react-hooks/set-state-in-effect\n setOptions(schemaType.options.options)\n setLoading(false)\n return\n }\n\n const path = schemaType.options?.autocompleteFieldPath ?? 'title'\n const {\n query: groqQuery,\n transform,\n params = {},\n } = schemaType.options?.groq || {\n query: `*[defined(${path})] { \"value\": ${path} }`,\n }\n\n const resolvedParams =\n typeof params === 'function'\n ? params(documentValue as Record<string, unknown> | undefined)\n : params\n\n setLoading(true)\n sanityClient.fetch(groqQuery, resolvedParams).then((results) => {\n if (Array.isArray(results)) {\n const transformedResults = transform ? transform(results) : results\n const compactedValues = compact(transformedResults.map((doc) => get(doc, 'value')))\n setOptions(compactedValues.map((optionValue) => ({value: String(optionValue)})))\n setLoading(false)\n }\n })\n }, [schemaType.options, documentValue, sanityClient])\n\n return (\n <Autocomplete\n id={id}\n readOnly={readOnly ?? false}\n customValidity={validationError}\n loading={loading}\n disabled={loading}\n options={optionsList}\n value={value ?? ''}\n onChange={handleChange}\n onQueryChange={handleQueryChange}\n renderOption={renderOption}\n />\n )\n}\n","import {defineType, StringDefinition} from 'sanity'\n\nimport {AutoCompleteInput} from '../AutoCompleteInput.js'\nimport type {InputOptions} from '../types/index.js'\n\nconst typeName = 'autocomplete' as const\n\n/**\n * @public\n */\nexport interface AutocompleteStringDefinition extends Omit<\n StringDefinition,\n 'type' | 'fields' | 'options'\n> {\n type: typeof typeName\n options?: InputOptions\n}\n\ndeclare module '@sanity/types' {\n // makes type: 'color' narrow correctly when using defineTyp/defineField/defineArrayMember\n export interface IntrinsicDefinitions {\n autocomplete: AutocompleteStringDefinition\n }\n}\n\nexport const autocompleteString = defineType({\n name: typeName,\n type: 'string',\n title: 'Autocomplete',\n components: {input: AutoCompleteInput},\n})\n","import {definePlugin} from 'sanity'\n\nimport {autocompleteString} from './schemas/autocompleteString.js'\n\nexport const autocompletInput = definePlugin({\n name: 'sanity-plugin-autocomplete-input',\n schema: {\n types: [autocompleteString],\n },\n})\n"],"names":["useClient","useFormValue","useState","React","useMemo","unique","useCallback","PatchEvent","set","unset","jsx","Card","Text","useEffect","compact","get","Autocomplete","defineType","definePlugin"],"mappings":"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"}
|
package/dist/index.d.cts
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useClient, useFormValue, PatchEvent, set, unset, defineType, definePlugin } from "sanity";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Card, Text, Autocomplete } from "@sanity/ui";
|
|
4
|
+
import compact from "just-compact";
|
|
5
|
+
import get from "just-safe-get";
|
|
6
|
+
import unique from "just-unique";
|
|
7
|
+
import React, { useState, useMemo, useCallback, useEffect } from "react";
|
|
8
|
+
const AutoCompleteInput = (props) => {
|
|
9
|
+
const { id, schemaType, value, validationError, readOnly, onChange } = props, sanityClient = useClient(), documentValue = useFormValue([]), [loading, setLoading] = useState(!1), [query, setQuery] = React.useState(""), [options, setOptions] = React.useState([]), canCreateNew = schemaType.options?.disableNew !== !0, optionsList = useMemo(() => {
|
|
10
|
+
const uniqueOptions = unique(
|
|
11
|
+
options.map(({ value: optionValue }) => optionValue),
|
|
12
|
+
!1,
|
|
13
|
+
!0
|
|
14
|
+
);
|
|
15
|
+
return !uniqueOptions.find((optionValue) => optionValue === query) && canCreateNew ? [
|
|
16
|
+
...uniqueOptions.map((optionValue) => ({ value: optionValue })),
|
|
17
|
+
{ value: query, isNew: !0 }
|
|
18
|
+
] : uniqueOptions.map((optionValue) => ({ value: optionValue }));
|
|
19
|
+
}, [query, options, canCreateNew]), handleQueryChange = useCallback((queryValue) => {
|
|
20
|
+
setQuery(queryValue ?? "");
|
|
21
|
+
}, []), handleChange = useCallback(
|
|
22
|
+
(newValue) => {
|
|
23
|
+
onChange(PatchEvent.from(newValue ? set(newValue) : unset()));
|
|
24
|
+
},
|
|
25
|
+
[onChange]
|
|
26
|
+
), renderOption = useCallback(
|
|
27
|
+
(option) => /* @__PURE__ */ jsx(Card, { as: "button", padding: 3, tone: option.isNew ? "primary" : "default", shadow: 1, children: option.isNew ? canCreateNew && /* @__PURE__ */ jsxs(Text, { children: [
|
|
28
|
+
'Create new option "',
|
|
29
|
+
option.value,
|
|
30
|
+
'"'
|
|
31
|
+
] }) : /* @__PURE__ */ jsx(Text, { children: option.value }) }),
|
|
32
|
+
[canCreateNew]
|
|
33
|
+
);
|
|
34
|
+
return useEffect(() => {
|
|
35
|
+
if (schemaType.options?.options) {
|
|
36
|
+
setOptions(schemaType.options.options), setLoading(!1);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const path = schemaType.options?.autocompleteFieldPath ?? "title", {
|
|
40
|
+
query: groqQuery,
|
|
41
|
+
transform,
|
|
42
|
+
params = {}
|
|
43
|
+
} = schemaType.options?.groq || {
|
|
44
|
+
query: `*[defined(${path})] { "value": ${path} }`
|
|
45
|
+
}, resolvedParams = typeof params == "function" ? params(documentValue) : params;
|
|
46
|
+
setLoading(!0), sanityClient.fetch(groqQuery, resolvedParams).then((results) => {
|
|
47
|
+
if (Array.isArray(results)) {
|
|
48
|
+
const transformedResults = transform ? transform(results) : results, compactedValues = compact(transformedResults.map((doc) => get(doc, "value")));
|
|
49
|
+
setOptions(compactedValues.map((optionValue) => ({ value: String(optionValue) }))), setLoading(!1);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}, [schemaType.options, documentValue, sanityClient]), /* @__PURE__ */ jsx(
|
|
53
|
+
Autocomplete,
|
|
54
|
+
{
|
|
55
|
+
id,
|
|
56
|
+
readOnly: readOnly ?? !1,
|
|
57
|
+
customValidity: validationError,
|
|
58
|
+
loading,
|
|
59
|
+
disabled: loading,
|
|
60
|
+
options: optionsList,
|
|
61
|
+
value: value ?? "",
|
|
62
|
+
onChange: handleChange,
|
|
63
|
+
onQueryChange: handleQueryChange,
|
|
64
|
+
renderOption
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
}, typeName = "autocomplete", autocompleteString = defineType({
|
|
68
|
+
name: typeName,
|
|
69
|
+
type: "string",
|
|
70
|
+
title: "Autocomplete",
|
|
71
|
+
components: { input: AutoCompleteInput }
|
|
72
|
+
}), autocompletInput = definePlugin({
|
|
73
|
+
name: "sanity-plugin-autocomplete-input",
|
|
74
|
+
schema: {
|
|
75
|
+
types: [autocompleteString]
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
export {
|
|
79
|
+
autocompletInput
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/AutoCompleteInput.tsx","../src/schemas/autocompleteString.ts","../src/index.ts"],"sourcesContent":["import {Autocomplete, Card, Text} from '@sanity/ui'\nimport compact from 'just-compact'\nimport get from 'just-safe-get'\nimport unique from 'just-unique'\nimport React, {useCallback, useEffect, useMemo, useState} from 'react'\nimport {\n PatchEvent,\n set,\n StringInputProps,\n StringSchemaType,\n unset,\n useClient,\n useFormValue,\n} from 'sanity'\n\nimport type {InputOptions, Option} from './types/index.js'\n\nexport type AutocompleteSchemaType = Omit<StringSchemaType, 'options'> & {\n options?: StringSchemaType['options'] & InputOptions\n}\nexport type InputProps = StringInputProps<AutocompleteSchemaType>\n\nexport const AutoCompleteInput = (props: InputProps): React.ReactElement => {\n const {id, schemaType, value, validationError, readOnly, onChange} = props\n\n const sanityClient = useClient()\n const documentValue = useFormValue([])\n const [loading, setLoading] = useState(false)\n const [query, setQuery] = React.useState('')\n const [options, setOptions] = React.useState<Option[]>([])\n const canCreateNew = schemaType.options?.disableNew !== true\n\n const optionsList = useMemo<(Option & {isNew?: boolean})[]>(() => {\n const uniqueOptions = unique(\n options.map(({value: optionValue}) => optionValue),\n false,\n true,\n )\n const queryInOptions = uniqueOptions.find((optionValue) => optionValue === query)\n if (!queryInOptions && canCreateNew) {\n return [\n ...uniqueOptions.map((optionValue) => ({value: optionValue})),\n {value: query, isNew: true},\n ]\n }\n\n return uniqueOptions.map((optionValue) => ({value: optionValue}))\n }, [query, options, canCreateNew])\n\n const handleQueryChange = useCallback((queryValue: string | null) => {\n setQuery(queryValue ?? '')\n }, [])\n\n const handleChange = useCallback(\n (newValue: string) => {\n onChange(PatchEvent.from(newValue ? set(newValue) : unset()))\n },\n [onChange],\n )\n\n const renderOption = useCallback(\n (option: Option & {isNew?: boolean}) => (\n <Card as=\"button\" padding={3} tone={option.isNew ? 'primary' : 'default'} shadow={1}>\n {option.isNew ? (\n canCreateNew && <Text>Create new option "{option.value}"</Text>\n ) : (\n <Text>{option.value}</Text>\n )}\n </Card>\n ),\n [canCreateNew],\n )\n\n useEffect(() => {\n if (schemaType.options?.options) {\n // eslint-disable-next-line react-hooks/set-state-in-effect\n setOptions(schemaType.options.options)\n setLoading(false)\n return\n }\n\n const path = schemaType.options?.autocompleteFieldPath ?? 'title'\n const {\n query: groqQuery,\n transform,\n params = {},\n } = schemaType.options?.groq || {\n query: `*[defined(${path})] { \"value\": ${path} }`,\n }\n\n const resolvedParams =\n typeof params === 'function'\n ? params(documentValue as Record<string, unknown> | undefined)\n : params\n\n setLoading(true)\n sanityClient.fetch(groqQuery, resolvedParams).then((results) => {\n if (Array.isArray(results)) {\n const transformedResults = transform ? transform(results) : results\n const compactedValues = compact(transformedResults.map((doc) => get(doc, 'value')))\n setOptions(compactedValues.map((optionValue) => ({value: String(optionValue)})))\n setLoading(false)\n }\n })\n }, [schemaType.options, documentValue, sanityClient])\n\n return (\n <Autocomplete\n id={id}\n readOnly={readOnly ?? false}\n customValidity={validationError}\n loading={loading}\n disabled={loading}\n options={optionsList}\n value={value ?? ''}\n onChange={handleChange}\n onQueryChange={handleQueryChange}\n renderOption={renderOption}\n />\n )\n}\n","import {defineType, StringDefinition} from 'sanity'\n\nimport {AutoCompleteInput} from '../AutoCompleteInput.js'\nimport type {InputOptions} from '../types/index.js'\n\nconst typeName = 'autocomplete' as const\n\n/**\n * @public\n */\nexport interface AutocompleteStringDefinition extends Omit<\n StringDefinition,\n 'type' | 'fields' | 'options'\n> {\n type: typeof typeName\n options?: InputOptions\n}\n\ndeclare module '@sanity/types' {\n // makes type: 'color' narrow correctly when using defineTyp/defineField/defineArrayMember\n export interface IntrinsicDefinitions {\n autocomplete: AutocompleteStringDefinition\n }\n}\n\nexport const autocompleteString = defineType({\n name: typeName,\n type: 'string',\n title: 'Autocomplete',\n components: {input: AutoCompleteInput},\n})\n","import {definePlugin} from 'sanity'\n\nimport {autocompleteString} from './schemas/autocompleteString.js'\n\nexport const autocompletInput = definePlugin({\n name: 'sanity-plugin-autocomplete-input',\n schema: {\n types: [autocompleteString],\n },\n})\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,30 +1,65 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@srothgan/sanity-plugin-autocomplete-input",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
|
+
"description": "Autocomplete input plugin for Sanity",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"sanity",
|
|
7
|
+
"sanity-plugin",
|
|
8
|
+
"autocomplete",
|
|
9
|
+
"input",
|
|
10
|
+
"v4",
|
|
11
|
+
"v5",
|
|
12
|
+
"maintained",
|
|
13
|
+
"sanity-v5"
|
|
14
|
+
],
|
|
4
15
|
"license": "MIT",
|
|
16
|
+
"author": "Simon Rothgang <simonrothgang@gmail.com>",
|
|
17
|
+
"contributors": [
|
|
18
|
+
{
|
|
19
|
+
"name": "Liam Martens",
|
|
20
|
+
"email": "liam@freighter.studio",
|
|
21
|
+
"url": "https://github.com/LiamMartens"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
5
25
|
"type": "module",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"build:js": "swc ./src -d ./lib --strip-leading-paths",
|
|
8
|
-
"build:types": "tsc -p .",
|
|
9
|
-
"build": "run-p build:*",
|
|
10
|
-
"lint": "eslint src/**/*.{ts,tsx}",
|
|
11
|
-
"lint:fix": "eslint src/**/*.{ts,tsx} --fix",
|
|
12
|
-
"format": "prettier --write \"src/**/*.{ts,tsx,json}\"",
|
|
13
|
-
"format:check": "prettier --check \"src/**/*.{ts,tsx,json}\""
|
|
14
|
-
},
|
|
15
|
-
"types": "./lib/index.d.ts",
|
|
16
|
-
"module": "./lib/index.js",
|
|
17
26
|
"exports": {
|
|
18
27
|
".": {
|
|
19
|
-
"
|
|
20
|
-
"import": "./
|
|
21
|
-
"
|
|
22
|
-
|
|
28
|
+
"source": "./src/index.ts",
|
|
29
|
+
"import": "./dist/index.js",
|
|
30
|
+
"require": "./dist/index.cjs",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./package.json": "./package.json"
|
|
23
34
|
},
|
|
24
|
-
"
|
|
35
|
+
"main": "./dist/index.cjs",
|
|
36
|
+
"module": "./dist/index.js",
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
25
38
|
"files": [
|
|
26
|
-
"
|
|
39
|
+
"dist",
|
|
40
|
+
"sanity.json",
|
|
41
|
+
"src",
|
|
42
|
+
"v2-incompatible.js"
|
|
27
43
|
],
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"exports": {
|
|
46
|
+
".": {
|
|
47
|
+
"import": "./dist/index.js",
|
|
48
|
+
"require": "./dist/index.cjs",
|
|
49
|
+
"default": "./dist/index.js"
|
|
50
|
+
},
|
|
51
|
+
"./package.json": "./package.json"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "plugin-kit verify-package --silent && pkg-utils build --strict --check --clean",
|
|
56
|
+
"format": "prettier --write --cache --ignore-unknown .",
|
|
57
|
+
"format:check": "prettier --check --cache --ignore-unknown .",
|
|
58
|
+
"link-watch": "plugin-kit link-watch",
|
|
59
|
+
"lint": "eslint .",
|
|
60
|
+
"prepublishOnly": "npm run build",
|
|
61
|
+
"watch": "pkg-utils watch --strict"
|
|
62
|
+
},
|
|
28
63
|
"repository": {
|
|
29
64
|
"type": "git",
|
|
30
65
|
"url": "https://github.com/srothgan/srothgan-sanity-plugin-autocomplete-input"
|
|
@@ -32,51 +67,48 @@
|
|
|
32
67
|
"bugs": {
|
|
33
68
|
"url": "https://github.com/srothgan/srothgan-sanity-plugin-autocomplete-input/issues"
|
|
34
69
|
},
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
70
|
+
"dependencies": {
|
|
71
|
+
"@sanity/incompatible-plugin": "^1.0.5",
|
|
72
|
+
"just-compact": "^3.2.0",
|
|
73
|
+
"just-pick": "^4.2.0",
|
|
74
|
+
"just-safe-get": "^4.2.0",
|
|
75
|
+
"just-unique": "^4.2.0"
|
|
38
76
|
},
|
|
39
|
-
"contributors": [
|
|
40
|
-
{
|
|
41
|
-
"name": "Liam Martens",
|
|
42
|
-
"email": "liam@freighter.studio",
|
|
43
|
-
"url": "https://github.com/LiamMartens"
|
|
44
|
-
}
|
|
45
|
-
],
|
|
46
|
-
"keywords": [
|
|
47
|
-
"sanity-plugin",
|
|
48
|
-
"autocomplete",
|
|
49
|
-
"v4",
|
|
50
|
-
"v5",
|
|
51
|
-
"maintained",
|
|
52
|
-
"input",
|
|
53
|
-
"sanity-v5"
|
|
54
|
-
],
|
|
55
77
|
"devDependencies": {
|
|
78
|
+
"@eslint/eslintrc": "^3.2.0",
|
|
56
79
|
"@eslint/js": "^9.39.2",
|
|
80
|
+
"@sanity/pkg-utils": "^10.2.3",
|
|
81
|
+
"@sanity/plugin-kit": "^4.0.20",
|
|
57
82
|
"@sanity/ui": "^3.1.11",
|
|
58
|
-
"@
|
|
59
|
-
"@swc/core": "^1.3.99",
|
|
60
|
-
"@types/node": "^20.19.0",
|
|
61
|
-
"@types/react": "^18.2.38",
|
|
62
|
-
"@types/react-dom": "^18.2.17",
|
|
83
|
+
"@types/react": "^19.2.7",
|
|
63
84
|
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
|
64
85
|
"@typescript-eslint/parser": "^8.50.1",
|
|
65
86
|
"eslint": "^9.39.2",
|
|
66
87
|
"eslint-config-prettier": "^10.1.8",
|
|
88
|
+
"eslint-config-sanity": "^7.1.4",
|
|
89
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
67
90
|
"eslint-plugin-react": "^7.37.5",
|
|
68
91
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
69
|
-
"
|
|
92
|
+
"prettier": "^3.7.4",
|
|
93
|
+
"prettier-plugin-packagejson": "^2.5.20",
|
|
70
94
|
"react": "^19.2.3",
|
|
71
95
|
"react-dom": "^19.2.3",
|
|
72
96
|
"sanity": "^5.1.0",
|
|
73
97
|
"styled-components": "^6.1.19",
|
|
74
|
-
"typescript": "
|
|
98
|
+
"typescript": "^5.9.3"
|
|
75
99
|
},
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
|
|
100
|
+
"peerDependencies": {
|
|
101
|
+
"@sanity/ui": "^2 || ^3",
|
|
102
|
+
"react": "^18 || ^19",
|
|
103
|
+
"sanity": "^4 || ^5"
|
|
104
|
+
},
|
|
105
|
+
"engines": {
|
|
106
|
+
"node": ">=18"
|
|
107
|
+
},
|
|
108
|
+
"browserslist": "extends @sanity/browserslist-config",
|
|
109
|
+
"sanityPlugin": {
|
|
110
|
+
"verifyPackage": {
|
|
111
|
+
"eslintImports": false
|
|
112
|
+
}
|
|
81
113
|
}
|
|
82
114
|
}
|
package/sanity.json
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {Autocomplete, Card, Text} from '@sanity/ui'
|
|
2
|
+
import compact from 'just-compact'
|
|
3
|
+
import get from 'just-safe-get'
|
|
4
|
+
import unique from 'just-unique'
|
|
5
|
+
import React, {useCallback, useEffect, useMemo, useState} from 'react'
|
|
6
|
+
import {
|
|
7
|
+
PatchEvent,
|
|
8
|
+
set,
|
|
9
|
+
StringInputProps,
|
|
10
|
+
StringSchemaType,
|
|
11
|
+
unset,
|
|
12
|
+
useClient,
|
|
13
|
+
useFormValue,
|
|
14
|
+
} from 'sanity'
|
|
15
|
+
|
|
16
|
+
import type {InputOptions, Option} from './types/index.js'
|
|
17
|
+
|
|
18
|
+
export type AutocompleteSchemaType = Omit<StringSchemaType, 'options'> & {
|
|
19
|
+
options?: StringSchemaType['options'] & InputOptions
|
|
20
|
+
}
|
|
21
|
+
export type InputProps = StringInputProps<AutocompleteSchemaType>
|
|
22
|
+
|
|
23
|
+
export const AutoCompleteInput = (props: InputProps): React.ReactElement => {
|
|
24
|
+
const {id, schemaType, value, validationError, readOnly, onChange} = props
|
|
25
|
+
|
|
26
|
+
const sanityClient = useClient()
|
|
27
|
+
const documentValue = useFormValue([])
|
|
28
|
+
const [loading, setLoading] = useState(false)
|
|
29
|
+
const [query, setQuery] = React.useState('')
|
|
30
|
+
const [options, setOptions] = React.useState<Option[]>([])
|
|
31
|
+
const canCreateNew = schemaType.options?.disableNew !== true
|
|
32
|
+
|
|
33
|
+
const optionsList = useMemo<(Option & {isNew?: boolean})[]>(() => {
|
|
34
|
+
const uniqueOptions = unique(
|
|
35
|
+
options.map(({value: optionValue}) => optionValue),
|
|
36
|
+
false,
|
|
37
|
+
true,
|
|
38
|
+
)
|
|
39
|
+
const queryInOptions = uniqueOptions.find((optionValue) => optionValue === query)
|
|
40
|
+
if (!queryInOptions && canCreateNew) {
|
|
41
|
+
return [
|
|
42
|
+
...uniqueOptions.map((optionValue) => ({value: optionValue})),
|
|
43
|
+
{value: query, isNew: true},
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return uniqueOptions.map((optionValue) => ({value: optionValue}))
|
|
48
|
+
}, [query, options, canCreateNew])
|
|
49
|
+
|
|
50
|
+
const handleQueryChange = useCallback((queryValue: string | null) => {
|
|
51
|
+
setQuery(queryValue ?? '')
|
|
52
|
+
}, [])
|
|
53
|
+
|
|
54
|
+
const handleChange = useCallback(
|
|
55
|
+
(newValue: string) => {
|
|
56
|
+
onChange(PatchEvent.from(newValue ? set(newValue) : unset()))
|
|
57
|
+
},
|
|
58
|
+
[onChange],
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const renderOption = useCallback(
|
|
62
|
+
(option: Option & {isNew?: boolean}) => (
|
|
63
|
+
<Card as="button" padding={3} tone={option.isNew ? 'primary' : 'default'} shadow={1}>
|
|
64
|
+
{option.isNew ? (
|
|
65
|
+
canCreateNew && <Text>Create new option "{option.value}"</Text>
|
|
66
|
+
) : (
|
|
67
|
+
<Text>{option.value}</Text>
|
|
68
|
+
)}
|
|
69
|
+
</Card>
|
|
70
|
+
),
|
|
71
|
+
[canCreateNew],
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (schemaType.options?.options) {
|
|
76
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
77
|
+
setOptions(schemaType.options.options)
|
|
78
|
+
setLoading(false)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const path = schemaType.options?.autocompleteFieldPath ?? 'title'
|
|
83
|
+
const {
|
|
84
|
+
query: groqQuery,
|
|
85
|
+
transform,
|
|
86
|
+
params = {},
|
|
87
|
+
} = schemaType.options?.groq || {
|
|
88
|
+
query: `*[defined(${path})] { "value": ${path} }`,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const resolvedParams =
|
|
92
|
+
typeof params === 'function'
|
|
93
|
+
? params(documentValue as Record<string, unknown> | undefined)
|
|
94
|
+
: params
|
|
95
|
+
|
|
96
|
+
setLoading(true)
|
|
97
|
+
sanityClient.fetch(groqQuery, resolvedParams).then((results) => {
|
|
98
|
+
if (Array.isArray(results)) {
|
|
99
|
+
const transformedResults = transform ? transform(results) : results
|
|
100
|
+
const compactedValues = compact(transformedResults.map((doc) => get(doc, 'value')))
|
|
101
|
+
setOptions(compactedValues.map((optionValue) => ({value: String(optionValue)})))
|
|
102
|
+
setLoading(false)
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
}, [schemaType.options, documentValue, sanityClient])
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<Autocomplete
|
|
109
|
+
id={id}
|
|
110
|
+
readOnly={readOnly ?? false}
|
|
111
|
+
customValidity={validationError}
|
|
112
|
+
loading={loading}
|
|
113
|
+
disabled={loading}
|
|
114
|
+
options={optionsList}
|
|
115
|
+
value={value ?? ''}
|
|
116
|
+
onChange={handleChange}
|
|
117
|
+
onQueryChange={handleQueryChange}
|
|
118
|
+
renderOption={renderOption}
|
|
119
|
+
/>
|
|
120
|
+
)
|
|
121
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {defineType, StringDefinition} from 'sanity'
|
|
2
|
+
|
|
3
|
+
import {AutoCompleteInput} from '../AutoCompleteInput.js'
|
|
4
|
+
import type {InputOptions} from '../types/index.js'
|
|
5
|
+
|
|
6
|
+
const typeName = 'autocomplete' as const
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
export interface AutocompleteStringDefinition extends Omit<
|
|
12
|
+
StringDefinition,
|
|
13
|
+
'type' | 'fields' | 'options'
|
|
14
|
+
> {
|
|
15
|
+
type: typeof typeName
|
|
16
|
+
options?: InputOptions
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare module '@sanity/types' {
|
|
20
|
+
// makes type: 'color' narrow correctly when using defineTyp/defineField/defineArrayMember
|
|
21
|
+
export interface IntrinsicDefinitions {
|
|
22
|
+
autocomplete: AutocompleteStringDefinition
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const autocompleteString = defineType({
|
|
27
|
+
name: typeName,
|
|
28
|
+
type: 'string',
|
|
29
|
+
title: 'Autocomplete',
|
|
30
|
+
components: {input: AutoCompleteInput},
|
|
31
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type {Option} from './Option.js'
|
|
2
|
+
|
|
3
|
+
export type InputOptions<Parent = Record<string, unknown>, Params = Record<string, unknown>> = {
|
|
4
|
+
autocompleteFieldPath?: string
|
|
5
|
+
disableNew?: boolean
|
|
6
|
+
options?: Option[]
|
|
7
|
+
groq?: {
|
|
8
|
+
query: string
|
|
9
|
+
params?: Params | ((parent?: Parent) => Params)
|
|
10
|
+
transform?: (result: unknown) => Option[]
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export type Option = {
|
|
2
|
-
|
|
3
|
-
}
|
|
2
|
+
value: string
|
|
3
|
+
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { StringInputProps, StringSchemaType } from "sanity";
|
|
3
|
-
import type { InputOptions } from "./types";
|
|
4
|
-
export type AutocompleteSchemaType = Omit<StringSchemaType, "options"> & {
|
|
5
|
-
options?: StringSchemaType["options"] & InputOptions;
|
|
6
|
-
};
|
|
7
|
-
export type InputProps = StringInputProps<AutocompleteSchemaType>;
|
|
8
|
-
export declare const AutoCompleteInput: (props: InputProps) => React.JSX.Element;
|
package/lib/AutoCompleteInput.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
-
import get from "just-safe-get";
|
|
3
|
-
import compact from "just-compact";
|
|
4
|
-
import unique from "just-unique";
|
|
5
|
-
import { Autocomplete, Text, Card } from "@sanity/ui";
|
|
6
|
-
import { PatchEvent, set, unset, useClient, useFormValue } from "sanity";
|
|
7
|
-
export const AutoCompleteInput = (props)=>{
|
|
8
|
-
const { id, schemaType, value, validationError, readOnly, onChange } = props;
|
|
9
|
-
const sanityClient = useClient();
|
|
10
|
-
const documentValue = useFormValue([]);
|
|
11
|
-
const [loading, setLoading] = useState(false);
|
|
12
|
-
const [query, setQuery] = React.useState("");
|
|
13
|
-
const [options, setOptions] = React.useState([]);
|
|
14
|
-
const canCreateNew = schemaType.options?.disableNew !== true;
|
|
15
|
-
const optionsList = useMemo(()=>{
|
|
16
|
-
const uniqueOptions = unique(options.map(({ value })=>value), false, true);
|
|
17
|
-
const queryInOptions = uniqueOptions.find((value)=>value === query);
|
|
18
|
-
if (!queryInOptions && canCreateNew) {
|
|
19
|
-
return [
|
|
20
|
-
...uniqueOptions.map((value)=>({
|
|
21
|
-
value
|
|
22
|
-
})),
|
|
23
|
-
{
|
|
24
|
-
value: query,
|
|
25
|
-
isNew: true
|
|
26
|
-
}
|
|
27
|
-
];
|
|
28
|
-
}
|
|
29
|
-
return uniqueOptions.map((value)=>({
|
|
30
|
-
value
|
|
31
|
-
}));
|
|
32
|
-
}, [
|
|
33
|
-
query,
|
|
34
|
-
options,
|
|
35
|
-
canCreateNew
|
|
36
|
-
]);
|
|
37
|
-
const handleQueryChange = useCallback((query)=>{
|
|
38
|
-
setQuery(query ?? "");
|
|
39
|
-
}, []);
|
|
40
|
-
const handleChange = useCallback((value)=>{
|
|
41
|
-
onChange(PatchEvent.from(value ? set(value) : unset()));
|
|
42
|
-
}, [
|
|
43
|
-
onChange
|
|
44
|
-
]);
|
|
45
|
-
useEffect(()=>{
|
|
46
|
-
if (schemaType.options?.options) {
|
|
47
|
-
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
48
|
-
setOptions(schemaType.options.options);
|
|
49
|
-
setLoading(false);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
const path = schemaType.options?.autocompleteFieldPath ?? "title";
|
|
53
|
-
const { query, transform, params = {} } = schemaType.options?.groq || {
|
|
54
|
-
query: `*[defined(${path})] { "value": ${path} }`
|
|
55
|
-
};
|
|
56
|
-
const resolvedParams = typeof params === "function" ? params(documentValue) : params;
|
|
57
|
-
setLoading(true);
|
|
58
|
-
sanityClient.fetch(query, resolvedParams).then((results)=>{
|
|
59
|
-
if (Array.isArray(results)) {
|
|
60
|
-
const transformedResults = transform ? transform(results) : results;
|
|
61
|
-
const compactedValues = compact(transformedResults.map((doc)=>get(doc, "value")));
|
|
62
|
-
setOptions(compactedValues.map((value)=>({
|
|
63
|
-
value: String(value)
|
|
64
|
-
})));
|
|
65
|
-
setLoading(false);
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
}, [
|
|
69
|
-
schemaType.options,
|
|
70
|
-
documentValue,
|
|
71
|
-
sanityClient
|
|
72
|
-
]);
|
|
73
|
-
return /*#__PURE__*/ React.createElement(Autocomplete, {
|
|
74
|
-
id: id,
|
|
75
|
-
readOnly: readOnly ?? false,
|
|
76
|
-
customValidity: validationError,
|
|
77
|
-
loading: loading,
|
|
78
|
-
disabled: loading,
|
|
79
|
-
options: optionsList,
|
|
80
|
-
value: value ?? "",
|
|
81
|
-
onChange: handleChange,
|
|
82
|
-
onQueryChange: handleQueryChange,
|
|
83
|
-
renderOption: (option)=>/*#__PURE__*/ React.createElement(Card, {
|
|
84
|
-
as: "button",
|
|
85
|
-
padding: 3,
|
|
86
|
-
tone: option.isNew ? "primary" : "default",
|
|
87
|
-
shadow: 1
|
|
88
|
-
}, option.isNew ? canCreateNew && /*#__PURE__*/ React.createElement(Text, null, 'Create new option "', option.value, '"') : /*#__PURE__*/ React.createElement(Text, null, option.value))
|
|
89
|
-
});
|
|
90
|
-
};
|
package/lib/index.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const autocompletInput: import("sanity").Plugin<void>;
|
package/lib/index.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { StringDefinition } from "sanity";
|
|
2
|
-
import type { InputOptions } from "../types";
|
|
3
|
-
declare const typeName: "autocomplete";
|
|
4
|
-
/**
|
|
5
|
-
* @public
|
|
6
|
-
*/
|
|
7
|
-
export interface AutocompleteStringDefinition extends Omit<StringDefinition, "type" | "fields" | "options"> {
|
|
8
|
-
type: typeof typeName;
|
|
9
|
-
options?: InputOptions;
|
|
10
|
-
}
|
|
11
|
-
declare module "@sanity/types" {
|
|
12
|
-
interface IntrinsicDefinitions {
|
|
13
|
-
autocomplete: AutocompleteStringDefinition;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
export declare const autocompleteString: {
|
|
17
|
-
type: "string";
|
|
18
|
-
name: "autocomplete";
|
|
19
|
-
} & Omit<StringDefinition, "preview">;
|
|
20
|
-
export {};
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { defineType } from "sanity";
|
|
2
|
-
import { AutoCompleteInput } from "../AutoCompleteInput";
|
|
3
|
-
const typeName = "autocomplete";
|
|
4
|
-
export const autocompleteString = defineType({
|
|
5
|
-
name: typeName,
|
|
6
|
-
type: "string",
|
|
7
|
-
title: "Autocomplete",
|
|
8
|
-
components: {
|
|
9
|
-
input: AutoCompleteInput
|
|
10
|
-
}
|
|
11
|
-
});
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { Option } from "./Option";
|
|
2
|
-
export type InputOptions<Parent = Record<string, unknown>, Params = Record<string, unknown>> = {
|
|
3
|
-
autocompleteFieldPath?: string;
|
|
4
|
-
disableNew?: boolean;
|
|
5
|
-
options?: Option[];
|
|
6
|
-
groq?: {
|
|
7
|
-
query: string;
|
|
8
|
-
params?: Params | ((parent?: Parent) => Params);
|
|
9
|
-
transform?: (result: unknown) => Option[];
|
|
10
|
-
};
|
|
11
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|
package/lib/types/Option.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|
package/lib/types/index.d.ts
DELETED
package/lib/types/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|