@sanity/assist 1.2.16 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +551 -30
  3. package/dist/index.cjs.mjs +1 -0
  4. package/dist/index.d.ts +333 -9
  5. package/dist/index.esm.js +2463 -390
  6. package/dist/index.esm.js.map +1 -1
  7. package/dist/index.js +2457 -383
  8. package/dist/index.js.map +1 -1
  9. package/package.json +12 -11
  10. package/src/_lib/form/DocumentForm.tsx +2 -1
  11. package/src/_lib/form/constants.ts +1 -0
  12. package/src/assistDocument/AssistDocumentInput.tsx +24 -4
  13. package/src/assistDocument/RequestRunInstructionProvider.tsx +37 -21
  14. package/src/assistDocument/components/AssistDocumentForm.tsx +65 -21
  15. package/src/assistDocument/components/instruction/InstructionInput.tsx +5 -4
  16. package/src/assistDocument/components/instruction/InstructionOutputField.tsx +45 -0
  17. package/src/assistDocument/components/instruction/InstructionOutputInput.tsx +205 -0
  18. package/src/assistDocument/hooks/useStudioAssistDocument.ts +5 -32
  19. package/src/assistFormComponents/AssistField.tsx +11 -5
  20. package/src/assistFormComponents/AssistFormBlock.tsx +2 -3
  21. package/src/assistFormComponents/validation/listItem.tsx +2 -2
  22. package/src/assistInspector/AssistInspector.tsx +6 -0
  23. package/src/assistInspector/FieldAutocomplete.tsx +1 -0
  24. package/src/assistInspector/helpers.ts +9 -11
  25. package/src/assistLayout/AssistLayout.tsx +9 -9
  26. package/src/components/ImageContext.tsx +30 -13
  27. package/src/components/SafeValueInput.tsx +4 -1
  28. package/src/fieldActions/assistFieldActions.tsx +42 -13
  29. package/src/fieldActions/generateCaptionActions.tsx +17 -6
  30. package/src/fieldActions/generateImageActions.tsx +57 -0
  31. package/src/helpers/assistSupported.ts +10 -16
  32. package/src/helpers/conditionalMembers.test.ts +200 -0
  33. package/src/helpers/conditionalMembers.ts +127 -0
  34. package/src/helpers/misc.ts +8 -4
  35. package/src/helpers/typeUtils.ts +19 -5
  36. package/src/index.ts +3 -0
  37. package/src/plugin.tsx +18 -4
  38. package/src/schemas/assistDocumentSchema.tsx +40 -1
  39. package/src/schemas/serialize/serializeSchema.test.ts +239 -8
  40. package/src/schemas/serialize/serializeSchema.ts +77 -10
  41. package/src/schemas/typeDefExtensions.ts +89 -5
  42. package/src/translate/FieldTranslationProvider.tsx +360 -0
  43. package/src/translate/getLanguageParams.ts +26 -0
  44. package/src/translate/languageStore.ts +18 -0
  45. package/src/translate/paths.test.ts +133 -0
  46. package/src/translate/paths.ts +175 -0
  47. package/src/translate/translateActions.tsx +188 -0
  48. package/src/translate/types.ts +160 -0
  49. package/src/types.ts +67 -15
  50. package/src/useApiClient.ts +134 -2
  51. package/src/assistLayout/AlphaMigration.tsx +0 -310
  52. package/src/legacy-types.ts +0 -72
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sanity/assist",
3
- "version": "1.2.16",
4
- "description": "",
3
+ "version": "2.0.1",
4
+ "description": "You create the instructions; Sanity AI Assist does the rest.",
5
5
  "keywords": [
6
6
  "sanity",
7
7
  "sanity-plugin",
@@ -54,10 +54,11 @@
54
54
  "release": "semantic-release"
55
55
  },
56
56
  "dependencies": {
57
- "@sanity/icons": "^2.4.0",
57
+ "@sanity/icons": "^2.8.0",
58
58
  "@sanity/incompatible-plugin": "^1.0.4",
59
- "@sanity/ui": "^2.0.0-beta.13",
59
+ "@sanity/ui": "^2.0.2",
60
60
  "date-fns": "^2.30.0",
61
+ "lodash.get": "^4.4.2",
61
62
  "react-fast-compare": "^3.2.1",
62
63
  "react-is": "^18.2.0",
63
64
  "rxjs": "^7.8.0",
@@ -69,11 +70,11 @@
69
70
  "@rollup/plugin-image": "^3.0.3",
70
71
  "@sanity/pkg-utils": "^2.4.10",
71
72
  "@sanity/plugin-kit": "^3.1.10",
72
- "@sanity/semantic-release-preset": "^4.1.6",
73
+ "@sanity/semantic-release-preset": "^4.1.7",
73
74
  "@types/react": "^18.2.37",
74
75
  "@types/styled-components": "^5.1.30",
75
- "@typescript-eslint/eslint-plugin": "^5.62.0",
76
- "@typescript-eslint/parser": "^5.62.0",
76
+ "@typescript-eslint/eslint-plugin": "^7.0.1",
77
+ "@typescript-eslint/parser": "^7.0.1",
77
78
  "date-fns": "^2.30.0",
78
79
  "eslint": "^8.56.0",
79
80
  "eslint-config-prettier": "^8.10.0",
@@ -85,15 +86,15 @@
85
86
  "react": "^18.2.0",
86
87
  "react-dom": "^18.2.0",
87
88
  "rimraf": "^5.0.5",
88
- "sanity": "^3.23.1",
89
- "semantic-release": "^21.1.2",
89
+ "sanity": "^3.28.0",
90
+ "semantic-release": "^23.0.2",
90
91
  "styled-components": "^6.1.1",
91
92
  "typescript": "^5.3.3",
92
- "vitest": "^0.34.6"
93
+ "vitest": "^1.2.1"
93
94
  },
94
95
  "peerDependencies": {
95
96
  "react": "^18",
96
- "sanity": "^3.16",
97
+ "sanity": "^3.26",
97
98
  "styled-components": "^5.2 || ^6.0.0"
98
99
  },
99
100
  "engines": {
@@ -12,6 +12,7 @@ import {
12
12
  useDocumentStore,
13
13
  } from 'sanity'
14
14
  import {useDocumentPane} from 'sanity/desk'
15
+ import {assistFormId} from './constants'
15
16
 
16
17
  const preventDefault = (ev: React.FormEvent) => ev.preventDefault()
17
18
 
@@ -135,7 +136,7 @@ export function DocumentForm(
135
136
  changed={formState.changed}
136
137
  focused={formState.focused}
137
138
  groups={formState.groups}
138
- id="root"
139
+ id={assistFormId}
139
140
  members={formState.members}
140
141
  onChange={onChange}
141
142
  onFieldGroupSelect={onSetActiveFieldGroup}
@@ -0,0 +1 @@
1
+ export const assistFormId = 'assist'
@@ -1,4 +1,4 @@
1
- import {InputProps, ObjectInputProps} from 'sanity'
1
+ import {InputProps, ObjectInputProps, ObjectSchemaType} from 'sanity'
2
2
  import {AssistDocumentContextProvider} from './AssistDocumentContextProvider'
3
3
  import {FirstAssistedPathProvider} from '../onboarding/FirstAssistedPathProvider'
4
4
  import {useInstructionToaster} from './hooks/useInstructionToaster'
@@ -7,9 +7,12 @@ import {useLayer} from '@sanity/ui'
7
7
  import {useDocumentPane} from 'sanity/desk'
8
8
  import {usePathKey} from '../helpers/misc'
9
9
  import {ConnectFromRegion} from '../_lib/connector'
10
+ import {assistDocumentTypeName} from '../types'
11
+ import {useMemo} from 'react'
12
+ import {assistFormId} from '../_lib/form/constants'
10
13
 
11
14
  export function AssistDocumentInputWrapper(props: InputProps) {
12
- if (!isType(props.schemaType, 'document') && props.id !== 'root') {
15
+ if (!isType(props.schemaType, 'document') && props.id !== 'root' && props.id !== assistFormId) {
13
16
  return <AssistInput {...props} />
14
17
  }
15
18
 
@@ -24,10 +27,27 @@ export function AssistDocumentInputWrapper(props: InputProps) {
24
27
  function AssistDocumentInput({documentId, ...props}: ObjectInputProps & {documentId: string}) {
25
28
  useInstructionToaster(documentId, props.schemaType)
26
29
 
30
+ const schemaType = useMemo(() => {
31
+ if (props.schemaType.name !== assistDocumentTypeName) {
32
+ return props.schemaType
33
+ }
34
+ return {
35
+ ...props.schemaType,
36
+ type: {
37
+ ...props.schemaType.type,
38
+ // compatability with i18nArrays plugin that requires this to be document
39
+ name: 'document',
40
+ },
41
+ } as ObjectSchemaType
42
+ }, [props.schemaType])
43
+
27
44
  return (
28
45
  <FirstAssistedPathProvider members={props.members}>
29
- <AssistDocumentContextProvider schemaType={props.schemaType} documentId={documentId}>
30
- {props.renderDefault(props)}
46
+ <AssistDocumentContextProvider schemaType={schemaType} documentId={documentId}>
47
+ {props.renderDefault({
48
+ ...props,
49
+ schemaType,
50
+ })}
31
51
  </AssistDocumentContextProvider>
32
52
  </FirstAssistedPathProvider>
33
53
  )
@@ -1,13 +1,13 @@
1
1
  import {useRunInstruction} from '../assistLayout/RunInstructionProvider'
2
2
  import {useCallback, useEffect, useState} from 'react'
3
3
  import {ObjectSchemaType, PatchEvent, SanityDocument, unset} from 'sanity'
4
- import {RunInstructionArgs} from '../assistLayout/AssistLayout'
5
4
  import {publicId} from '../helpers/ids'
6
5
 
7
- export interface DocumentArgs {
6
+ export interface DraftDelayedTaskArgs<T> {
8
7
  documentOnChange: (event: PatchEvent) => void
9
8
  // indicates if the document is a draft or liveEditable currently
10
9
  isDocAssistable: boolean
10
+ task: (args: T) => void
11
11
  }
12
12
 
13
13
  export function isDocAssistable(
@@ -23,28 +23,44 @@ export function getAssistableDocId(documentSchemaType: ObjectSchemaType, documen
23
23
  return documentSchemaType.liveEdit ? baseId : `drafts.${baseId}`
24
24
  }
25
25
 
26
- export function useRequestRunInstruction(args: DocumentArgs) {
27
- const {documentOnChange, isDocAssistable} = args
28
-
26
+ export function useRequestRunInstruction(args: {
27
+ documentOnChange: (event: PatchEvent) => void
28
+ // indicates if the document is a draft or liveEditable currently
29
+ isDocAssistable: boolean
30
+ }) {
29
31
  const {runInstruction, instructionLoading} = useRunInstruction()
30
- const [queuedTask, setQueuedTask] = useState<RunInstructionArgs | undefined>(undefined)
31
-
32
- useEffect(() => {
33
- if (queuedTask && isDocAssistable) {
34
- runInstruction(queuedTask)
35
- setQueuedTask(undefined)
36
- }
37
- }, [queuedTask, isDocAssistable, runInstruction])
32
+ const requestRunInstruction = useDraftDelayedTask({
33
+ ...args,
34
+ task: runInstruction,
35
+ })
38
36
 
39
37
  return {
40
38
  instructionLoading,
41
- requestRunInstruction: useCallback(
42
- (task: RunInstructionArgs) => {
43
- // make a dummy edit: this will trigger the document/draft to be created
44
- documentOnChange(PatchEvent.from([unset(['_force_document_creation'])]))
45
- setQueuedTask(task)
46
- },
47
- [setQueuedTask, documentOnChange]
48
- ),
39
+ requestRunInstruction,
49
40
  }
50
41
  }
42
+
43
+ /**
44
+ * Ensures that the current document is a draft before running task
45
+ */
46
+ export function useDraftDelayedTask<T>(args: DraftDelayedTaskArgs<T>) {
47
+ const {documentOnChange, isDocAssistable, task} = args
48
+
49
+ const [queuedArgs, setQueuedArgs] = useState<T | undefined>(undefined)
50
+
51
+ useEffect(() => {
52
+ if (queuedArgs && isDocAssistable) {
53
+ task(queuedArgs)
54
+ setQueuedArgs(undefined)
55
+ }
56
+ }, [queuedArgs, isDocAssistable, task])
57
+
58
+ return useCallback(
59
+ (taskArgs: T) => {
60
+ // make a dummy edit: this will trigger the document/draft to be created
61
+ documentOnChange(PatchEvent.from([unset(['_force_document_creation'])]))
62
+ setQueuedArgs(taskArgs)
63
+ },
64
+ [setQueuedArgs, documentOnChange]
65
+ )
66
+ }
@@ -6,6 +6,7 @@ import {
6
6
  documentRootKey,
7
7
  fieldPathParam,
8
8
  instructionParam,
9
+ StudioInstruction,
9
10
  } from '../../types'
10
11
  import {createContext, useContext, useEffect, useMemo, useRef} from 'react'
11
12
  import {
@@ -16,6 +17,7 @@ import {
16
17
  KeyedSegment,
17
18
  ObjectInputProps,
18
19
  ObjectSchemaType,
20
+ PatchEvent,
19
21
  Path,
20
22
  SchemaType,
21
23
  set,
@@ -30,6 +32,7 @@ import {useAiPaneRouter} from '../../assistInspector/helpers'
30
32
  import {SelectedFieldContextProvider, SelectedFieldContextValue} from './SelectedFieldContext'
31
33
  import {Card, Stack, Text} from '@sanity/ui'
32
34
  import {documentTypeFromAiDocumentId} from '../../helpers/ids'
35
+ import {useAiAssistanceConfig} from '../../assistLayout/AiAssistanceConfigContext'
33
36
 
34
37
  const EMPTY_FIELDS: AssistField[] = []
35
38
 
@@ -104,8 +107,6 @@ function AssistDocumentFormEditable(props: ObjectInputProps) {
104
107
  }
105
108
  }, [title, documentSchema, onChange, id])
106
109
 
107
- const fieldExists = !!fields?.some((f) => f._key === typePath)
108
-
109
110
  const {onPathOpen, ...formCallbacks} = useFormCallbacks()
110
111
 
111
112
  const newCallbacks: FormCallbacksValue = useMemo(
@@ -141,7 +142,8 @@ function AssistDocumentFormEditable(props: ObjectInputProps) {
141
142
  key={typePath}
142
143
  pathKey={typePath}
143
144
  activePath={activePath}
144
- fieldExists={fieldExists}
145
+ fields={fields}
146
+ documentSchema={documentSchema}
145
147
  onChange={onChange}
146
148
  />
147
149
  {instruction && <BackToInstructionListLink />}
@@ -195,36 +197,78 @@ function useSelectedSchema(
195
197
  function FieldsInitializer({
196
198
  pathKey,
197
199
  activePath,
198
- fieldExists,
200
+ fields,
201
+ documentSchema,
199
202
  onChange,
200
203
  }: {
201
204
  pathKey?: string
202
205
  activePath?: Path
203
- fieldExists: boolean
206
+ fields: AssistField[] | undefined
207
+ documentSchema: ObjectSchemaType | undefined
204
208
  onChange: ObjectInputProps['onChange']
205
209
  }) {
210
+ const {
211
+ config: {__presets: presets},
212
+ } = useAiAssistanceConfig()
213
+
214
+ const existingField = fields?.find((f) => f._key === pathKey)
215
+ const documentPresets = !!documentSchema?.name && presets?.[documentSchema?.name]
216
+
217
+ const missingPresetInstructions = useMemo(() => {
218
+ if (!documentPresets || !pathKey) {
219
+ return undefined
220
+ }
221
+ const existingInstructions = existingField?.instructions
222
+ const presetField = documentPresets.fields?.find((f) => f.path === pathKey)
223
+ return presetField?.instructions?.filter(
224
+ (i) => !existingInstructions?.some((ei) => ei._key === i._key)
225
+ )
226
+ }, [documentPresets, pathKey, existingField])
227
+
206
228
  // need this to not fire onChange twice in React strict mode
207
229
  const initialized = useRef(false)
208
230
  useEffect(() => {
209
- if (initialized.current || fieldExists || activePath || !pathKey) {
231
+ if (initialized.current || !pathKey) {
232
+ return
233
+ }
234
+ if (existingField && !missingPresetInstructions?.length) {
210
235
  return
211
236
  }
212
- onChange([
213
- setIfMissing([], ['fields']),
214
- insert(
215
- [
216
- typed<AssistField>({
217
- _key: pathKey,
218
- _type: assistFieldTypeName,
219
- path: pathKey,
220
- }),
221
- ],
222
- 'after',
223
- ['fields', -1]
224
- ),
225
- ])
237
+
238
+ let event = PatchEvent.from([setIfMissing([], ['fields'])])
239
+ if (!existingField) {
240
+ event = event.append(
241
+ insert(
242
+ [
243
+ typed<AssistField>({
244
+ _key: pathKey,
245
+ _type: assistFieldTypeName,
246
+ path: pathKey,
247
+ }),
248
+ ],
249
+ 'after',
250
+ ['fields', -1]
251
+ )
252
+ )
253
+ }
254
+ if (missingPresetInstructions?.length) {
255
+ event = event.append(
256
+ insert(
257
+ missingPresetInstructions.map(
258
+ (preset): StudioInstruction => ({
259
+ ...preset,
260
+ _type: 'sanity.assist.instruction',
261
+ prompt: preset.prompt?.map((p) => ({markDefs: [], ...p})),
262
+ })
263
+ ),
264
+ 'after',
265
+ ['fields', {_key: pathKey}, 'instructions', -1]
266
+ )
267
+ )
268
+ }
269
+ onChange(event)
226
270
  initialized.current = true
227
- }, [activePath, onChange, pathKey, fieldExists])
271
+ }, [activePath, onChange, pathKey, existingField, missingPresetInstructions])
228
272
 
229
273
  return null
230
274
  }
@@ -8,14 +8,15 @@ export function InstructionInput(props: ObjectInputProps) {
8
8
  <Stack space={[4, 4, 4, 5]}>
9
9
  <NameField {...props} />
10
10
  <ShareField {...props} />
11
- <PromptField {...props} />
11
+ <ObjectMember fieldName={'prompt'} {...props} />
12
+ <ObjectMember fieldName={'output'} {...props} />
12
13
  </Stack>
13
14
  )
14
15
  }
15
16
 
16
- function PromptField(props: ObjectInputProps) {
17
- const promptMember = findFieldMember(props.members, 'prompt')
18
- return promptMember ? <ObjectInputMember {...props} member={promptMember} /> : null
17
+ function ObjectMember({fieldName, ...props}: ObjectInputProps & {fieldName: string}) {
18
+ const member = findFieldMember(props.members, fieldName)
19
+ return member ? <ObjectInputMember {...props} member={member} /> : null
19
20
  }
20
21
 
21
22
  const NONE: (FieldMember | FieldError)[] = []
@@ -0,0 +1,45 @@
1
+ import {
2
+ ArrayFieldProps,
3
+ ArraySchemaType,
4
+ isArrayOfObjectsSchemaType,
5
+ isObjectSchemaType,
6
+ ObjectSchemaType,
7
+ } from 'sanity'
8
+ import {useCallback, useContext, useState} from 'react'
9
+ import {SelectedFieldContext} from '../SelectedFieldContext'
10
+
11
+ export function InstructionOutputField(props: ArrayFieldProps) {
12
+ const {fieldSchema} = useContext(SelectedFieldContext) ?? {}
13
+
14
+ if (
15
+ !fieldSchema ||
16
+ !(isObjectSchemaType(fieldSchema) || isArrayOfObjectsSchemaType(fieldSchema))
17
+ ) {
18
+ return null
19
+ }
20
+
21
+ return (
22
+ <EnabledOutputField {...props} fieldSchema={fieldSchema}>
23
+ {props.children}
24
+ </EnabledOutputField>
25
+ )
26
+ }
27
+
28
+ function EnabledOutputField({
29
+ fieldSchema,
30
+ ...props
31
+ }: ArrayFieldProps & {fieldSchema: ObjectSchemaType | ArraySchemaType<ObjectSchemaType>}) {
32
+ const [open, setOpen] = useState(!!props.value?.length)
33
+ const onExpand = useCallback(() => setOpen(true), [])
34
+ const onCollapse = useCallback(() => setOpen(false), [])
35
+
36
+ return props.renderDefault({
37
+ ...props,
38
+ collapsible: true,
39
+ onExpand,
40
+ onCollapse,
41
+ collapsed: !open,
42
+ level: 1,
43
+ title: isObjectSchemaType(fieldSchema) ? 'Allowed fields' : 'Allowed types',
44
+ })
45
+ }
@@ -0,0 +1,205 @@
1
+ import {
2
+ ArrayOfObjectsInputProps,
3
+ ArraySchemaType,
4
+ FormPatch,
5
+ insert,
6
+ isArrayOfObjectsSchemaType,
7
+ isObjectSchemaType,
8
+ ObjectSchemaType,
9
+ PatchEvent,
10
+ setIfMissing,
11
+ typed,
12
+ unset,
13
+ } from 'sanity'
14
+ import {useCallback, useContext, useEffect, useMemo} from 'react'
15
+ import {SelectedFieldContext} from '../SelectedFieldContext'
16
+ import {Card, Checkbox, Flex, Stack, Text} from '@sanity/ui'
17
+ import {isType} from '../../../helpers/typeUtils'
18
+ import {isAssistSupported} from '../../../helpers/assistSupported'
19
+ import {OutputFieldItem, outputFieldTypeName, OutputTypeItem} from '../../../types'
20
+
21
+ export function InstructionOutputInput(props: ArrayOfObjectsInputProps) {
22
+ const {fieldSchema} = useContext(SelectedFieldContext) ?? {}
23
+
24
+ if (!fieldSchema) {
25
+ return null
26
+ }
27
+
28
+ if (isObjectSchemaType(fieldSchema)) {
29
+ return <ObjectOutputInput {...props} fieldSchema={fieldSchema} />
30
+ }
31
+
32
+ if (isArrayOfObjectsSchemaType(fieldSchema)) {
33
+ return <ArrayOutputInput {...props} fieldSchema={fieldSchema} />
34
+ }
35
+ return null
36
+ }
37
+
38
+ function useEmptySelectAllValue(
39
+ value: (OutputTypeItem | OutputFieldItem)[],
40
+ allowedValues: {name: string}[],
41
+ onChange: (patch: FormPatch | FormPatch[] | PatchEvent) => void
42
+ ) {
43
+ useEffect(() => {
44
+ const validValues = value?.filter((v) =>
45
+ allowedValues.find(
46
+ (f) => f.name === (v._type === outputFieldTypeName ? v.relativePath : v.type)
47
+ )
48
+ )
49
+ const valueLength = value?.length ?? 0
50
+ const validLength = validValues?.length ?? 0
51
+ if ((!validLength && valueLength) || validLength >= allowedValues.length) {
52
+ // if we end up here, we consider this a "no selected fields/types" selections. This should render and behave as all values selected.
53
+ // we need this behaviour to accommodate new fields/types being added to the model, so they get visited by instructions without having to update the filter
54
+ // when things have been explicitly selected, we let the selection remain as is
55
+ onChange(PatchEvent.from([unset()]))
56
+ }
57
+ }, [allowedValues, value, onChange])
58
+ }
59
+
60
+ function ObjectOutputInput({
61
+ fieldSchema,
62
+ ...props
63
+ }: ArrayOfObjectsInputProps & {fieldSchema: ObjectSchemaType}) {
64
+ const {value, onChange} = props
65
+
66
+ const fields = useMemo(
67
+ () => fieldSchema.fields.filter((field) => isAssistSupported(field.type)),
68
+ [fieldSchema.fields]
69
+ )
70
+
71
+ useEmptySelectAllValue(value as OutputTypeItem[], fields, onChange)
72
+
73
+ const onSelectChange = useCallback(
74
+ (checked: boolean, selectedValue: string) => {
75
+ if (checked) {
76
+ if (value?.length) {
77
+ onChange(PatchEvent.from(unset([{_key: selectedValue}])))
78
+ } else {
79
+ // we went from empty array to everything selected but one
80
+ const items = fields
81
+ .filter((f) => f.name !== selectedValue)
82
+ .map((field) =>
83
+ typed<OutputFieldItem>({
84
+ _key: field.name,
85
+ _type: 'sanity.assist.output.field',
86
+ relativePath: field.name,
87
+ })
88
+ )
89
+ onChange(PatchEvent.from([setIfMissing([]), insert(items, 'after', [-1])]))
90
+ }
91
+ } else {
92
+ const patchValue: OutputFieldItem = {
93
+ _key: selectedValue,
94
+ _type: 'sanity.assist.output.field',
95
+ relativePath: selectedValue,
96
+ }
97
+ onChange(PatchEvent.from([setIfMissing([]), insert([patchValue], 'after', [-1])]))
98
+ }
99
+ },
100
+ [onChange, value, fields]
101
+ )
102
+
103
+ return (
104
+ <Stack space={2}>
105
+ {fields.map((field) => {
106
+ return (
107
+ <Flex key={field.name} align="center" gap={2}>
108
+ <Selectable
109
+ value={field.name}
110
+ title={field.type.title ?? field.name}
111
+ arrayValue={value as OutputFieldItem[]}
112
+ onChange={onSelectChange}
113
+ />
114
+ </Flex>
115
+ )
116
+ })}
117
+ </Stack>
118
+ )
119
+ }
120
+
121
+ function ArrayOutputInput({
122
+ fieldSchema,
123
+ ...props
124
+ }: ArrayOfObjectsInputProps & {fieldSchema: ArraySchemaType}) {
125
+ const {value, onChange} = props
126
+
127
+ const ofItems = useMemo(
128
+ () => fieldSchema.of.filter((itemType) => isAssistSupported(itemType)),
129
+ [fieldSchema.of]
130
+ )
131
+
132
+ useEmptySelectAllValue(value as OutputTypeItem[], ofItems, onChange)
133
+
134
+ const onSelectChange = useCallback(
135
+ (checked: boolean, selectedValue: string) => {
136
+ if (checked) {
137
+ if (value?.length) {
138
+ onChange(PatchEvent.from(unset([{_key: selectedValue}])))
139
+ } else {
140
+ // we went from empty array to everything selected but one
141
+ const items = ofItems
142
+ .filter((f) => f.name !== selectedValue)
143
+ .map((field) =>
144
+ typed<OutputTypeItem>({
145
+ _key: field.name,
146
+ _type: 'sanity.assist.output.type',
147
+ type: field.name,
148
+ })
149
+ )
150
+ onChange(PatchEvent.from([setIfMissing([]), insert(items, 'after', [-1])]))
151
+ }
152
+ } else {
153
+ const patchValue: OutputTypeItem = {
154
+ _key: selectedValue,
155
+ _type: 'sanity.assist.output.type',
156
+ type: selectedValue,
157
+ }
158
+ onChange(PatchEvent.from([setIfMissing([]), insert([patchValue], 'after', [-1])]))
159
+ }
160
+ },
161
+ [onChange, value, ofItems]
162
+ )
163
+ return (
164
+ <Stack space={2}>
165
+ {ofItems.map((itemType) => {
166
+ return (
167
+ <Flex key={itemType.name}>
168
+ <Selectable
169
+ value={itemType.name}
170
+ title={isType(itemType, 'block') ? 'Text' : itemType.title ?? itemType.name}
171
+ arrayValue={value as OutputTypeItem[] | undefined}
172
+ onChange={onSelectChange}
173
+ />
174
+ </Flex>
175
+ )
176
+ })}
177
+ </Stack>
178
+ )
179
+ }
180
+
181
+ function Selectable({
182
+ title,
183
+ arrayValue,
184
+ value,
185
+ onChange,
186
+ }: {
187
+ title: string
188
+ value: string
189
+ arrayValue?: {_key: string}[]
190
+ onChange: (checked: boolean, value: string) => void
191
+ }) {
192
+ const checked = !arrayValue?.length || !!arrayValue?.find((v) => v._key === value)
193
+ const handleChange = useCallback(() => onChange(checked, value), [onChange, checked, value])
194
+
195
+ return (
196
+ <Flex gap={2} align="flex-start">
197
+ <Checkbox checked={checked} onChange={handleChange} />
198
+ <Card marginTop={1} onClick={handleChange}>
199
+ <Text style={{cursor: 'default'}} size={1}>
200
+ {title}
201
+ </Text>
202
+ </Card>
203
+ </Flex>
204
+ )
205
+ }