sanity-plugin-dashboard-widget-vercel 3.1.1 → 3.1.3

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.
@@ -1,10 +1,10 @@
1
1
  import {UploadIcon} from '@sanity/icons'
2
2
  import {Box, Button, useToast} from '@sanity/ui'
3
3
  import {useMachine} from '@xstate/react'
4
- import React, {useEffect, useMemo} from 'react'
4
+ import React, {useEffect} from 'react'
5
5
 
6
6
  import {WIDGET_NAME} from '../../constants'
7
- import deployMachine from '../../machines/deploy'
7
+ import {deployMachine} from '../../machines/deploy'
8
8
  import StateDebug from '../StateDebug'
9
9
 
10
10
  type Props = {
@@ -16,9 +16,9 @@ type Props = {
16
16
  const DeployButton = (props: Props) => {
17
17
  const {deployHook, onDeploySuccess, targetName} = props
18
18
 
19
- const machine = useMemo(() => deployMachine(deployHook), [deployHook])
20
-
21
- const [deployState, deployStateTransition, deployStateInterpreter] = useMachine(machine)
19
+ const [deployState, deployStateTransition, deployStateInterpreter] = useMachine(deployMachine, {
20
+ input: {deployHook},
21
+ })
22
22
 
23
23
  const toast = useToast()
24
24
 
@@ -54,18 +54,19 @@ const DeployButton = (props: Props) => {
54
54
  }, [isError, isSuccess, toast, targetName, deployState.context.error])
55
55
 
56
56
  useEffect(() => {
57
- deployStateInterpreter.onTransition((state) => {
57
+ const subscription = deployStateInterpreter.subscribe((state) => {
58
58
  if (state.value === 'success') {
59
59
  if (onDeploySuccess) {
60
60
  onDeploySuccess()
61
61
  }
62
62
  }
63
63
  })
64
+
65
+ return () => subscription.unsubscribe()
64
66
  }, [deployStateInterpreter, onDeploySuccess])
65
67
 
66
68
  return (
67
69
  <Box padding={3} style={{position: 'relative'}}>
68
- {/* xstate debug */}
69
70
  <StateDebug name="Deploy" state={deployState} />
70
71
 
71
72
  <Button
@@ -1,19 +1,17 @@
1
- // @ts-expect-error - fix typings later
2
1
  import {yupResolver} from '@hookform/resolvers/yup'
3
- import {Box, Button, Dialog, Flex, Stack} from '@sanity/ui'
4
- import {uuid} from '@sanity/uuid'
5
- import {useMachine} from '@xstate/react'
6
- import React, {FC} from 'react'
7
- // @ts-expect-error - fix typings later
2
+ import {Box, Button, Dialog, Flex, Stack, useToast} from '@sanity/ui'
3
+ import {useActor} from '@xstate/react'
4
+ import React, {FC, useEffect} from 'react'
8
5
  import {useForm} from 'react-hook-form'
9
6
  import * as yup from 'yup'
10
7
 
11
- import {DEPLOYMENT_TARGET_DOCUMENT_TYPE, Z_INDEX_DIALOG} from '../../constants'
12
- import formMachine from '../../machines/form'
8
+ import {Z_INDEX_DIALOG} from '../../constants'
9
+ import {formMachine} from '../../machines/form'
13
10
  import sanitizeFormData from '../../utils/sanitizeFormData'
14
11
  import FormFieldInputText from '../FormFieldInputText'
15
12
  import {Sanity} from '../../types'
16
13
  import {useSanityClient} from '../../client'
14
+ import {toPromise} from 'xstate'
17
15
 
18
16
  type Props = {
19
17
  deploymentTarget?: Sanity.DeploymentTarget
@@ -44,129 +42,111 @@ const formSchema = yup.object().shape({
44
42
  const DialogForm: FC<Props> = (props: Props) => {
45
43
  const {deploymentTarget, onClose, onCreate, onDelete, onUpdate} = props
46
44
  const client = useSanityClient()
45
+ const toast = useToast()
47
46
 
48
- // xstate
49
- const [formState, formStateTransition] = useMachine(formMachine, {
50
- services: {
51
- formSubmittedService: async () => {
52
- onClose()
53
- },
54
- // TODO: refactor
55
- createDocumentService: async (_context, event: any) => {
56
- let document
57
- try {
58
- document = await client.create({
59
- _id: `vercel.${uuid()}`,
60
- _type: DEPLOYMENT_TARGET_DOCUMENT_TYPE,
61
- ...event.formData,
62
- })
63
- if (onCreate) {
64
- onCreate(document as Sanity.DeploymentTarget)
65
- }
66
- return Promise.resolve()
67
- } catch (e) {
68
- return Promise.reject(e)
69
- }
70
- },
71
- // TODO: refactor
72
- deleteDocumentService: async () => {
73
- if (deploymentTarget) {
74
- try {
75
- await client.delete(deploymentTarget._id)
76
- if (onDelete) {
77
- onDelete(deploymentTarget._id)
78
- }
79
- return Promise.resolve()
80
- } catch (e) {
81
- return Promise.reject(e)
82
- }
83
- }
84
- return Promise.resolve()
85
- },
86
- // TODO: refactor
87
- updateDocumentService: async (_context, event: any) => {
88
- let document
89
- if (deploymentTarget) {
90
- try {
91
- document = await client.patch(deploymentTarget._id).set(event.formData).commit()
92
- if (onUpdate) {
93
- onUpdate(document as Sanity.DeploymentTarget)
94
- }
95
- return Promise.resolve()
96
- } catch (e) {
97
- return Promise.reject(e)
98
- }
99
- }
100
- return Promise.resolve()
101
- },
102
- },
47
+ const [formState, formStateTransition, formStateActorRef] = useActor(formMachine, {
48
+ input: {client},
103
49
  })
104
50
 
105
- const formUpdating =
106
- formState.matches('creating') || formState.matches('deleting') || formState.matches('updating')
51
+ const formUpdating = formState.hasTag('busy')
107
52
 
108
- // react-hook-form
53
+ // react-hook-form v7
109
54
  const {
110
- // Read the formState before render to subscribe the form state through Proxy
111
55
  formState: {errors, isDirty, isValid},
112
56
  handleSubmit,
113
57
  register,
114
- } = useForm({
58
+ } = useForm<FormData>({
59
+ // @ts-expect-error - fix typings later
115
60
  defaultValues: {
116
61
  deployHook: deploymentTarget?.deployHook || '',
117
62
  deployLimit: deploymentTarget?.deployLimit || 5,
118
- name: deploymentTarget?.name,
119
- projectId: deploymentTarget?.projectId,
63
+ name: deploymentTarget?.name || '',
64
+ projectId: deploymentTarget?.projectId || '',
120
65
  teamId: deploymentTarget?.teamId || '',
121
- token: deploymentTarget?.token,
66
+ token: deploymentTarget?.token || '',
122
67
  },
123
68
  mode: 'onChange',
124
69
  resolver: yupResolver(formSchema),
125
70
  })
126
71
 
72
+ /**
73
+ * Handle errors and reaching the done state
74
+ */
75
+ useEffect(() => {
76
+ if (formState.matches('error')) {
77
+ toast.push({
78
+ status: 'error',
79
+ title: formState.context.message || 'An error occurred',
80
+ })
81
+ }
82
+ /**
83
+ * If the machine is done it means it reached updated, created, deleted or error state.
84
+ * We don't care which one, we just want to close the dialog
85
+ */
86
+ if (formState.status === 'done') {
87
+ onClose()
88
+ }
89
+ }, [formState, onClose, toast])
90
+
127
91
  // Callbacks
128
92
  // - submit react-hook-form
129
93
  const onSubmit = async (formData: FormData) => {
130
94
  const sanitizedFormData = sanitizeFormData(formData)
131
- await formStateTransition(deploymentTarget ? 'UPDATE' : 'CREATE', {
132
- formData: sanitizedFormData,
133
- })
95
+ if (deploymentTarget) {
96
+ formStateTransition({type: 'UPDATE', id: deploymentTarget._id, formData: sanitizedFormData})
97
+ } else {
98
+ formStateTransition({type: 'CREATE', formData: sanitizedFormData})
99
+ }
100
+ await toPromise(formStateActorRef)
101
+ const snapshot = formStateActorRef.getSnapshot()
102
+ const {document} = snapshot.context
103
+ if (!document) return
104
+ if (snapshot.matches('created')) {
105
+ onCreate?.(document)
106
+ } else if (snapshot.matches('updated')) {
107
+ onUpdate?.(document)
108
+ }
134
109
  }
135
110
 
136
- const handleDelete = () => {
137
- formStateTransition('DELETE', {id: deploymentTarget?._id})
111
+ const handleDelete = async () => {
112
+ const id = deploymentTarget!._id
113
+ formStateTransition({type: 'DELETE', id})
114
+ await toPromise(formStateActorRef)
115
+ if (formStateActorRef.getSnapshot().matches('deleted')) {
116
+ onDelete?.(id)
117
+ }
138
118
  }
139
119
 
140
- const Footer = () => (
141
- <Box padding={3}>
142
- <Flex justify={deploymentTarget ? 'space-between' : 'flex-end'}>
143
- {/* Delete button */}
144
- {deploymentTarget && (
145
- <Button
146
- disabled={formUpdating}
147
- fontSize={1}
148
- mode="bleed"
149
- onClick={handleDelete}
150
- text="Delete"
151
- tone="critical"
152
- />
153
- )}
154
-
155
- {/* Submit button */}
156
- <Button
157
- disabled={formUpdating || !isDirty || !isValid}
158
- fontSize={1}
159
- onClick={handleSubmit(onSubmit)}
160
- text={deploymentTarget ? 'Update and close' : 'Create'}
161
- tone="primary"
162
- />
163
- </Flex>
164
- </Box>
165
- )
166
-
167
120
  return (
168
121
  <Dialog
169
- footer={<Footer />}
122
+ footer={
123
+ <Box padding={3}>
124
+ <Flex justify={deploymentTarget ? 'space-between' : 'flex-end'}>
125
+ {/* Delete button */}
126
+ {deploymentTarget && (
127
+ <Button
128
+ loading={formState.matches('deleting')}
129
+ disabled={formUpdating}
130
+ fontSize={1}
131
+ mode="bleed"
132
+ onClick={handleDelete}
133
+ text="Delete"
134
+ tone="critical"
135
+ />
136
+ )}
137
+
138
+ {/* Submit button */}
139
+ <Button
140
+ loading={formState.matches('creating') || formState.matches('updating')}
141
+ disabled={!isDirty || !isValid}
142
+ fontSize={1}
143
+ onClick={handleSubmit(onSubmit)}
144
+ text={deploymentTarget ? 'Update and close' : 'Create'}
145
+ tone="primary"
146
+ />
147
+ </Flex>
148
+ </Box>
149
+ }
170
150
  header={`${deploymentTarget ? 'Edit' : 'Create'} deployment target`}
171
151
  id="create"
172
152
  onClose={onClose}
@@ -186,24 +166,24 @@ const DialogForm: FC<Props> = (props: Props) => {
186
166
  description="Name displayed in this plugin (e.g. production, staging)"
187
167
  error={errors?.name}
188
168
  label="Name"
189
- name="name"
190
- ref={register}
169
+ // @ts-expect-error - fix typings later
170
+ {...register('name')}
191
171
  />
192
172
 
193
173
  <FormFieldInputText
194
174
  disabled={formUpdating}
195
175
  error={errors?.token}
196
176
  label="Vercel Account Token"
197
- name="token"
198
- ref={register}
177
+ // @ts-expect-error - fix typings later
178
+ {...register('token')}
199
179
  />
200
180
 
201
181
  <FormFieldInputText
202
182
  disabled={formUpdating}
203
183
  error={errors?.projectId}
204
184
  label="Vercel Project ID"
205
- name="projectId"
206
- ref={register}
185
+ // @ts-expect-error - fix typings later
186
+ {...register('projectId')}
207
187
  />
208
188
 
209
189
  <FormFieldInputText
@@ -211,8 +191,8 @@ const DialogForm: FC<Props> = (props: Props) => {
211
191
  disabled={formUpdating}
212
192
  error={errors?.teamId}
213
193
  label="Vercel Team ID (optional)"
214
- name="teamId"
215
- ref={register}
194
+ // @ts-expect-error - fix typings later
195
+ {...register('teamId')}
216
196
  />
217
197
 
218
198
  <FormFieldInputText
@@ -220,16 +200,16 @@ const DialogForm: FC<Props> = (props: Props) => {
220
200
  disabled={formUpdating}
221
201
  error={errors?.deployHook}
222
202
  label="Vercel Deploy Hook (optional)"
223
- name="deployHook"
224
- ref={register}
203
+ // @ts-expect-error - fix typings later
204
+ {...register('deployHook')}
225
205
  />
226
206
 
227
207
  <FormFieldInputText
228
208
  disabled={formUpdating}
229
209
  error={errors?.deployLimit}
230
210
  label="Number of deploys to display"
231
- name="deployLimit"
232
- ref={register({valueAsNumber: true})}
211
+ // @ts-expect-error - fix typings later
212
+ {...register('deployLimit', {valueAsNumber: true})}
233
213
  />
234
214
  </Stack>
235
215
  </Box>
@@ -1,7 +1,6 @@
1
1
  import {ErrorOutlineIcon} from '@sanity/icons'
2
2
  import {Box, Inline, Text, Tooltip} from '@sanity/ui'
3
3
  import React, {FC} from 'react'
4
- // @ts-expect-error - fix typings later
5
4
  import {FieldError} from 'react-hook-form'
6
5
  import {styled} from 'styled-components'
7
6
 
@@ -1,6 +1,5 @@
1
1
  import {Box, TextInput} from '@sanity/ui'
2
2
  import React, {forwardRef} from 'react'
3
- // @ts-expect-error - fix typings later
4
3
  import {FieldError} from 'react-hook-form'
5
4
 
6
5
  import FormFieldInputLabel from '../FormFieldInputLabel'
@@ -13,12 +12,12 @@ type Props = {
13
12
  name: string
14
13
  placeholder?: string
15
14
  value?: string
15
+ onChange?: React.ChangeEventHandler<HTMLInputElement>
16
+ onBlur?: React.FocusEventHandler<HTMLInputElement>
16
17
  }
17
18
 
18
- type Ref = HTMLInputElement
19
-
20
- const FormFieldInputText = forwardRef<Ref, Props>((props: Props, ref) => {
21
- const {description, disabled, error, label, name, placeholder, value} = props
19
+ const FormFieldInputText = forwardRef<HTMLInputElement, Props>((props: Props, ref) => {
20
+ const {description, disabled, error, label, name, placeholder, value, onChange, onBlur} = props
22
21
 
23
22
  return (
24
23
  <Box>
@@ -33,6 +32,8 @@ const FormFieldInputText = forwardRef<Ref, Props>((props: Props, ref) => {
33
32
  id={name}
34
33
  name={name}
35
34
  placeholder={placeholder}
35
+ onChange={onChange}
36
+ onBlur={onBlur}
36
37
  ref={ref}
37
38
  />
38
39
  </Box>
@@ -1,8 +1,8 @@
1
- import fetch from 'unfetch'
2
- import {assign, Machine} from 'xstate'
1
+ import {assign, setup, fromPromise} from 'xstate'
3
2
  import {Vercel} from '../types'
4
3
 
5
4
  type Context = {
5
+ deployHook: string
6
6
  disabled: boolean
7
7
  feedback?: string
8
8
  label?: string
@@ -11,104 +11,107 @@ type Context = {
11
11
 
12
12
  type Event = {type: 'DEPLOY'}
13
13
 
14
- type Schema = {
15
- states: {
16
- idle: {}
17
- deploying: {}
18
- success: {}
19
- error: {}
20
- }
14
+ interface DeployActorInput {
15
+ deployHook: string
21
16
  }
22
17
 
23
- const deployMachine = (deployHook: string) =>
24
- Machine<Context, Schema, Event>(
25
- // Machine
26
- {
27
- id: 'deploy',
28
- initial: 'idle',
29
- context: {
30
- disabled: false,
31
- feedback: undefined,
32
- label: undefined,
33
- error: undefined,
18
+ export const deployMachine = setup({
19
+ types: {
20
+ context: {} as Context,
21
+ events: {} as Event,
22
+ input: {} as DeployActorInput,
23
+ },
24
+ actors: {
25
+ deploy: fromPromise(async ({input, signal}: {input: DeployActorInput; signal: AbortSignal}) => {
26
+ try {
27
+ if (!input.deployHook) {
28
+ throw new Error('No deployHook URL defined')
29
+ }
30
+ const res = await fetch(input.deployHook, {method: 'POST', signal})
31
+ const data = await res.json()
32
+ if (!res.ok) {
33
+ const errorMessage = (data?.error as Vercel.Error).message || res.statusText
34
+ throw errorMessage
35
+ }
36
+ } catch (err) {
37
+ if (typeof err === 'string') {
38
+ throw err
39
+ }
40
+ console.error('Unable to deploy with error:', err)
41
+ throw new Error('Please check the developer console for more information')
42
+ }
43
+ }),
44
+ },
45
+ }).createMachine({
46
+ /** @xstate-layout N4IgpgJg5mDOIC5QTABwDYHsCeA6AlhOmAMQAiAogAoAyA8gJoDaADALqKiqaz4Au+TADtOIAB6IATCxa4AnADYAHAHYArArUslAZjksNAGhDZEARk3yVmhSxVyHC-QBYAvq+MoMOXF6zZ8ISgSCGEwAiEAN0wAa3C-HwSAoIRA6IBjAEMBYVY2PNFuXhyRJHFENTMzXElFJSUzNTVnWx0FY1MESXVcZzUdM2k1OTUVBoV3TzR-X2mcQOCwACclzCXcDGyAMzWAW1nvPCSF1KjMLJK8grKi-kFS0AkESura5QamlpY2jvMB+QcDkqLH0wxYEw8ICSuFgAFd0uk4LByNR6Mx2IUeHdhKIns1ZN9BtYQVpRnJfl1JM55P1BnY1Ep9CpGpMoXM8MtVksUbRGNcuFiSriKs4CQNurYRgZ7BSGr1ASNupIzEo+kp3JChJgUPAyklMcV7sKEABadomRAmtQAhW2wE6VnQwjEA3Yh7lBDOSQUsxeqyaPoWYbWFSO9kHfwLV1CspPHSSHQ1ZyA1UKJzJlQ+iz+5oKewKWrfNyQ6FwhFI6NG2OINOSXDiypKAxtMyZi1dEE1Qk6L0FpTSUMl8OctaVnHVhC1+uDRvNhStin6XDaHQ9liSXTJiUa1xAA */
47
+ id: 'deploy',
48
+ initial: 'idle',
49
+ context: ({input}) => ({
50
+ disabled: false,
51
+ feedback: undefined,
52
+ label: undefined,
53
+ error: undefined,
54
+ deployHook: input.deployHook,
55
+ }),
56
+ states: {
57
+ idle: {
58
+ entry: assign({
59
+ feedback: () => undefined,
60
+ label: () => 'Deploy',
61
+ }),
62
+ on: {
63
+ DEPLOY: {
64
+ target: 'deploying',
65
+ },
34
66
  },
35
- states: {
36
- idle: {
37
- entry: assign({
38
- feedback: () => undefined,
39
- label: () => 'Deploy',
40
- }),
41
- on: {
42
- DEPLOY: 'deploying',
43
- },
67
+ },
68
+ deploying: {
69
+ entry: assign({
70
+ disabled: () => true,
71
+ label: () => 'Deploying',
72
+ }),
73
+ exit: assign({
74
+ disabled: () => false,
75
+ label: () => 'Deploy',
76
+ }),
77
+ invoke: {
78
+ src: 'deploy',
79
+ input: ({context}) => ({deployHook: context.deployHook}),
80
+ onDone: {
81
+ target: 'success',
44
82
  },
45
- deploying: {
46
- entry: assign({
47
- disabled: () => true,
48
- label: () => 'Deploying',
49
- }),
50
- exit: assign({
51
- disabled: () => false,
52
- label: () => 'Deploy',
53
- }),
54
- invoke: {
55
- onDone: {
56
- target: 'success',
57
- },
58
- onError: {
59
- target: 'error',
60
- actions: assign({
61
- error: (_context, event) => {
62
- return event.data
63
- },
64
- }),
83
+ onError: {
84
+ target: 'error',
85
+ actions: assign({
86
+ error: ({event}) => {
87
+ if ('error' in event) {
88
+ return event.error as unknown as string
89
+ }
90
+ return 'Unknown error'
65
91
  },
66
- src: 'deploy',
67
- },
68
- },
69
- success: {
70
- entry: [assign({feedback: () => 'Succesfully started!'})],
71
- exit: assign({
72
- feedback: () => undefined,
73
92
  }),
74
- on: {
75
- DEPLOY: 'deploying',
76
- },
77
93
  },
78
- error: {
79
- on: {
80
- DEPLOY: 'deploying',
81
- },
94
+ },
95
+ },
96
+ success: {
97
+ entry: assign({
98
+ feedback: () => 'Successfully started!',
99
+ }),
100
+ exit: assign({
101
+ feedback: () => undefined,
102
+ }),
103
+ on: {
104
+ DEPLOY: {
105
+ target: 'deploying',
82
106
  },
83
107
  },
84
108
  },
85
- // Config
86
- {
87
- services: {
88
- deploy: (): Promise<void> => {
89
- return new Promise(async (resolve, reject) => {
90
- try {
91
- if (!deployHook) {
92
- return reject(new Error('No deployHook URL defined'))
93
- }
94
-
95
- const res = await fetch(deployHook, {method: 'POST'})
96
- const data = await res.json()
97
-
98
- if (!res.ok) {
99
- const errorMessage = (data?.error as Vercel.Error).message || res.statusText
100
- return reject(errorMessage)
101
- }
102
-
103
- return resolve()
104
- } catch (err) {
105
- console.error('Unable to deploy with error:', err)
106
- return reject(new Error('Please check the developer console for more information'))
107
- }
108
- })
109
+ error: {
110
+ on: {
111
+ DEPLOY: {
112
+ target: 'deploying',
109
113
  },
110
114
  },
111
- }
112
- )
113
-
114
- export default deployMachine
115
+ },
116
+ },
117
+ })