@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.
- package/LICENSE +1 -1
- package/README.md +551 -30
- package/dist/index.cjs.mjs +1 -0
- package/dist/index.d.ts +333 -9
- package/dist/index.esm.js +2463 -390
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +2457 -383
- package/dist/index.js.map +1 -1
- package/package.json +12 -11
- package/src/_lib/form/DocumentForm.tsx +2 -1
- package/src/_lib/form/constants.ts +1 -0
- package/src/assistDocument/AssistDocumentInput.tsx +24 -4
- package/src/assistDocument/RequestRunInstructionProvider.tsx +37 -21
- package/src/assistDocument/components/AssistDocumentForm.tsx +65 -21
- package/src/assistDocument/components/instruction/InstructionInput.tsx +5 -4
- package/src/assistDocument/components/instruction/InstructionOutputField.tsx +45 -0
- package/src/assistDocument/components/instruction/InstructionOutputInput.tsx +205 -0
- package/src/assistDocument/hooks/useStudioAssistDocument.ts +5 -32
- package/src/assistFormComponents/AssistField.tsx +11 -5
- package/src/assistFormComponents/AssistFormBlock.tsx +2 -3
- package/src/assistFormComponents/validation/listItem.tsx +2 -2
- package/src/assistInspector/AssistInspector.tsx +6 -0
- package/src/assistInspector/FieldAutocomplete.tsx +1 -0
- package/src/assistInspector/helpers.ts +9 -11
- package/src/assistLayout/AssistLayout.tsx +9 -9
- package/src/components/ImageContext.tsx +30 -13
- package/src/components/SafeValueInput.tsx +4 -1
- package/src/fieldActions/assistFieldActions.tsx +42 -13
- package/src/fieldActions/generateCaptionActions.tsx +17 -6
- package/src/fieldActions/generateImageActions.tsx +57 -0
- package/src/helpers/assistSupported.ts +10 -16
- package/src/helpers/conditionalMembers.test.ts +200 -0
- package/src/helpers/conditionalMembers.ts +127 -0
- package/src/helpers/misc.ts +8 -4
- package/src/helpers/typeUtils.ts +19 -5
- package/src/index.ts +3 -0
- package/src/plugin.tsx +18 -4
- package/src/schemas/assistDocumentSchema.tsx +40 -1
- package/src/schemas/serialize/serializeSchema.test.ts +239 -8
- package/src/schemas/serialize/serializeSchema.ts +77 -10
- package/src/schemas/typeDefExtensions.ts +89 -5
- package/src/translate/FieldTranslationProvider.tsx +360 -0
- package/src/translate/getLanguageParams.ts +26 -0
- package/src/translate/languageStore.ts +18 -0
- package/src/translate/paths.test.ts +133 -0
- package/src/translate/paths.ts +175 -0
- package/src/translate/translateActions.tsx +188 -0
- package/src/translate/types.ts +160 -0
- package/src/types.ts +67 -15
- package/src/useApiClient.ts +134 -2
- package/src/assistLayout/AlphaMigration.tsx +0 -310
- package/src/legacy-types.ts +0 -72
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/assist",
|
|
3
|
-
"version": "
|
|
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.
|
|
57
|
+
"@sanity/icons": "^2.8.0",
|
|
58
58
|
"@sanity/incompatible-plugin": "^1.0.4",
|
|
59
|
-
"@sanity/ui": "^2.0.
|
|
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.
|
|
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": "^
|
|
76
|
-
"@typescript-eslint/parser": "^
|
|
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.
|
|
89
|
-
"semantic-release": "^
|
|
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": "^
|
|
93
|
+
"vitest": "^1.2.1"
|
|
93
94
|
},
|
|
94
95
|
"peerDependencies": {
|
|
95
96
|
"react": "^18",
|
|
96
|
-
"sanity": "^3.
|
|
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=
|
|
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={
|
|
30
|
-
{props.renderDefault(
|
|
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
|
|
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:
|
|
27
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
200
|
+
fields,
|
|
201
|
+
documentSchema,
|
|
199
202
|
onChange,
|
|
200
203
|
}: {
|
|
201
204
|
pathKey?: string
|
|
202
205
|
activePath?: Path
|
|
203
|
-
|
|
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 ||
|
|
231
|
+
if (initialized.current || !pathKey) {
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
if (existingField && !missingPresetInstructions?.length) {
|
|
210
235
|
return
|
|
211
236
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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,
|
|
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
|
-
<
|
|
11
|
+
<ObjectMember fieldName={'prompt'} {...props} />
|
|
12
|
+
<ObjectMember fieldName={'output'} {...props} />
|
|
12
13
|
</Stack>
|
|
13
14
|
)
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
function
|
|
17
|
-
const
|
|
18
|
-
return
|
|
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
|
+
}
|