@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.
- package/LICENSE +21 -0
- package/README.md +205 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.esm.js +2341 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +2341 -0
- package/dist/index.js.map +1 -0
- package/package.json +98 -0
- package/sanity.json +8 -0
- package/src/_lib/connector/ConnectFromRegion.tsx +24 -0
- package/src/_lib/connector/ConnectToRegion.tsx +22 -0
- package/src/_lib/connector/ConnectorRegion.tsx +23 -0
- package/src/_lib/connector/ConnectorsProvider.tsx +19 -0
- package/src/_lib/connector/ConnectorsStore.ts +122 -0
- package/src/_lib/connector/ConnectorsStoreContext.ts +4 -0
- package/src/_lib/connector/helpers.ts +5 -0
- package/src/_lib/connector/index.ts +9 -0
- package/src/_lib/connector/mapConnectorToLine.ts +83 -0
- package/src/_lib/connector/types.ts +56 -0
- package/src/_lib/connector/useConnectorsStore.ts +13 -0
- package/src/_lib/connector/useRegionRects.ts +141 -0
- package/src/_lib/fixedListenQuery.ts +101 -0
- package/src/_lib/form/DocumentForm.tsx +197 -0
- package/src/_lib/form/helpers.ts +31 -0
- package/src/_lib/form/index.ts +1 -0
- package/src/_lib/randomKey.ts +29 -0
- package/src/_lib/useListeningQuery.ts +61 -0
- package/src/_lib/usePrevious.ts +9 -0
- package/src/assistConnectors/AssistConnectorsOverlay.tsx +132 -0
- package/src/assistConnectors/ConnectorPath.tsx +62 -0
- package/src/assistConnectors/draw/arrowPath.ts +9 -0
- package/src/assistConnectors/draw/connectorPath.ts +142 -0
- package/src/assistConnectors/index.ts +1 -0
- package/src/assistDocument/AssistDocumentContext.tsx +31 -0
- package/src/assistDocument/AssistDocumentContextProvider.tsx +17 -0
- package/src/assistDocument/AssistDocumentInput.tsx +46 -0
- package/src/assistDocument/RequestRunInstructionProvider.tsx +50 -0
- package/src/assistDocument/components/AssistDocumentForm.tsx +188 -0
- package/src/assistDocument/components/FieldRefPreview.tsx +27 -0
- package/src/assistDocument/components/InstructionsArrayField.tsx +8 -0
- package/src/assistDocument/components/InstructionsArrayInput.tsx +26 -0
- package/src/assistDocument/components/SelectedFieldContext.tsx +10 -0
- package/src/assistDocument/components/generic/HiddenFieldTitle.tsx +5 -0
- package/src/assistDocument/components/helpers.ts +21 -0
- package/src/assistDocument/components/instruction/BackToInstructionsLink.tsx +31 -0
- package/src/assistDocument/components/instruction/FieldRefInput.tsx +33 -0
- package/src/assistDocument/components/instruction/InstructionInput.tsx +87 -0
- package/src/assistDocument/components/instruction/PromptInput.tsx +52 -0
- package/src/assistDocument/components/instruction/appearance/IconInput.tsx +46 -0
- package/src/assistDocument/components/instruction/appearance/InstructionVisibility.tsx +37 -0
- package/src/assistDocument/hooks/useAssistDocumentContextValue.tsx +68 -0
- package/src/assistDocument/hooks/useDocumentState.ts +6 -0
- package/src/assistDocument/hooks/useInstructionToaster.tsx +74 -0
- package/src/assistDocument/hooks/useStudioAssistDocument.ts +119 -0
- package/src/assistDocument/index.ts +1 -0
- package/src/assistFormComponents/AssistField.tsx +51 -0
- package/src/assistFormComponents/AssistFormBlock.tsx +31 -0
- package/src/assistFormComponents/AssistInlineFormBlock.tsx +14 -0
- package/src/assistFormComponents/AssistItem.tsx +20 -0
- package/src/assistFormComponents/validation/listItem.tsx +63 -0
- package/src/assistFormComponents/validation/validationList.tsx +89 -0
- package/src/assistInspector/AssistInspector.tsx +379 -0
- package/src/assistInspector/FieldAutocomplete.tsx +119 -0
- package/src/assistInspector/InstructionTaskHistoryButton.tsx +261 -0
- package/src/assistInspector/constants.ts +1 -0
- package/src/assistInspector/helpers.ts +125 -0
- package/src/assistInspector/index.ts +26 -0
- package/src/assistLayout/AiAssistanceConfigContext.tsx +81 -0
- package/src/assistLayout/AlphaMigration.tsx +311 -0
- package/src/assistLayout/AssistLayout.tsx +38 -0
- package/src/assistLayout/RunInstructionProvider.tsx +222 -0
- package/src/components/AssistFeatureBadge.tsx +9 -0
- package/src/components/Delay.tsx +25 -0
- package/src/components/HideReferenceChangedBannerInput.tsx +25 -0
- package/src/components/SafeValueInput.tsx +73 -0
- package/src/components/TimeAgo.tsx +18 -0
- package/src/constants.ts +20 -0
- package/src/fieldActions/PrivateIcon.tsx +20 -0
- package/src/fieldActions/assistFieldActions.tsx +230 -0
- package/src/globals.d.ts +4 -0
- package/src/helpers/assistSupported.ts +44 -0
- package/src/helpers/ids.ts +19 -0
- package/src/helpers/misc.ts +16 -0
- package/src/helpers/typeUtils.ts +15 -0
- package/src/helpers/useAssistSupported.ts +10 -0
- package/src/index.ts +6 -0
- package/src/legacy-types.ts +72 -0
- package/src/onboarding/FieldActionsOnboarding.tsx +90 -0
- package/src/onboarding/FirstAssistedPathProvider.tsx +29 -0
- package/src/onboarding/InspectorOnboarding.tsx +46 -0
- package/src/onboarding/onboardingStore.ts +33 -0
- package/src/plugin.tsx +80 -0
- package/src/presence/AiFieldPresence.tsx +28 -0
- package/src/presence/AssistAvatar.tsx +96 -0
- package/src/presence/AssistDocumentPresence.tsx +58 -0
- package/src/presence/useAssistPresence.ts +61 -0
- package/src/schemas/assistDocumentSchema.tsx +450 -0
- package/src/schemas/contextDocumentSchema.tsx +56 -0
- package/src/schemas/index.ts +25 -0
- package/src/schemas/serialize/SchemTypeTool.tsx +102 -0
- package/src/schemas/serialize/schemaUtils.ts +37 -0
- package/src/schemas/serialize/serializeSchema.test.ts +382 -0
- package/src/schemas/serialize/serializeSchema.ts +162 -0
- package/src/schemas/serializedSchemaTypeSchema.ts +59 -0
- package/src/schemas/typeDefExtensions.ts +30 -0
- package/src/types.ts +167 -0
- package/src/useApiClient.ts +140 -0
- package/src/vite.config.ts +9 -0
- package/v2-incompatible.js +11 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {SanityClient} from '@sanity/client'
|
|
2
|
+
import {defer, delay, merge, Observable, of, partition, switchMap, throwError} from 'rxjs'
|
|
3
|
+
import {filter, mergeMap, share, take} from 'rxjs/operators'
|
|
4
|
+
import {exhaustMapToWithTrailing} from 'rxjs-exhaustmap-with-trailing'
|
|
5
|
+
import {MutationEvent, ReconnectEvent, WelcomeEvent} from 'sanity'
|
|
6
|
+
|
|
7
|
+
/** @internal */
|
|
8
|
+
export type ListenQueryParams = Record<string, string | number | boolean | string[]>
|
|
9
|
+
|
|
10
|
+
/** @beta */
|
|
11
|
+
export interface ListenQueryOptions {
|
|
12
|
+
tag?: string
|
|
13
|
+
apiVersion?: string
|
|
14
|
+
throttleTime?: number
|
|
15
|
+
transitions?: ('update' | 'appear' | 'disappear')[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const fetch = (
|
|
19
|
+
client: SanityClient,
|
|
20
|
+
query: string,
|
|
21
|
+
params: ListenQueryParams,
|
|
22
|
+
options: ListenQueryOptions
|
|
23
|
+
) =>
|
|
24
|
+
defer(() =>
|
|
25
|
+
// getVersionedClient(options.apiVersion)
|
|
26
|
+
client.observable.fetch(query, params, {
|
|
27
|
+
tag: options.tag,
|
|
28
|
+
filterResponse: true,
|
|
29
|
+
})
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const listen = (
|
|
33
|
+
client: SanityClient,
|
|
34
|
+
query: string,
|
|
35
|
+
params: ListenQueryParams,
|
|
36
|
+
options: ListenQueryOptions
|
|
37
|
+
) =>
|
|
38
|
+
defer(() =>
|
|
39
|
+
// getVersionedClient(options.apiVersion)
|
|
40
|
+
client.listen(query, params, {
|
|
41
|
+
events: ['welcome', 'mutation', 'reconnect'],
|
|
42
|
+
includeResult: false,
|
|
43
|
+
visibility: 'query',
|
|
44
|
+
tag: options.tag,
|
|
45
|
+
})
|
|
46
|
+
) as Observable<ReconnectEvent | WelcomeEvent | MutationEvent>
|
|
47
|
+
|
|
48
|
+
function isWelcomeEvent(
|
|
49
|
+
event: MutationEvent | ReconnectEvent | WelcomeEvent
|
|
50
|
+
): event is WelcomeEvent {
|
|
51
|
+
return event.type === 'welcome'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @internal */
|
|
55
|
+
export const listenQuery = (
|
|
56
|
+
client: SanityClient,
|
|
57
|
+
query: string | {fetch: string; listen: string},
|
|
58
|
+
params: ListenQueryParams = {},
|
|
59
|
+
options: ListenQueryOptions = {}
|
|
60
|
+
) => {
|
|
61
|
+
const fetchQuery = typeof query === 'string' ? query : query.fetch
|
|
62
|
+
const listenerQuery = typeof query === 'string' ? query : query.listen
|
|
63
|
+
|
|
64
|
+
const fetchOnce$ = fetch(client, fetchQuery, params, options)
|
|
65
|
+
|
|
66
|
+
const events$ = listen(client, listenerQuery, params, options).pipe(
|
|
67
|
+
mergeMap((ev, i) => {
|
|
68
|
+
const isFirst = i === 0
|
|
69
|
+
if (isFirst && !isWelcomeEvent(ev)) {
|
|
70
|
+
// if the first event is not welcome, it is most likely a reconnect and
|
|
71
|
+
// if it's not a reconnect something is very wrong
|
|
72
|
+
return throwError(
|
|
73
|
+
new Error(
|
|
74
|
+
ev.type === 'reconnect'
|
|
75
|
+
? 'Could not establish EventSource connection'
|
|
76
|
+
: `Received unexpected type of first event "${ev.type}"`
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
return of(ev)
|
|
81
|
+
}),
|
|
82
|
+
share()
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
const [welcome$, mutationAndReconnect$] = partition(events$, isWelcomeEvent)
|
|
86
|
+
const isRelevantEvent = (event: MutationEvent | ReconnectEvent | WelcomeEvent): boolean => {
|
|
87
|
+
if (!options.transitions || event.type !== 'mutation') {
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return options.transitions.includes(event.transition)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return merge(
|
|
95
|
+
welcome$.pipe(take(1)),
|
|
96
|
+
mutationAndReconnect$.pipe(
|
|
97
|
+
filter(isRelevantEvent),
|
|
98
|
+
switchMap((event) => merge(of(event), of(event).pipe(delay(options.throttleTime || 1000))))
|
|
99
|
+
)
|
|
100
|
+
).pipe(exhaustMapToWithTrailing(fetchOnce$))
|
|
101
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import {Box, BoxProps, Flex, focusFirstDescendant, Spinner, Text} from '@sanity/ui'
|
|
2
|
+
import React, {HTMLProps, useEffect, useMemo, useRef} from 'react'
|
|
3
|
+
import {tap} from 'rxjs/operators'
|
|
4
|
+
import {
|
|
5
|
+
createPatchChannel,
|
|
6
|
+
DocumentMutationEvent,
|
|
7
|
+
DocumentRebaseEvent,
|
|
8
|
+
FormBuilder,
|
|
9
|
+
fromMutationPatches,
|
|
10
|
+
PatchMsg,
|
|
11
|
+
useDocumentPresence,
|
|
12
|
+
useDocumentStore,
|
|
13
|
+
} from 'sanity'
|
|
14
|
+
import {useDocumentPane} from 'sanity/desk'
|
|
15
|
+
|
|
16
|
+
const preventDefault = (ev: React.FormEvent) => ev.preventDefault()
|
|
17
|
+
|
|
18
|
+
export function DocumentForm(
|
|
19
|
+
props: Omit<BoxProps, 'as'> & Omit<HTMLProps<HTMLDivElement>, 'as' | 'onSubmit' | 'ref'>
|
|
20
|
+
) {
|
|
21
|
+
const {
|
|
22
|
+
collapsedFieldSets,
|
|
23
|
+
collapsedPaths,
|
|
24
|
+
displayed: value,
|
|
25
|
+
documentId,
|
|
26
|
+
documentType,
|
|
27
|
+
editState,
|
|
28
|
+
formState,
|
|
29
|
+
onBlur,
|
|
30
|
+
onChange,
|
|
31
|
+
onFocus,
|
|
32
|
+
onPathOpen,
|
|
33
|
+
onSetActiveFieldGroup,
|
|
34
|
+
onSetCollapsedFieldSet,
|
|
35
|
+
onSetCollapsedPath,
|
|
36
|
+
ready,
|
|
37
|
+
validation,
|
|
38
|
+
} = useDocumentPane()
|
|
39
|
+
|
|
40
|
+
const documentStore = useDocumentStore()
|
|
41
|
+
const presence = useDocumentPresence(documentId)
|
|
42
|
+
|
|
43
|
+
// The `patchChannel` is an INTERNAL publish/subscribe channel that we use to notify form-builder
|
|
44
|
+
// nodes about both remote and local patches.
|
|
45
|
+
// - Used by the Portable Text input to modify selections.
|
|
46
|
+
// - Used by `withDocument` to reset value.
|
|
47
|
+
const patchChannel = useMemo(() => createPatchChannel(), [])
|
|
48
|
+
|
|
49
|
+
const isLocked = editState?.transactionSyncLock?.enabled
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const sub = documentStore.pair
|
|
53
|
+
.documentEvents(documentId, documentType)
|
|
54
|
+
.pipe(
|
|
55
|
+
tap((event) => {
|
|
56
|
+
if (event.type === 'mutation') {
|
|
57
|
+
patchChannel.publish(prepareMutationEvent(event))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (event.type === 'rebase') {
|
|
61
|
+
patchChannel.publish(prepareRebaseEvent(event))
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
)
|
|
65
|
+
.subscribe()
|
|
66
|
+
|
|
67
|
+
return () => {
|
|
68
|
+
sub.unsubscribe()
|
|
69
|
+
}
|
|
70
|
+
}, [documentId, documentStore, documentType, patchChannel])
|
|
71
|
+
|
|
72
|
+
const hasRev = Boolean(value?._rev)
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (hasRev) {
|
|
75
|
+
// this is a workaround for an issue that caused the document pushed to withDocument to get
|
|
76
|
+
// stuck at the first initial value.
|
|
77
|
+
// This effect is triggered only when the document goes from not having a revision, to getting one
|
|
78
|
+
// so it will kick in as soon as the document is received from the backend
|
|
79
|
+
patchChannel.publish({
|
|
80
|
+
type: 'mutation',
|
|
81
|
+
patches: [],
|
|
82
|
+
snapshot: value,
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
// React to changes in hasRev only
|
|
86
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
87
|
+
}, [hasRev])
|
|
88
|
+
|
|
89
|
+
const formRef = useRef<null | HTMLDivElement>(null)
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
focusFirstDescendant(formRef.current!)
|
|
93
|
+
}, [])
|
|
94
|
+
|
|
95
|
+
if (isLocked) {
|
|
96
|
+
return (
|
|
97
|
+
<Box as="form" {...props} ref={formRef}>
|
|
98
|
+
<Flex
|
|
99
|
+
align="center"
|
|
100
|
+
direction="column"
|
|
101
|
+
height="fill"
|
|
102
|
+
justify="center"
|
|
103
|
+
padding={3}
|
|
104
|
+
sizing="border"
|
|
105
|
+
>
|
|
106
|
+
<Text size={1}>
|
|
107
|
+
Please hold tight while the document is synced. This usually happens right after the
|
|
108
|
+
document has been published, and it shouldn’t take more than a few seconds
|
|
109
|
+
</Text>
|
|
110
|
+
</Flex>
|
|
111
|
+
</Box>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<Box as="form" {...props} onSubmit={preventDefault} ref={formRef}>
|
|
117
|
+
{ready ? (
|
|
118
|
+
formState === null ? (
|
|
119
|
+
<Flex
|
|
120
|
+
align="center"
|
|
121
|
+
direction="column"
|
|
122
|
+
height="fill"
|
|
123
|
+
justify="center"
|
|
124
|
+
padding={3}
|
|
125
|
+
sizing="border"
|
|
126
|
+
>
|
|
127
|
+
<Text size={1}>This form is hidden</Text>
|
|
128
|
+
</Flex>
|
|
129
|
+
) : (
|
|
130
|
+
<FormBuilder
|
|
131
|
+
__internal_patchChannel={patchChannel}
|
|
132
|
+
collapsedFieldSets={collapsedFieldSets}
|
|
133
|
+
collapsedPaths={collapsedPaths}
|
|
134
|
+
focusPath={formState.focusPath}
|
|
135
|
+
changed={formState.changed}
|
|
136
|
+
focused={formState.focused}
|
|
137
|
+
groups={formState.groups}
|
|
138
|
+
id="root"
|
|
139
|
+
members={formState.members}
|
|
140
|
+
onChange={onChange}
|
|
141
|
+
onFieldGroupSelect={onSetActiveFieldGroup}
|
|
142
|
+
onPathBlur={onBlur}
|
|
143
|
+
onPathFocus={onFocus}
|
|
144
|
+
onPathOpen={onPathOpen}
|
|
145
|
+
onSetFieldSetCollapsed={onSetCollapsedFieldSet}
|
|
146
|
+
onSetPathCollapsed={onSetCollapsedPath}
|
|
147
|
+
presence={presence}
|
|
148
|
+
readOnly={formState.readOnly}
|
|
149
|
+
schemaType={formState.schemaType}
|
|
150
|
+
validation={validation}
|
|
151
|
+
value={formState.value}
|
|
152
|
+
/>
|
|
153
|
+
)
|
|
154
|
+
) : (
|
|
155
|
+
<Flex
|
|
156
|
+
align="center"
|
|
157
|
+
direction="column"
|
|
158
|
+
height="fill"
|
|
159
|
+
justify="center"
|
|
160
|
+
padding={3}
|
|
161
|
+
sizing="border"
|
|
162
|
+
>
|
|
163
|
+
<Spinner muted />
|
|
164
|
+
|
|
165
|
+
<Box marginTop={3}>
|
|
166
|
+
<Text align="center" muted size={1}>
|
|
167
|
+
Loading document
|
|
168
|
+
</Text>
|
|
169
|
+
</Box>
|
|
170
|
+
</Flex>
|
|
171
|
+
)}
|
|
172
|
+
</Box>
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function prepareMutationEvent(event: DocumentMutationEvent): PatchMsg {
|
|
177
|
+
const patches = event.mutations.map((mut) => mut.patch).filter(Boolean)
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
type: 'mutation',
|
|
181
|
+
snapshot: event.document,
|
|
182
|
+
patches: fromMutationPatches(event.origin, patches),
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function prepareRebaseEvent(event: DocumentRebaseEvent): PatchMsg {
|
|
187
|
+
const remotePatches = event.remoteMutations.map((mut) => mut.patch).filter(Boolean)
|
|
188
|
+
const localPatches = event.localMutations.map((mut) => mut.patch).filter(Boolean)
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
type: 'rebase',
|
|
192
|
+
snapshot: event.document,
|
|
193
|
+
patches: fromMutationPatches('remote', remotePatches).concat(
|
|
194
|
+
fromMutationPatches('local', localPatches)
|
|
195
|
+
),
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {isArraySchemaType, isObjectSchemaType, ObjectItem, SchemaType} from 'sanity'
|
|
2
|
+
import {randomKey} from '../randomKey'
|
|
3
|
+
|
|
4
|
+
export function createProtoValue(type: SchemaType): any {
|
|
5
|
+
if (isObjectSchemaType(type)) {
|
|
6
|
+
return type.name === 'object' ? {} : {_type: type.name}
|
|
7
|
+
}
|
|
8
|
+
if (isArraySchemaType(type)) {
|
|
9
|
+
return []
|
|
10
|
+
}
|
|
11
|
+
if (type.jsonType === 'string') {
|
|
12
|
+
return ''
|
|
13
|
+
}
|
|
14
|
+
if (type.jsonType === 'number') {
|
|
15
|
+
return 0
|
|
16
|
+
}
|
|
17
|
+
if (type.jsonType === 'boolean') {
|
|
18
|
+
return false
|
|
19
|
+
}
|
|
20
|
+
return undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createProtoArrayValue<Item extends ObjectItem>(type: SchemaType): Item {
|
|
24
|
+
if (!isObjectSchemaType(type)) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Invalid item type: "${type.type}". Default array input can only contain objects (for now)`
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {...createProtoValue(type), _key: randomKey(12)} as Item
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './DocumentForm'
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import getRandomValues from 'get-random-values-esm'
|
|
2
|
+
|
|
3
|
+
// WHATWG crypto RNG - https://w3c.github.io/webcrypto/Overview.html
|
|
4
|
+
function whatwgRNG(length = 16) {
|
|
5
|
+
const rnds8 = new Uint8Array(length)
|
|
6
|
+
getRandomValues(rnds8)
|
|
7
|
+
return rnds8
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const getByteHexTable = (() => {
|
|
11
|
+
let table: string[]
|
|
12
|
+
return () => {
|
|
13
|
+
if (table) {
|
|
14
|
+
return table
|
|
15
|
+
}
|
|
16
|
+
table = []
|
|
17
|
+
for (let i = 0; i < 256; ++i) {
|
|
18
|
+
table[i] = (i + 0x100).toString(16).substring(1)
|
|
19
|
+
}
|
|
20
|
+
return table
|
|
21
|
+
}
|
|
22
|
+
})()
|
|
23
|
+
|
|
24
|
+
export function randomKey(length?: number) {
|
|
25
|
+
const table = getByteHexTable()
|
|
26
|
+
return whatwgRNG(length)
|
|
27
|
+
.reduce((str, n) => str + table[n], '')
|
|
28
|
+
.slice(0, length)
|
|
29
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {useEffect, useRef, useState} from 'react'
|
|
2
|
+
import {catchError, distinctUntilChanged} from 'rxjs/operators'
|
|
3
|
+
import isEqual from 'react-fast-compare'
|
|
4
|
+
import {ListenQueryOptions, useClient} from 'sanity'
|
|
5
|
+
import {listenQuery} from './fixedListenQuery'
|
|
6
|
+
|
|
7
|
+
type Params = Record<string, string | number | boolean | string[]>
|
|
8
|
+
|
|
9
|
+
type ReturnShape<T> = {
|
|
10
|
+
loading: boolean
|
|
11
|
+
error: boolean
|
|
12
|
+
data: T | null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type Observable = {
|
|
16
|
+
unsubscribe: () => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const DEFAULT_PARAMS = {}
|
|
20
|
+
const DEFAULT_OPTIONS: ListenQueryOptions = {apiVersion: `v2022-05-09`}
|
|
21
|
+
|
|
22
|
+
export function useListeningQuery<T>(
|
|
23
|
+
query: string,
|
|
24
|
+
params: Params = DEFAULT_PARAMS,
|
|
25
|
+
options: ListenQueryOptions = DEFAULT_OPTIONS
|
|
26
|
+
): ReturnShape<T> {
|
|
27
|
+
const [loading, setLoading] = useState(true)
|
|
28
|
+
const [error, setError] = useState(false)
|
|
29
|
+
const [data, setData] = useState<T | null>(null)
|
|
30
|
+
const subscription = useRef<null | Observable>(null)
|
|
31
|
+
|
|
32
|
+
const client = useClient({apiVersion: `v2022-05-09`})
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (query) {
|
|
36
|
+
subscription.current = listenQuery(client, query, params, options)
|
|
37
|
+
.pipe(
|
|
38
|
+
distinctUntilChanged(isEqual),
|
|
39
|
+
catchError((err) => {
|
|
40
|
+
console.error(err)
|
|
41
|
+
setError(err)
|
|
42
|
+
setLoading(false)
|
|
43
|
+
setData(null)
|
|
44
|
+
|
|
45
|
+
return err
|
|
46
|
+
})
|
|
47
|
+
)
|
|
48
|
+
.subscribe((documents) => {
|
|
49
|
+
setData((current) => (isEqual(current, documents) ? current : documents))
|
|
50
|
+
setLoading(false)
|
|
51
|
+
setError(false)
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return () => {
|
|
56
|
+
return subscription.current ? subscription.current.unsubscribe() : undefined
|
|
57
|
+
}
|
|
58
|
+
}, [query, params, options, client])
|
|
59
|
+
|
|
60
|
+
return {loading, error, data}
|
|
61
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {Fragment, useEffect, useState} from 'react'
|
|
2
|
+
import {Connector, ConnectorOptions} from '../_lib/connector'
|
|
3
|
+
import {ConnectorPath} from './ConnectorPath'
|
|
4
|
+
|
|
5
|
+
const DEBUG = false
|
|
6
|
+
|
|
7
|
+
const options: ConnectorOptions = {
|
|
8
|
+
arrow: {
|
|
9
|
+
marginX: 10.5,
|
|
10
|
+
marginY: 5,
|
|
11
|
+
size: 4,
|
|
12
|
+
threshold: 16.5,
|
|
13
|
+
},
|
|
14
|
+
divider: {
|
|
15
|
+
offsetX: -10.5,
|
|
16
|
+
},
|
|
17
|
+
path: {
|
|
18
|
+
cornerRadius: 3,
|
|
19
|
+
marginY: 10.5,
|
|
20
|
+
strokeWidth: 1,
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function AssistConnectorsOverlay(props: {connectors: Connector[]}) {
|
|
25
|
+
const {connectors} = props
|
|
26
|
+
// const zIndexes = connectors.map((connector) => {
|
|
27
|
+
// const zIndex = connector.from.payload?.zIndex
|
|
28
|
+
|
|
29
|
+
// if (typeof zIndex === 'number') {
|
|
30
|
+
// return zIndex
|
|
31
|
+
// }
|
|
32
|
+
|
|
33
|
+
// return 1
|
|
34
|
+
// })
|
|
35
|
+
const [, setRedraw] = useState(false)
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
// hacky workaround to force redraw for connectors on initial render
|
|
38
|
+
// this seem to improve initial measurements of elements
|
|
39
|
+
setRedraw(true)
|
|
40
|
+
}, [])
|
|
41
|
+
|
|
42
|
+
// const zIndex = zIndexes.length ? Math.max(...zIndexes) : 1
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<>
|
|
46
|
+
<svg
|
|
47
|
+
fill="none"
|
|
48
|
+
width={window.innerWidth}
|
|
49
|
+
height={window.innerHeight}
|
|
50
|
+
style={{
|
|
51
|
+
position: 'absolute',
|
|
52
|
+
top: 0,
|
|
53
|
+
left: 0,
|
|
54
|
+
width: '100%',
|
|
55
|
+
height: '100%',
|
|
56
|
+
pointerEvents: 'none',
|
|
57
|
+
zIndex: 150,
|
|
58
|
+
// zIndex,
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
{connectors.map((connector) => (
|
|
62
|
+
<ConnectorPath
|
|
63
|
+
from={connector.from}
|
|
64
|
+
key={connector.key}
|
|
65
|
+
options={options}
|
|
66
|
+
to={connector.to}
|
|
67
|
+
/>
|
|
68
|
+
))}
|
|
69
|
+
</svg>
|
|
70
|
+
{DEBUG &&
|
|
71
|
+
connectors.map(({key, from, to}) => {
|
|
72
|
+
return (
|
|
73
|
+
<Fragment key={key}>
|
|
74
|
+
<div
|
|
75
|
+
style={{
|
|
76
|
+
position: 'fixed',
|
|
77
|
+
top: from.bounds.y,
|
|
78
|
+
left: from.bounds.x,
|
|
79
|
+
width: from.bounds.w,
|
|
80
|
+
height: from.bounds.h,
|
|
81
|
+
pointerEvents: 'none',
|
|
82
|
+
overflow: 'hidden',
|
|
83
|
+
outline: '1px dotted red',
|
|
84
|
+
outlineOffset: -1,
|
|
85
|
+
zIndex: 10000000 - 1,
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
<div
|
|
89
|
+
style={{
|
|
90
|
+
position: 'absolute',
|
|
91
|
+
top: from.element.y - from.bounds.y,
|
|
92
|
+
left: from.element.x - from.bounds.x,
|
|
93
|
+
width: from.element.w,
|
|
94
|
+
height: from.element.h,
|
|
95
|
+
border: '1px solid red',
|
|
96
|
+
boxSizing: 'border-box',
|
|
97
|
+
}}
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div
|
|
102
|
+
style={{
|
|
103
|
+
position: 'fixed',
|
|
104
|
+
top: to.bounds.y,
|
|
105
|
+
left: to.bounds.x,
|
|
106
|
+
width: to.bounds.w,
|
|
107
|
+
height: to.bounds.h,
|
|
108
|
+
pointerEvents: 'none',
|
|
109
|
+
overflow: 'hidden',
|
|
110
|
+
outline: '1px dotted teal',
|
|
111
|
+
outlineOffset: -1,
|
|
112
|
+
zIndex: 10000000 - 1,
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
<div
|
|
116
|
+
style={{
|
|
117
|
+
position: 'absolute',
|
|
118
|
+
top: to.element.y - to.bounds.y,
|
|
119
|
+
left: to.element.x - to.bounds.x,
|
|
120
|
+
width: to.element.w,
|
|
121
|
+
height: to.element.h,
|
|
122
|
+
border: '1px solid teal',
|
|
123
|
+
boxSizing: 'border-box',
|
|
124
|
+
}}
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
</Fragment>
|
|
128
|
+
)
|
|
129
|
+
})}
|
|
130
|
+
</>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {rgba, useTheme} from '@sanity/ui'
|
|
2
|
+
import {useMemo} from 'react'
|
|
3
|
+
import {ConnectorOptions, mapConnectorToLine, Rect} from '../_lib/connector'
|
|
4
|
+
import {arrowPath} from './draw/arrowPath'
|
|
5
|
+
import {drawConnectorPath} from './draw/connectorPath'
|
|
6
|
+
|
|
7
|
+
export function ConnectorPath(props: {
|
|
8
|
+
from: {bounds: Rect; element: Rect}
|
|
9
|
+
to: {bounds: Rect; element: Rect}
|
|
10
|
+
options: ConnectorOptions
|
|
11
|
+
}) {
|
|
12
|
+
const {from, options, to} = props
|
|
13
|
+
const {strokeWidth} = options.path
|
|
14
|
+
const theme = useTheme()
|
|
15
|
+
|
|
16
|
+
const line = useMemo(() => mapConnectorToLine(options, {from, to}), [from, options, to])
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
<path
|
|
21
|
+
d={drawConnectorPath(options, line)}
|
|
22
|
+
stroke={theme.sanity.color.base.bg}
|
|
23
|
+
strokeWidth={strokeWidth + 4}
|
|
24
|
+
/>
|
|
25
|
+
|
|
26
|
+
<path
|
|
27
|
+
d={drawConnectorPath(options, line)}
|
|
28
|
+
stroke={rgba(theme.sanity.color.base.border, 0.5)}
|
|
29
|
+
strokeWidth={strokeWidth}
|
|
30
|
+
/>
|
|
31
|
+
|
|
32
|
+
{line.from.isAbove && (
|
|
33
|
+
<path
|
|
34
|
+
d={arrowPath(
|
|
35
|
+
options,
|
|
36
|
+
line.from.x + options.arrow.marginX,
|
|
37
|
+
line.from.bounds.y - options.arrow.threshold + options.arrow.marginY,
|
|
38
|
+
-1
|
|
39
|
+
)}
|
|
40
|
+
stroke={theme.sanity.color.base.border}
|
|
41
|
+
strokeWidth={strokeWidth}
|
|
42
|
+
/>
|
|
43
|
+
)}
|
|
44
|
+
|
|
45
|
+
{line.from.isBelow && (
|
|
46
|
+
<path
|
|
47
|
+
d={arrowPath(
|
|
48
|
+
options,
|
|
49
|
+
line.from.x + options.arrow.marginX,
|
|
50
|
+
line.from.bounds.y +
|
|
51
|
+
line.from.bounds.h +
|
|
52
|
+
options.arrow.threshold -
|
|
53
|
+
options.arrow.marginY,
|
|
54
|
+
1
|
|
55
|
+
)}
|
|
56
|
+
stroke={theme.sanity.color.base.border}
|
|
57
|
+
strokeWidth={strokeWidth}
|
|
58
|
+
/>
|
|
59
|
+
)}
|
|
60
|
+
</>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import {ConnectorOptions} from '../../_lib/connector'
|
|
2
|
+
|
|
3
|
+
export function arrowPath(options: ConnectorOptions, x: number, y: number, dir: 1 | -1): string {
|
|
4
|
+
return [
|
|
5
|
+
`M ${x - options.arrow.size} ${y - options.arrow.size * dir} `,
|
|
6
|
+
`L ${x} ${y}`,
|
|
7
|
+
`L ${x + options.arrow.size} ${y - options.arrow.size * dir}`,
|
|
8
|
+
].join('')
|
|
9
|
+
}
|