sanity-plugin-dashboard-widget-vercel 3.1.2 → 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,17 +1,17 @@
1
1
  import {yupResolver} from '@hookform/resolvers/yup'
2
- import {Box, Button, Dialog, Flex, Stack} from '@sanity/ui'
3
- import {uuid} from '@sanity/uuid'
4
- import {useMachine} from '@xstate/react'
5
- import React, {FC} from 'react'
2
+ import {Box, Button, Dialog, Flex, Stack, useToast} from '@sanity/ui'
3
+ import {useActor} from '@xstate/react'
4
+ import React, {FC, useEffect} from 'react'
6
5
  import {useForm} from 'react-hook-form'
7
6
  import * as yup from 'yup'
8
7
 
9
- import {DEPLOYMENT_TARGET_DOCUMENT_TYPE, Z_INDEX_DIALOG} from '../../constants'
10
- import formMachine from '../../machines/form'
8
+ import {Z_INDEX_DIALOG} from '../../constants'
9
+ import {formMachine} from '../../machines/form'
11
10
  import sanitizeFormData from '../../utils/sanitizeFormData'
12
11
  import FormFieldInputText from '../FormFieldInputText'
13
12
  import {Sanity} from '../../types'
14
13
  import {useSanityClient} from '../../client'
14
+ import {toPromise} from 'xstate'
15
15
 
16
16
  type Props = {
17
17
  deploymentTarget?: Sanity.DeploymentTarget
@@ -42,66 +42,13 @@ const formSchema = yup.object().shape({
42
42
  const DialogForm: FC<Props> = (props: Props) => {
43
43
  const {deploymentTarget, onClose, onCreate, onDelete, onUpdate} = props
44
44
  const client = useSanityClient()
45
+ const toast = useToast()
45
46
 
46
- // xstate
47
- const [formState, formStateTransition] = useMachine(formMachine, {
48
- services: {
49
- formSubmittedService: async () => {
50
- onClose()
51
- },
52
- // TODO: refactor
53
- createDocumentService: async (_context, event: any) => {
54
- let document
55
- try {
56
- document = await client.create({
57
- _id: `vercel.${uuid()}`,
58
- _type: DEPLOYMENT_TARGET_DOCUMENT_TYPE,
59
- ...event.formData,
60
- })
61
- if (onCreate) {
62
- onCreate(document as Sanity.DeploymentTarget)
63
- }
64
- return Promise.resolve()
65
- } catch (e) {
66
- return Promise.reject(e)
67
- }
68
- },
69
- // TODO: refactor
70
- deleteDocumentService: async () => {
71
- if (deploymentTarget) {
72
- try {
73
- await client.delete(deploymentTarget._id)
74
- if (onDelete) {
75
- onDelete(deploymentTarget._id)
76
- }
77
- return Promise.resolve()
78
- } catch (e) {
79
- return Promise.reject(e)
80
- }
81
- }
82
- return Promise.resolve()
83
- },
84
- // TODO: refactor
85
- updateDocumentService: async (_context, event: any) => {
86
- let document
87
- if (deploymentTarget) {
88
- try {
89
- document = await client.patch(deploymentTarget._id).set(event.formData).commit()
90
- if (onUpdate) {
91
- onUpdate(document as Sanity.DeploymentTarget)
92
- }
93
- return Promise.resolve()
94
- } catch (e) {
95
- return Promise.reject(e)
96
- }
97
- }
98
- return Promise.resolve()
99
- },
100
- },
47
+ const [formState, formStateTransition, formStateActorRef] = useActor(formMachine, {
48
+ input: {client},
101
49
  })
102
50
 
103
- const formUpdating =
104
- formState.matches('creating') || formState.matches('deleting') || formState.matches('updating')
51
+ const formUpdating = formState.hasTag('busy')
105
52
 
106
53
  // react-hook-form v7
107
54
  const {
@@ -122,49 +69,84 @@ const DialogForm: FC<Props> = (props: Props) => {
122
69
  resolver: yupResolver(formSchema),
123
70
  })
124
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
+
125
91
  // Callbacks
126
92
  // - submit react-hook-form
127
93
  const onSubmit = async (formData: FormData) => {
128
94
  const sanitizedFormData = sanitizeFormData(formData)
129
- await formStateTransition(deploymentTarget ? 'UPDATE' : 'CREATE', {
130
- formData: sanitizedFormData,
131
- })
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
+ }
132
109
  }
133
110
 
134
- const handleDelete = () => {
135
- 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
+ }
136
118
  }
137
119
 
138
- const Footer = () => (
139
- <Box padding={3}>
140
- <Flex justify={deploymentTarget ? 'space-between' : 'flex-end'}>
141
- {/* Delete button */}
142
- {deploymentTarget && (
143
- <Button
144
- disabled={formUpdating}
145
- fontSize={1}
146
- mode="bleed"
147
- onClick={handleDelete}
148
- text="Delete"
149
- tone="critical"
150
- />
151
- )}
152
-
153
- {/* Submit button */}
154
- <Button
155
- disabled={formUpdating || !isDirty || !isValid}
156
- fontSize={1}
157
- onClick={handleSubmit(onSubmit)}
158
- text={deploymentTarget ? 'Update and close' : 'Create'}
159
- tone="primary"
160
- />
161
- </Flex>
162
- </Box>
163
- )
164
-
165
120
  return (
166
121
  <Dialog
167
- 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
+ }
168
150
  header={`${deploymentTarget ? 'Edit' : 'Create'} deployment target`}
169
151
  id="create"
170
152
  onClose={onClose}
@@ -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
+ })