@sanity/assist 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +205 -0
  3. package/dist/index.d.ts +52 -0
  4. package/dist/index.esm.js +2341 -0
  5. package/dist/index.esm.js.map +1 -0
  6. package/dist/index.js +2341 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +98 -0
  9. package/sanity.json +8 -0
  10. package/src/_lib/connector/ConnectFromRegion.tsx +24 -0
  11. package/src/_lib/connector/ConnectToRegion.tsx +22 -0
  12. package/src/_lib/connector/ConnectorRegion.tsx +23 -0
  13. package/src/_lib/connector/ConnectorsProvider.tsx +19 -0
  14. package/src/_lib/connector/ConnectorsStore.ts +122 -0
  15. package/src/_lib/connector/ConnectorsStoreContext.ts +4 -0
  16. package/src/_lib/connector/helpers.ts +5 -0
  17. package/src/_lib/connector/index.ts +9 -0
  18. package/src/_lib/connector/mapConnectorToLine.ts +83 -0
  19. package/src/_lib/connector/types.ts +56 -0
  20. package/src/_lib/connector/useConnectorsStore.ts +13 -0
  21. package/src/_lib/connector/useRegionRects.ts +141 -0
  22. package/src/_lib/fixedListenQuery.ts +101 -0
  23. package/src/_lib/form/DocumentForm.tsx +197 -0
  24. package/src/_lib/form/helpers.ts +31 -0
  25. package/src/_lib/form/index.ts +1 -0
  26. package/src/_lib/randomKey.ts +29 -0
  27. package/src/_lib/useListeningQuery.ts +61 -0
  28. package/src/_lib/usePrevious.ts +9 -0
  29. package/src/assistConnectors/AssistConnectorsOverlay.tsx +132 -0
  30. package/src/assistConnectors/ConnectorPath.tsx +62 -0
  31. package/src/assistConnectors/draw/arrowPath.ts +9 -0
  32. package/src/assistConnectors/draw/connectorPath.ts +142 -0
  33. package/src/assistConnectors/index.ts +1 -0
  34. package/src/assistDocument/AssistDocumentContext.tsx +31 -0
  35. package/src/assistDocument/AssistDocumentContextProvider.tsx +17 -0
  36. package/src/assistDocument/AssistDocumentInput.tsx +46 -0
  37. package/src/assistDocument/RequestRunInstructionProvider.tsx +50 -0
  38. package/src/assistDocument/components/AssistDocumentForm.tsx +188 -0
  39. package/src/assistDocument/components/FieldRefPreview.tsx +27 -0
  40. package/src/assistDocument/components/InstructionsArrayField.tsx +8 -0
  41. package/src/assistDocument/components/InstructionsArrayInput.tsx +26 -0
  42. package/src/assistDocument/components/SelectedFieldContext.tsx +10 -0
  43. package/src/assistDocument/components/generic/HiddenFieldTitle.tsx +5 -0
  44. package/src/assistDocument/components/helpers.ts +21 -0
  45. package/src/assistDocument/components/instruction/BackToInstructionsLink.tsx +31 -0
  46. package/src/assistDocument/components/instruction/FieldRefInput.tsx +33 -0
  47. package/src/assistDocument/components/instruction/InstructionInput.tsx +87 -0
  48. package/src/assistDocument/components/instruction/PromptInput.tsx +52 -0
  49. package/src/assistDocument/components/instruction/appearance/IconInput.tsx +46 -0
  50. package/src/assistDocument/components/instruction/appearance/InstructionVisibility.tsx +37 -0
  51. package/src/assistDocument/hooks/useAssistDocumentContextValue.tsx +68 -0
  52. package/src/assistDocument/hooks/useDocumentState.ts +6 -0
  53. package/src/assistDocument/hooks/useInstructionToaster.tsx +74 -0
  54. package/src/assistDocument/hooks/useStudioAssistDocument.ts +119 -0
  55. package/src/assistDocument/index.ts +1 -0
  56. package/src/assistFormComponents/AssistField.tsx +51 -0
  57. package/src/assistFormComponents/AssistFormBlock.tsx +31 -0
  58. package/src/assistFormComponents/AssistInlineFormBlock.tsx +14 -0
  59. package/src/assistFormComponents/AssistItem.tsx +20 -0
  60. package/src/assistFormComponents/validation/listItem.tsx +63 -0
  61. package/src/assistFormComponents/validation/validationList.tsx +89 -0
  62. package/src/assistInspector/AssistInspector.tsx +379 -0
  63. package/src/assistInspector/FieldAutocomplete.tsx +119 -0
  64. package/src/assistInspector/InstructionTaskHistoryButton.tsx +261 -0
  65. package/src/assistInspector/constants.ts +1 -0
  66. package/src/assistInspector/helpers.ts +125 -0
  67. package/src/assistInspector/index.ts +26 -0
  68. package/src/assistLayout/AiAssistanceConfigContext.tsx +81 -0
  69. package/src/assistLayout/AlphaMigration.tsx +311 -0
  70. package/src/assistLayout/AssistLayout.tsx +38 -0
  71. package/src/assistLayout/RunInstructionProvider.tsx +222 -0
  72. package/src/components/AssistFeatureBadge.tsx +9 -0
  73. package/src/components/Delay.tsx +25 -0
  74. package/src/components/HideReferenceChangedBannerInput.tsx +25 -0
  75. package/src/components/SafeValueInput.tsx +73 -0
  76. package/src/components/TimeAgo.tsx +18 -0
  77. package/src/constants.ts +20 -0
  78. package/src/fieldActions/PrivateIcon.tsx +20 -0
  79. package/src/fieldActions/assistFieldActions.tsx +230 -0
  80. package/src/globals.d.ts +4 -0
  81. package/src/helpers/assistSupported.ts +44 -0
  82. package/src/helpers/ids.ts +19 -0
  83. package/src/helpers/misc.ts +16 -0
  84. package/src/helpers/typeUtils.ts +15 -0
  85. package/src/helpers/useAssistSupported.ts +10 -0
  86. package/src/index.ts +6 -0
  87. package/src/legacy-types.ts +72 -0
  88. package/src/onboarding/FieldActionsOnboarding.tsx +90 -0
  89. package/src/onboarding/FirstAssistedPathProvider.tsx +29 -0
  90. package/src/onboarding/InspectorOnboarding.tsx +46 -0
  91. package/src/onboarding/onboardingStore.ts +33 -0
  92. package/src/plugin.tsx +80 -0
  93. package/src/presence/AiFieldPresence.tsx +28 -0
  94. package/src/presence/AssistAvatar.tsx +96 -0
  95. package/src/presence/AssistDocumentPresence.tsx +58 -0
  96. package/src/presence/useAssistPresence.ts +61 -0
  97. package/src/schemas/assistDocumentSchema.tsx +450 -0
  98. package/src/schemas/contextDocumentSchema.tsx +56 -0
  99. package/src/schemas/index.ts +25 -0
  100. package/src/schemas/serialize/SchemTypeTool.tsx +102 -0
  101. package/src/schemas/serialize/schemaUtils.ts +37 -0
  102. package/src/schemas/serialize/serializeSchema.test.ts +382 -0
  103. package/src/schemas/serialize/serializeSchema.ts +162 -0
  104. package/src/schemas/serializedSchemaTypeSchema.ts +59 -0
  105. package/src/schemas/typeDefExtensions.ts +30 -0
  106. package/src/types.ts +167 -0
  107. package/src/useApiClient.ts +140 -0
  108. package/src/vite.config.ts +9 -0
  109. package/v2-incompatible.js +11 -0
@@ -0,0 +1,89 @@
1
+ import {useCallback} from 'react'
2
+ import {
3
+ isValidationErrorMarker,
4
+ isValidationInfoMarker,
5
+ isValidationWarningMarker,
6
+ ObjectSchemaType,
7
+ Path,
8
+ ValidationMarker,
9
+ } from 'sanity'
10
+ import {Container} from '@sanity/ui'
11
+ import {ListItem} from './listItem'
12
+
13
+ /** @internal */
14
+ export interface ValidationListProps {
15
+ documentType?: ObjectSchemaType
16
+ kind?: 'simple'
17
+ validation: ValidationMarker[]
18
+ onFocus?: (path: Path) => void
19
+ onClose?: () => void
20
+ truncate?: boolean
21
+ }
22
+
23
+ export function ValidationList(props: ValidationListProps) {
24
+ const {documentType, kind, validation, onFocus, onClose, truncate} = props
25
+ const errors = validation.filter(isValidationErrorMarker)
26
+ const warnings = validation.filter(isValidationWarningMarker)
27
+ const info = validation.filter(isValidationInfoMarker)
28
+
29
+ const handleClick = useCallback(
30
+ (path: Path = []) => {
31
+ if (onFocus) onFocus(path)
32
+ if (onClose) onClose()
33
+ },
34
+ [onFocus, onClose]
35
+ )
36
+
37
+ const resolvePathTitle = (path: Path) => {
38
+ const fields = documentType && documentType.fields
39
+ const field = fields && fields.find((curr) => curr.name === path[0])
40
+
41
+ return (field && field.type.title) || ''
42
+ }
43
+
44
+ const hasErrors = errors.length > 0
45
+ const hasWarnings = warnings.length > 0
46
+ const hasInfo = info.length > 0
47
+
48
+ if (!hasErrors && !hasWarnings && !hasInfo) {
49
+ return null
50
+ }
51
+
52
+ return (
53
+ <Container width={0} data-kind={kind} data-testid="validation-list">
54
+ {hasErrors &&
55
+ errors.map((_error, i) => (
56
+ <ListItem
57
+ // eslint-disable-next-line react/no-array-index-key
58
+ key={i}
59
+ truncate={truncate}
60
+ path={resolvePathTitle(_error.path)}
61
+ marker={_error}
62
+ onClick={handleClick}
63
+ />
64
+ ))}
65
+ {hasWarnings &&
66
+ warnings.map((_warning, i) => (
67
+ <ListItem
68
+ // eslint-disable-next-line react/no-array-index-key
69
+ key={i}
70
+ truncate={truncate}
71
+ path={resolvePathTitle(_warning.path)}
72
+ marker={_warning}
73
+ onClick={handleClick}
74
+ />
75
+ ))}
76
+ {hasInfo &&
77
+ info.map((_info, i) => (
78
+ <ListItem
79
+ // eslint-disable-next-line react/no-array-index-key
80
+ key={i}
81
+ truncate={truncate}
82
+ path={resolvePathTitle(_info.path)}
83
+ marker={_info}
84
+ onClick={handleClick}
85
+ />
86
+ ))}
87
+ </Container>
88
+ )
89
+ }
@@ -0,0 +1,379 @@
1
+ import {ArrowRightIcon, CloseIcon, PlayIcon, RetryIcon} from '@sanity/icons'
2
+ import {Box, Button, Card, Flex, Spinner, Stack, Text} from '@sanity/ui'
3
+ import {useCallback, useMemo, useState} from 'react'
4
+ import {
5
+ DocumentInspectorProps,
6
+ PresenceOverlay,
7
+ useEditState,
8
+ VirtualizerScrollInstanceProvider,
9
+ } from 'sanity'
10
+ import {
11
+ DocumentInspectorHeader,
12
+ DocumentPaneNode,
13
+ DocumentPaneProvider,
14
+ useDocumentPane,
15
+ } from 'sanity/desk'
16
+ import {DocumentForm} from '../_lib/form'
17
+ import {assistDocumentTypeName, fieldPathParam, instructionParam} from '../types'
18
+ import {getFieldTitle, useAiPaneRouter, useSelectedField} from './helpers'
19
+ import styled from 'styled-components'
20
+ import {useStudioAssistDocument} from '../assistDocument/hooks/useStudioAssistDocument'
21
+ import {InstructionTaskHistoryButton} from './InstructionTaskHistoryButton'
22
+ import {useAiAssistanceConfig} from '../assistLayout/AiAssistanceConfigContext'
23
+ import {giveFeedbackUrl, pluginTitle, releaseAnnouncementUrl, salesUrl} from '../constants'
24
+ import {assistDocumentId} from '../helpers/ids'
25
+ import {
26
+ getAssistableDocId,
27
+ isDocAssistable,
28
+ useRequestRunInstruction,
29
+ } from '../assistDocument/RequestRunInstructionProvider'
30
+ import {InspectorOnboarding} from '../onboarding/InspectorOnboarding'
31
+ import {inspectorOnboardingKey, useOnboardingFeature} from '../onboarding/onboardingStore'
32
+
33
+ const CardWithShadowBelow = styled(Card)`
34
+ position: relative;
35
+
36
+ &:after {
37
+ content: '';
38
+ display: block;
39
+ position: absolute;
40
+ left: 0;
41
+ right: 0;
42
+ bottom: -1px;
43
+ border-bottom: 1px solid var(--card-border-color);
44
+ opacity: 0.5;
45
+ z-index: 100;
46
+ }
47
+ `
48
+
49
+ const CardWithShadowAbove = styled(Card)`
50
+ position: relative;
51
+
52
+ &:after {
53
+ content: '';
54
+ display: block;
55
+ position: absolute;
56
+ left: 0;
57
+ right: 0;
58
+ top: -1px;
59
+ border-top: 1px solid var(--card-border-color);
60
+ opacity: 0.5;
61
+ z-index: 100;
62
+ }
63
+ `
64
+
65
+ export function AssistInspectorWrapper(props: DocumentInspectorProps) {
66
+ const context = useAiAssistanceConfig()
67
+
68
+ if (context.statusLoading) {
69
+ return (
70
+ <Flex align="center" height="fill" justify="center" padding={4} sizing="border">
71
+ <Stack space={3} style={{textAlign: 'center'}}>
72
+ <Spinner muted />
73
+ <Text muted size={1}>
74
+ Loading {pluginTitle}...
75
+ </Text>
76
+ </Stack>
77
+ </Flex>
78
+ )
79
+ }
80
+
81
+ const status = context.status
82
+
83
+ if (!status?.enabled) {
84
+ return (
85
+ <Flex direction="column" height="fill">
86
+ <DocumentInspectorHeader
87
+ closeButtonLabel="Close"
88
+ onClose={props.onClose}
89
+ title={pluginTitle}
90
+ />
91
+
92
+ <Stack flex={1} overflow="auto" padding={4} space={3}>
93
+ <Text as="p" size={1} weight="semibold">
94
+ {pluginTitle} is not available
95
+ </Text>
96
+
97
+ <Text as="p" muted size={1}>
98
+ Please get in touch with a Sanity account manager or{' '}
99
+ <a href={salesUrl} target="_blank" rel="noreferrer">
100
+ contact our sales team
101
+ </a>{' '}
102
+ to get started with {pluginTitle}.{' '}
103
+ <a href={releaseAnnouncementUrl} target="_blank" rel="noreferrer">
104
+ Learn more &rarr;
105
+ </a>
106
+ </Text>
107
+ </Stack>
108
+ </Flex>
109
+ )
110
+ }
111
+
112
+ if (!status?.initialized || !status.validToken) {
113
+ return (
114
+ <Flex direction="column" height="fill">
115
+ <DocumentInspectorHeader
116
+ closeButtonLabel="Close"
117
+ onClose={props.onClose}
118
+ title={pluginTitle}
119
+ />
120
+
121
+ <Stack padding={4} space={3}>
122
+ {context.error ? (
123
+ <Text size={1} weight="semibold">
124
+ Failed to start {pluginTitle}
125
+ </Text>
126
+ ) : null}
127
+
128
+ {!context.error && !status?.initialized ? (
129
+ <Text size={1} weight="semibold">
130
+ {pluginTitle} is not enabled
131
+ </Text>
132
+ ) : null}
133
+
134
+ {!context.error && status?.initialized && !status.validToken ? (
135
+ <>
136
+ <Text size={1} weight="semibold">
137
+ Invalid token
138
+ </Text>
139
+ <Text muted size={1}>
140
+ The token used by the AI Assistant is not valid and has to be regenerated.
141
+ </Text>
142
+ </>
143
+ ) : null}
144
+
145
+ {context.error && (
146
+ <Text muted size={1}>
147
+ Something went wrong. See console for details.
148
+ </Text>
149
+ )}
150
+
151
+ {!context.error && !status?.initialized && (
152
+ <Text size={1} muted>
153
+ Please enable {pluginTitle}.
154
+ </Text>
155
+ )}
156
+
157
+ <Button
158
+ fontSize={1}
159
+ icon={
160
+ context.initLoading ? (
161
+ <Box marginTop={1}>
162
+ <Spinner />
163
+ </Box>
164
+ ) : context.error ? (
165
+ RetryIcon
166
+ ) : undefined
167
+ }
168
+ text={
169
+ context.error
170
+ ? 'Retry'
171
+ : status?.initialized && !status.validToken
172
+ ? `Restore ${pluginTitle}`
173
+ : `Enable ${pluginTitle} now`
174
+ }
175
+ tone="primary"
176
+ onClick={context.init}
177
+ disabled={context.initLoading}
178
+ />
179
+ </Stack>
180
+ </Flex>
181
+ )
182
+ }
183
+
184
+ return <AssistInspector {...props} />
185
+ }
186
+
187
+ export function AssistInspector(props: DocumentInspectorProps) {
188
+ const {params} = useAiPaneRouter()
189
+
190
+ const [boundary, setBoundary] = useState<HTMLDivElement | null>(null)
191
+ const pathKey = params?.[fieldPathParam]
192
+ const instructionKey = params?.[instructionParam]
193
+ const documentPane = useDocumentPane()
194
+ const {documentId, documentType, schemaType, onChange: documentOnChange} = documentPane
195
+ const {published, draft} = useEditState(documentId, documentType, 'low')
196
+
197
+ const assistableDocId = getAssistableDocId(schemaType, documentId)
198
+ const {instructionLoading, requestRunInstruction} = useRequestRunInstruction({
199
+ documentOnChange,
200
+ isDocAssistable: isDocAssistable(schemaType, published, draft),
201
+ })
202
+ const selectedField = useSelectedField(schemaType, params[fieldPathParam])
203
+
204
+ const aiDocId = assistDocumentId(documentType)
205
+
206
+ const assistDocument = useStudioAssistDocument({documentId, schemaType})
207
+ const assistField = assistDocument?.fields?.find((f) => f.path === pathKey)
208
+ const instruction = assistField?.instructions?.find((i) => i._key === instructionKey)
209
+ const tasks = useMemo(
210
+ () =>
211
+ assistDocument?.tasks?.filter((i) => !instructionKey || i.instructionKey === instructionKey),
212
+ [assistDocument?.tasks, instructionKey]
213
+ )
214
+ const instructions = useMemo(
215
+ () => assistDocument?.fields?.flatMap((f) => f.instructions ?? []),
216
+ [assistDocument?.fields]
217
+ )
218
+
219
+ const promptValue = instruction?.prompt
220
+ const isEmptyPrompt = useMemo(() => {
221
+ if (!promptValue?.length) {
222
+ return true
223
+ }
224
+ const firstBlock = promptValue[0] as any
225
+ const children = firstBlock?.children
226
+
227
+ return promptValue.length == 1 && children?.length === 1 && !children?.[0]?.text?.length
228
+ }, [promptValue])
229
+
230
+ const paneNode: DocumentPaneNode = useMemo(
231
+ () => ({
232
+ type: 'document',
233
+ id: aiDocId,
234
+ title: pluginTitle,
235
+ options: {
236
+ id: aiDocId,
237
+ type: assistDocumentTypeName,
238
+ },
239
+ }),
240
+ [aiDocId]
241
+ )
242
+
243
+ const runCurrentInstruction = useCallback(
244
+ () =>
245
+ instruction &&
246
+ pathKey &&
247
+ requestRunInstruction({
248
+ documentId: assistableDocId,
249
+ path: pathKey,
250
+ assistDocumentId: assistDocumentId(documentType),
251
+ instruction,
252
+ }),
253
+ [instruction, pathKey, documentType, assistableDocId, requestRunInstruction]
254
+ )
255
+
256
+ const Region = useCallback((_props: any) => {
257
+ // disabled for now
258
+ /* return (
259
+ <ConnectToRegion
260
+ {..._props}
261
+ _key={`${paneKey}_${selectedField?.key || '_field'}`}
262
+ style={{height: '100%', flex: 1, overflow: 'auto'}}
263
+ />
264
+ )*/
265
+ return <div {..._props} style={{height: '100%', flex: 1, overflow: 'auto'}} />
266
+ }, [])
267
+
268
+ if (!documentId || !schemaType || schemaType.jsonType !== 'object') {
269
+ return (
270
+ <Card flex={1} padding={4}>
271
+ <Text>Document not ready yet.</Text>
272
+ </Card>
273
+ )
274
+ }
275
+
276
+ return (
277
+ <Flex
278
+ ref={setBoundary}
279
+ direction="column"
280
+ height="fill"
281
+ overflow="hidden"
282
+ sizing="border"
283
+ style={{lineHeight: 0}}
284
+ >
285
+ <AiInspectorHeader onClose={props.onClose} fieldTitle={getFieldTitle(selectedField)} />
286
+
287
+ <Card as={Region} flex={1} overflow="auto">
288
+ <Flex direction="column" style={{minHeight: '100%'}}>
289
+ <Box flex={1}>
290
+ <PresenceOverlay>
291
+ <Box padding={4}>
292
+ {selectedField && (
293
+ <VirtualizerScrollInstanceProvider scrollElement={boundary}>
294
+ <DocumentPaneProvider
295
+ paneKey={documentPane.paneKey}
296
+ index={documentPane.index}
297
+ itemId="ai"
298
+ pane={paneNode}
299
+ >
300
+ <DocumentForm />
301
+ </DocumentPaneProvider>
302
+ </VirtualizerScrollInstanceProvider>
303
+ )}
304
+ </Box>
305
+ </PresenceOverlay>
306
+ </Box>
307
+
308
+ <Box flex="none" padding={4}>
309
+ <Text muted size={1}>
310
+ How is Sanity AI Assist working for you?{' '}
311
+ <a
312
+ href={giveFeedbackUrl}
313
+ target="_blank"
314
+ rel="noreferrer"
315
+ style={{whiteSpace: 'nowrap'}}
316
+ >
317
+ Let us know <ArrowRightIcon />
318
+ </a>
319
+ </Text>
320
+ </Box>
321
+ </Flex>
322
+ </Card>
323
+
324
+ <CardWithShadowAbove flex="none" paddingX={4} paddingY={3} style={{justifySelf: 'flex-end'}}>
325
+ <Flex gap={2} flex={1} justify="flex-end">
326
+ {schemaType?.name && pathKey && instructionKey && (
327
+ <Stack flex={1}>
328
+ <Button
329
+ mode="ghost"
330
+ disabled={isEmptyPrompt || instructionLoading}
331
+ fontSize={1}
332
+ icon={instructionLoading ? <Spinner /> : PlayIcon}
333
+ onClick={runCurrentInstruction}
334
+ padding={3}
335
+ text={'Run instruction'}
336
+ />
337
+ </Stack>
338
+ )}
339
+
340
+ <InstructionTaskHistoryButton
341
+ documentId={assistableDocId}
342
+ tasks={tasks}
343
+ instructions={instructions}
344
+ showTitles={!instructionKey}
345
+ />
346
+ </Flex>
347
+ </CardWithShadowAbove>
348
+ </Flex>
349
+ )
350
+ }
351
+
352
+ function AiInspectorHeader(props: {fieldTitle: string; onClose: () => void}) {
353
+ const {onClose, fieldTitle} = props
354
+ const {showOnboarding, dismissOnboarding} = useOnboardingFeature(inspectorOnboardingKey)
355
+
356
+ return (
357
+ <CardWithShadowBelow flex="none" padding={2}>
358
+ <Flex flex={1} align="center">
359
+ <Flex flex={1} padding={3} gap={2} align="center">
360
+ <Flex gap={1} align="center">
361
+ <Text size={1} weight="semibold">
362
+ AI instructions for
363
+ </Text>
364
+ <Card radius={2} border padding={1} style={{margin: '-4px 0'}}>
365
+ <Text size={1} weight="semibold">
366
+ {fieldTitle}
367
+ </Text>
368
+ </Card>
369
+ </Flex>
370
+ </Flex>
371
+ <Box flex="none">
372
+ <Button fontSize={1} icon={CloseIcon} mode="bleed" onClick={onClose} />
373
+ </Box>
374
+ </Flex>
375
+
376
+ {showOnboarding && <InspectorOnboarding onDismiss={dismissOnboarding} />}
377
+ </CardWithShadowBelow>
378
+ )
379
+ }
@@ -0,0 +1,119 @@
1
+ import {SearchIcon} from '@sanity/icons'
2
+ import {Autocomplete, Box, Breadcrumbs, Card, Flex, Text} from '@sanity/ui'
3
+ import {createElement, useCallback, useMemo} from 'react'
4
+ import {FieldRef, getFieldRefs, getFieldRefsWithDocument} from './helpers'
5
+ import {ObjectSchemaType} from 'sanity'
6
+ import {isType} from '../helpers/typeUtils'
7
+
8
+ interface FieldSelectorProps {
9
+ id: string
10
+ schemaType: ObjectSchemaType
11
+ fieldPath?: string
12
+ onSelect: (path: string) => void
13
+ includeDocument?: boolean
14
+ }
15
+
16
+ export function FieldAutocomplete(props: FieldSelectorProps) {
17
+ const {id, schemaType, fieldPath, onSelect, includeDocument} = props
18
+
19
+ const fieldNames = useMemo(() => {
20
+ if (includeDocument) {
21
+ return getFieldRefsWithDocument(schemaType)
22
+ }
23
+ return getFieldRefs(schemaType)
24
+ }, [schemaType, includeDocument])
25
+ const currentField = useMemo(
26
+ () => fieldNames.find((f) => f.key === fieldPath),
27
+ [fieldPath, fieldNames]
28
+ )
29
+
30
+ const autocompleteOptions = useMemo(
31
+ () => fieldNames.map((field) => ({value: field.key, field})),
32
+ [fieldNames]
33
+ )
34
+
35
+ const renderOption = useCallback((option: {value: string; field: FieldRef}) => {
36
+ const {value, field} = option
37
+
38
+ if (!value) {
39
+ return (
40
+ <Card as="button" padding={3} radius={1}>
41
+ <Text accent size={1}>
42
+ {option.value}
43
+ </Text>
44
+ </Card>
45
+ )
46
+ }
47
+
48
+ if (isType(field.schemaType, 'document')) {
49
+ return (
50
+ <Card as="button" padding={3} radius={1}>
51
+ <Text size={1} weight="semibold">
52
+ The entire document
53
+ </Text>
54
+ </Card>
55
+ )
56
+ }
57
+
58
+ const splitTitle = field.title.split('/')
59
+ return (
60
+ <Card as="button" padding={3} radius={1}>
61
+ <Flex gap={3}>
62
+ <Text size={1}>{createElement(field.icon)}</Text>
63
+
64
+ <Box flex="none">
65
+ <Breadcrumbs
66
+ separator={
67
+ <Text muted size={1}>
68
+ /
69
+ </Text>
70
+ }
71
+ space={1}
72
+ >
73
+ {splitTitle.slice(0, splitTitle.length - 1).map((pt, i) => (
74
+ // eslint-disable-next-line react/no-array-index-key
75
+ <Text key={i} muted size={1}>
76
+ {pt.trim()}
77
+ </Text>
78
+ ))}
79
+
80
+ <Text size={1} weight="medium">
81
+ {splitTitle[splitTitle.length - 1]}
82
+ </Text>
83
+ </Breadcrumbs>
84
+ </Box>
85
+ </Flex>
86
+ </Card>
87
+ )
88
+ }, [])
89
+
90
+ const renderValue = useCallback((value: string, option?: {value: string; field: FieldRef}) => {
91
+ return option?.field.title ?? value
92
+ }, [])
93
+
94
+ const filterOption = useCallback((query: string, option: {value: string; field: FieldRef}) => {
95
+ const lQuery = query.toLowerCase()
96
+ return (
97
+ option?.value?.toLowerCase().includes(lQuery) ||
98
+ option?.field?.title?.toLowerCase().includes(lQuery)
99
+ )
100
+ }, [])
101
+
102
+ // const id = useId()
103
+ return (
104
+ <Autocomplete
105
+ fontSize={1}
106
+ icon={currentField ? currentField.icon : SearchIcon}
107
+ onChange={onSelect}
108
+ openButton
109
+ id={id}
110
+ options={autocompleteOptions}
111
+ placeholder="Search for a field"
112
+ radius={2}
113
+ renderOption={renderOption}
114
+ renderValue={renderValue}
115
+ value={currentField?.key}
116
+ filterOption={filterOption}
117
+ />
118
+ )
119
+ }