@srothgan/sanity-plugin-autocomplete-input 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # @srothgan/sanity-plugin-autocomplete-input
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@srothgan/sanity-plugin-autocomplete-input)](https://www.npmjs.com/package/@srothgan/sanity-plugin-autocomplete-input)
4
+ [![Original Repository](https://img.shields.io/badge/original-LiamMartens%2Fsanity--plugin--autocomplete--input-blue)](https://github.com/LiamMartens/sanity-plugin-autocomplete-input)
5
+
6
+ ![example](https://raw.githubusercontent.com/LiamMartens/sanity-plugin-autocomplete-input/main/docs/img/example.gif)
7
+
8
+ This plugin is similar to the [Autocomplete Tags Plugin](https://www.sanity.io/plugins/autocomplete-tags), but it acts as a single text input as opposed to an array of tags. The input can also be customized to change the autocomplete options.
9
+
10
+ **Successor to [sanity-plugin-autocomplete-input](https://github.com/LiamMartens/sanity-plugin-autocomplete-input)**
11
+ Original by [@LiamMartens](https://github.com/LiamMartens) (last updated 2023).
12
+ This maintained fork adds Sanity v4 & v5 compatibility and modern tooling.
13
+
14
+ > **Note**: A PR with these changes was submitted to the original repository.
15
+ > If the original becomes active again, this fork may redirect users back.
16
+
17
+ ## Compatibility
18
+
19
+ - **v3.x:** Sanity Studio v4 & v5 (Node.js v20.19+, React 18 or 19)
20
+ - Fully compatible with both Sanity v4 and v5
21
+ - Supports React 18 (Sanity v4) and React 19 (Sanity v5)
22
+ - **v2.x:** Sanity Studio v3 (Node.js v18+, React 18)
23
+ - **v1.x:** Sanity Studio v2
24
+
25
+ ## Installation
26
+
27
+ ```
28
+ npm install --save @srothgan/sanity-plugin-autocomplete-input
29
+ ```
30
+
31
+ or
32
+
33
+ ```
34
+ yarn add @srothgan/sanity-plugin-autocomplete-input
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ Add it as a plugin in sanity.config.ts (or .js):
40
+
41
+ ```js
42
+ import { autocompletInput } from "@srothgan/sanity-plugin-autocomplete-input";
43
+
44
+ export default defineConfig({
45
+ // ...
46
+ plugins: [autocompletInput()],
47
+ });
48
+ ```
49
+
50
+ You can just use it as a schema type. To customize the autocomplete list you have 3 options:
51
+
52
+ 1. Specify the `autocompleteFieldPath` option, which the plugin will use to look for documents with the same field path to aggregate the option values.
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)
55
+
56
+ ```javascript
57
+ export default {
58
+ fields: [
59
+ {
60
+ name: "autocomplete-input",
61
+ type: "autocomplete",
62
+ options: {
63
+ // specify field path
64
+ autocompleteFieldPath: "title",
65
+ // this option can be used to disable using "new" values
66
+ disableNew: false,
67
+ // manually specify options
68
+ options: [{ value: "Option 1" }, { value: "Option 2" }],
69
+ // specify custom groq query
70
+ groq: {
71
+ query: '*[_type == $type] { "value": title }',
72
+ params: {
73
+ type: "page",
74
+ },
75
+ transform: (values) => values,
76
+ },
77
+ },
78
+ },
79
+ ],
80
+ };
81
+ ```
82
+
83
+ ### Advanced GROQ parameters
84
+
85
+ It is also possible to refer to the current parent value (for a top-level field this would be the current document) by passing a function to the `params` option:
86
+
87
+ ```javascript
88
+ export default {
89
+ fields: [
90
+ {
91
+ name: "autocomplete-input",
92
+ type: "autocomplete",
93
+ options: {
94
+ groq: {
95
+ query: "*[_id != $docId]",
96
+ params: (parent) => ({
97
+ docId: parent?._id,
98
+ }),
99
+ },
100
+ },
101
+ },
102
+ ],
103
+ };
104
+ ```
105
+
106
+ ## Differences from Original
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.
@@ -0,0 +1,8 @@
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;
@@ -0,0 +1,87 @@
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
+ setLoading(false);
48
+ setOptions(schemaType.options.options);
49
+ } else {
50
+ const path = schemaType.options?.autocompleteFieldPath ?? "title";
51
+ const { query, transform, params = {} } = schemaType.options?.groq || {
52
+ query: `*[defined(${path})] { "value": ${path} }`
53
+ };
54
+ const resolvedParams = typeof params === "function" ? params(documentValue) : params;
55
+ sanityClient.fetch(query, resolvedParams).then((results)=>{
56
+ if (Array.isArray(results)) {
57
+ const transformedResults = transform ? transform(results) : results;
58
+ const compactedValues = compact(transformedResults.map((doc)=>get(doc, "value")));
59
+ setLoading(false);
60
+ setOptions(compactedValues.map((value)=>({
61
+ value: String(value)
62
+ })));
63
+ }
64
+ });
65
+ }
66
+ }, [
67
+ query,
68
+ schemaType.options
69
+ ]);
70
+ return /*#__PURE__*/ React.createElement(Autocomplete, {
71
+ id: id,
72
+ readOnly: readOnly ?? false,
73
+ customValidity: validationError,
74
+ loading: loading,
75
+ disabled: loading,
76
+ options: optionsList,
77
+ value: value ?? "",
78
+ onChange: handleChange,
79
+ onQueryChange: handleQueryChange,
80
+ renderOption: (option)=>/*#__PURE__*/ React.createElement(Card, {
81
+ as: "button",
82
+ padding: 3,
83
+ tone: option.isNew ? "primary" : "default",
84
+ shadow: 1
85
+ }, option.isNew ? canCreateNew && /*#__PURE__*/ React.createElement(Text, null, 'Create new option "', option.value, '"') : /*#__PURE__*/ React.createElement(Text, null, option.value))
86
+ });
87
+ };
package/lib/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare const autocompletInput: import("sanity").Plugin<void>;
package/lib/index.js ADDED
@@ -0,0 +1,10 @@
1
+ import { definePlugin } from "sanity";
2
+ import { autocompleteString } from "./schemas/autocompleteString";
3
+ export const autocompletInput = definePlugin({
4
+ name: "sanity-plugin-autocomplete-input",
5
+ schema: {
6
+ types: [
7
+ autocompleteString
8
+ ]
9
+ }
10
+ });
@@ -0,0 +1,20 @@
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 {};
@@ -0,0 +1,11 @@
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
+ });
@@ -0,0 +1,87 @@
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
+ setLoading(false);
48
+ setOptions(schemaType.options.options);
49
+ } else {
50
+ const path = schemaType.options?.autocompleteFieldPath ?? "title";
51
+ const { query, transform, params = {} } = schemaType.options?.groq || {
52
+ query: `*[defined(${path})] { "value": ${path} }`
53
+ };
54
+ const resolvedParams = typeof params === "function" ? params(documentValue) : params;
55
+ sanityClient.fetch(query, resolvedParams).then((results)=>{
56
+ if (Array.isArray(results)) {
57
+ const transformedResults = transform ? transform(results) : results;
58
+ const compactedValues = compact(transformedResults.map((doc)=>get(doc, "value")));
59
+ setLoading(false);
60
+ setOptions(compactedValues.map((value)=>({
61
+ value: String(value)
62
+ })));
63
+ }
64
+ });
65
+ }
66
+ }, [
67
+ query,
68
+ schemaType.options
69
+ ]);
70
+ return /*#__PURE__*/ React.createElement(Autocomplete, {
71
+ id: id,
72
+ readOnly: readOnly ?? false,
73
+ customValidity: validationError,
74
+ loading: loading,
75
+ disabled: loading,
76
+ options: optionsList,
77
+ value: value ?? "",
78
+ onChange: handleChange,
79
+ onQueryChange: handleQueryChange,
80
+ renderOption: (option)=>/*#__PURE__*/ React.createElement(Card, {
81
+ as: "button",
82
+ padding: 3,
83
+ tone: option.isNew ? "primary" : "default",
84
+ shadow: 1
85
+ }, option.isNew ? canCreateNew && /*#__PURE__*/ React.createElement(Text, null, 'Create new option "', option.value, '"') : /*#__PURE__*/ React.createElement(Text, null, option.value))
86
+ });
87
+ };
@@ -0,0 +1,10 @@
1
+ import { definePlugin } from "sanity";
2
+ import { autocompleteString } from "./schemas/autocompleteString";
3
+ export const autocompletInput = definePlugin({
4
+ name: "sanity-plugin-autocomplete-input",
5
+ schema: {
6
+ types: [
7
+ autocompleteString
8
+ ]
9
+ }
10
+ });
@@ -0,0 +1,11 @@
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
+ });
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,11 @@
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
+ };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,3 @@
1
+ export type Option = {
2
+ value: string;
3
+ };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,2 @@
1
+ export type { InputOptions } from "./InputOptions";
2
+ export type { Option } from "./Option";
@@ -0,0 +1 @@
1
+ export { };
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@srothgan/sanity-plugin-autocomplete-input",
3
+ "version": "3.0.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "types": "./lib/index.d.ts",
7
+ "module": "./lib/index.js",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "import": "./lib/index.js",
12
+ "default": "./lib/index.js"
13
+ }
14
+ },
15
+ "browserslist": "> 0.5%, last 2 versions, not dead",
16
+ "files": [
17
+ "lib"
18
+ ],
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/srothgan/srothgan-sanity-plugin-autocomplete-input"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/srothgan/srothgan-sanity-plugin-autocomplete-input/issues"
25
+ },
26
+ "author": {
27
+ "name": "Simon Rothgang",
28
+ "email": "simonrothgang@gmail.com"
29
+ },
30
+ "contributors": [
31
+ {
32
+ "name": "Liam Martens",
33
+ "email": "liam@freighter.studio",
34
+ "url": "https://github.com/LiamMartens"
35
+ }
36
+ ],
37
+ "keywords": ["sanity-plugin", "autocomplete", "v4", "v5", "maintained", "input", "sanity-v5"],
38
+ "devDependencies": {
39
+ "@sanity/ui": "^3.1.11",
40
+ "@swc/cli": "^0.7.9",
41
+ "@swc/core": "^1.3.99",
42
+ "@types/node": "^20.19.0",
43
+ "@types/react": "^18.2.38",
44
+ "@types/react-dom": "^18.2.17",
45
+ "react": "^19.2.3",
46
+ "react-dom": "^19.2.3",
47
+ "sanity": "^5.1.0",
48
+ "styled-components": "^6.1.19",
49
+ "typescript": "~5.3.2"
50
+ },
51
+ "dependencies": {
52
+ "just-compact": "^3.2.0",
53
+ "just-pick": "^4.2.0",
54
+ "just-safe-get": "^4.2.0",
55
+ "just-unique": "^4.2.0",
56
+ "npm-run-all": "^4.1.5"
57
+ },
58
+ "scripts": {
59
+ "build:js": "swc ./src -d ./lib --strip-leading-paths",
60
+ "build:types": "tsc -p .",
61
+ "build": "run-p build:*"
62
+ }
63
+ }