@liiift-studio/deploy-vercel-from-sanity 0.1.7 → 0.2.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/README.md +44 -49
- package/dist/index.js +586 -329
- package/dist/index.mjs +597 -336
- package/package.json +10 -4
- package/src/components/DeployHistory.tsx +1 -1
- package/src/components/DeployItem.tsx +346 -275
- package/src/components/DeployTargetForm.tsx +143 -0
- package/src/components/DeployTool.tsx +127 -33
- package/src/components/StatusBadge.tsx +1 -1
- package/src/components/TokenSetup.tsx +57 -54
- package/src/version.ts +2 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Dialog form for creating and editing vercel_deploy documents
|
|
2
|
+
import { useState, useCallback } from 'react'
|
|
3
|
+
import { useClient } from 'sanity'
|
|
4
|
+
import {
|
|
5
|
+
Dialog, Box, Stack, Flex, Text, TextInput, Button, Switch, Label, Card,
|
|
6
|
+
} from '@sanity/ui'
|
|
7
|
+
import { CheckmarkCircleIcon } from '@sanity/icons'
|
|
8
|
+
import type { DeployTarget } from '../types'
|
|
9
|
+
|
|
10
|
+
const VERCEL_HOOK_RE = /^https:\/\/api\.vercel\.com\/v1\/integrations\/deploy\//
|
|
11
|
+
|
|
12
|
+
interface DeployTargetFormProps {
|
|
13
|
+
/** When provided, form is in edit mode; otherwise create mode */
|
|
14
|
+
initial?: DeployTarget
|
|
15
|
+
onSaved: () => void
|
|
16
|
+
onClose: () => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function DeployTargetForm({ initial, onSaved, onClose }: DeployTargetFormProps) {
|
|
20
|
+
const client = useClient({ apiVersion: '2025-01-01' })
|
|
21
|
+
const isEdit = Boolean(initial)
|
|
22
|
+
|
|
23
|
+
const [name, setName] = useState(initial?.name ?? '')
|
|
24
|
+
const [url, setUrl] = useState(initial?.url ?? '')
|
|
25
|
+
const [teamId, setTeamId] = useState(initial?.teamId ?? '')
|
|
26
|
+
const [disableDelete, setDisableDelete] = useState(initial?.disableDeleteAction ?? false)
|
|
27
|
+
const [saving, setSaving] = useState(false)
|
|
28
|
+
const [error, setError] = useState<string | null>(null)
|
|
29
|
+
|
|
30
|
+
const urlValid = !url || VERCEL_HOOK_RE.test(url.trim())
|
|
31
|
+
const canSave = name.trim() && url.trim() && urlValid
|
|
32
|
+
|
|
33
|
+
const save = useCallback(async () => {
|
|
34
|
+
if (!canSave) return
|
|
35
|
+
setSaving(true)
|
|
36
|
+
setError(null)
|
|
37
|
+
const fields = {
|
|
38
|
+
name: name.trim(),
|
|
39
|
+
url: url.trim(),
|
|
40
|
+
...(teamId.trim() ? { teamId: teamId.trim() } : { teamId: null }),
|
|
41
|
+
disableDeleteAction: disableDelete,
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
if (isEdit && initial) {
|
|
45
|
+
await client.patch(initial._id).set(fields).commit()
|
|
46
|
+
} else {
|
|
47
|
+
await client.create({ _type: 'vercel_deploy', ...fields })
|
|
48
|
+
}
|
|
49
|
+
onSaved()
|
|
50
|
+
} catch (err) {
|
|
51
|
+
setError(err instanceof Error ? err.message : 'Save failed')
|
|
52
|
+
} finally {
|
|
53
|
+
setSaving(false)
|
|
54
|
+
}
|
|
55
|
+
}, [canSave, isEdit, initial, client, name, url, teamId, disableDelete, onSaved])
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Dialog
|
|
59
|
+
header={isEdit ? `Edit "${initial?.name}"` : 'Add deploy target'}
|
|
60
|
+
id="deploy-target-form"
|
|
61
|
+
onClose={onClose}
|
|
62
|
+
width={1}
|
|
63
|
+
footer={
|
|
64
|
+
<Flex padding={3} gap={2} justify="flex-end">
|
|
65
|
+
<Button text="Cancel" mode="ghost" onClick={onClose} style={{ cursor: 'pointer' }} />
|
|
66
|
+
<Button
|
|
67
|
+
text={isEdit ? 'Save changes' : 'Add target'}
|
|
68
|
+
tone="primary"
|
|
69
|
+
icon={CheckmarkCircleIcon}
|
|
70
|
+
loading={saving}
|
|
71
|
+
disabled={!canSave || saving}
|
|
72
|
+
onClick={save}
|
|
73
|
+
style={{ cursor: 'pointer' }}
|
|
74
|
+
/>
|
|
75
|
+
</Flex>
|
|
76
|
+
}
|
|
77
|
+
>
|
|
78
|
+
<Box padding={4}>
|
|
79
|
+
<Stack space={4}>
|
|
80
|
+
|
|
81
|
+
{/* Name */}
|
|
82
|
+
<Stack space={2}>
|
|
83
|
+
<Label size={1}>Name <span style={{ color: 'var(--card-fg-color)' }}>*</span></Label>
|
|
84
|
+
<TextInput
|
|
85
|
+
value={name}
|
|
86
|
+
onChange={e => setName((e.target as HTMLInputElement).value)}
|
|
87
|
+
placeholder="Production"
|
|
88
|
+
/>
|
|
89
|
+
</Stack>
|
|
90
|
+
|
|
91
|
+
{/* Deploy hook URL */}
|
|
92
|
+
<Stack space={2}>
|
|
93
|
+
<Label size={1}>Deploy hook URL <span style={{ color: 'var(--card-fg-color)' }}>*</span></Label>
|
|
94
|
+
<TextInput
|
|
95
|
+
value={url}
|
|
96
|
+
onChange={e => setUrl((e.target as HTMLInputElement).value)}
|
|
97
|
+
placeholder="https://api.vercel.com/v1/integrations/deploy/…"
|
|
98
|
+
/>
|
|
99
|
+
{url && !urlValid && (
|
|
100
|
+
<Text size={1} style={{ color: 'var(--card-critical-fg-color, red)' }}>
|
|
101
|
+
Must be a Vercel deploy hook URL (api.vercel.com/v1/integrations/deploy/…)
|
|
102
|
+
</Text>
|
|
103
|
+
)}
|
|
104
|
+
<Text size={0} muted>
|
|
105
|
+
Vercel dashboard → Project → Settings → Git → Deploy Hooks
|
|
106
|
+
</Text>
|
|
107
|
+
</Stack>
|
|
108
|
+
|
|
109
|
+
{/* Team ID */}
|
|
110
|
+
<Stack space={2}>
|
|
111
|
+
<Label size={1}>Team ID <span style={{ opacity: 0.5, fontWeight: 'normal' }}>— optional</span></Label>
|
|
112
|
+
<TextInput
|
|
113
|
+
value={teamId}
|
|
114
|
+
onChange={e => setTeamId((e.target as HTMLInputElement).value)}
|
|
115
|
+
placeholder="team_xxxxxxxx"
|
|
116
|
+
/>
|
|
117
|
+
<Text size={0} muted>Required for team-owned Vercel projects.</Text>
|
|
118
|
+
</Stack>
|
|
119
|
+
|
|
120
|
+
{/* Disable delete */}
|
|
121
|
+
<Flex align="center" gap={3}>
|
|
122
|
+
<Switch
|
|
123
|
+
checked={disableDelete}
|
|
124
|
+
onChange={e => setDisableDelete((e.target as HTMLInputElement).checked)}
|
|
125
|
+
id="disable-delete"
|
|
126
|
+
/>
|
|
127
|
+
<Stack space={1}>
|
|
128
|
+
<Label size={1} htmlFor="disable-delete">Disable delete action</Label>
|
|
129
|
+
<Text size={0} muted>Hides the delete button for this target in the studio.</Text>
|
|
130
|
+
</Stack>
|
|
131
|
+
</Flex>
|
|
132
|
+
|
|
133
|
+
{error && (
|
|
134
|
+
<Card tone="critical" padding={3} radius={2}>
|
|
135
|
+
<Text size={1}>{error}</Text>
|
|
136
|
+
</Card>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
</Stack>
|
|
140
|
+
</Box>
|
|
141
|
+
</Dialog>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
@@ -2,26 +2,30 @@
|
|
|
2
2
|
import { useState, useEffect, useCallback } from 'react'
|
|
3
3
|
import { useClient } from 'sanity'
|
|
4
4
|
import {
|
|
5
|
-
Card, Box, Stack, Flex, Text, Heading, Spinner, Button,
|
|
5
|
+
Card, Box, Stack, Flex, Text, Heading, Spinner, Button, Dialog, useToast,
|
|
6
6
|
} from '@sanity/ui'
|
|
7
|
-
import {
|
|
7
|
+
import { TokenIcon, TrashIcon, WarningOutlineIcon, AddIcon } from '@sanity/icons'
|
|
8
8
|
import { DeployItem } from './DeployItem'
|
|
9
9
|
import { TokenSetup } from './TokenSetup'
|
|
10
|
+
import { DeployTargetForm } from './DeployTargetForm'
|
|
11
|
+
import { VERSION } from '../version'
|
|
10
12
|
import type { DeployTarget } from '../types'
|
|
11
13
|
|
|
12
|
-
const TOKEN_QUERY
|
|
14
|
+
const TOKEN_QUERY = `*[_id == "config.vercelDeploy"][0].accessToken`
|
|
13
15
|
const TARGETS_QUERY = `*[_type == "vercel_deploy"] | order(_createdAt asc)`
|
|
14
16
|
|
|
15
17
|
export function DeployTool() {
|
|
16
18
|
const client = useClient({ apiVersion: '2025-01-01' })
|
|
17
|
-
const toast
|
|
19
|
+
const toast = useToast()
|
|
18
20
|
|
|
19
|
-
const [token, setToken]
|
|
20
|
-
const [targets, setTargets]
|
|
21
|
-
const [loading, setLoading]
|
|
21
|
+
const [token, setToken] = useState<string | null>(null)
|
|
22
|
+
const [targets, setTargets] = useState<DeployTarget[]>([])
|
|
23
|
+
const [loading, setLoading] = useState(true)
|
|
22
24
|
const [showTokenSetup, setShowTokenSetup] = useState(false)
|
|
23
|
-
const [
|
|
24
|
-
const [
|
|
25
|
+
const [showCreateForm, setShowCreateForm] = useState(false)
|
|
26
|
+
const [pendingEdit, setPendingEdit] = useState<DeployTarget | null>(null)
|
|
27
|
+
const [pendingDelete, setPendingDelete] = useState<DeployTarget | null>(null)
|
|
28
|
+
const [deleting, setDeleting] = useState(false)
|
|
25
29
|
|
|
26
30
|
// ── Fetch token + targets ─────────────────────────────────────────────────
|
|
27
31
|
const load = useCallback(async () => {
|
|
@@ -42,6 +46,25 @@ export function DeployTool() {
|
|
|
42
46
|
|
|
43
47
|
useEffect(() => { load() }, [load])
|
|
44
48
|
|
|
49
|
+
// Inject responsive styles once on mount
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (document.getElementById('dvfs-styles')) return
|
|
52
|
+
const style = document.createElement('style')
|
|
53
|
+
style.id = 'dvfs-styles'
|
|
54
|
+
style.textContent = `
|
|
55
|
+
@media (max-width: 600px) {
|
|
56
|
+
.dvfs-header { flex-wrap: wrap !important; row-gap: 8px !important; }
|
|
57
|
+
.dvfs-header-actions { width: 100% !important; flex-wrap: wrap !important; justify-content: flex-start !important; }
|
|
58
|
+
.dvfs-grid { grid-template-columns: 1fr !important; }
|
|
59
|
+
.dvfs-card-flex { flex-direction: column !important; }
|
|
60
|
+
.dvfs-deploy-col { width: 100% !important; align-self: auto !important; }
|
|
61
|
+
.dvfs-deploy-col button { border-radius: 3px !important; }
|
|
62
|
+
}
|
|
63
|
+
`
|
|
64
|
+
document.head.appendChild(style)
|
|
65
|
+
return () => { document.getElementById('dvfs-styles')?.remove() }
|
|
66
|
+
}, [])
|
|
67
|
+
|
|
45
68
|
// Live subscription — update targets when documents change
|
|
46
69
|
useEffect(() => {
|
|
47
70
|
const sub = client
|
|
@@ -81,72 +104,141 @@ export function DeployTool() {
|
|
|
81
104
|
)
|
|
82
105
|
}
|
|
83
106
|
|
|
84
|
-
if (!token || showTokenSetup) {
|
|
85
|
-
return (
|
|
86
|
-
<TokenSetup
|
|
87
|
-
onSaved={() => {
|
|
88
|
-
setShowTokenSetup(false)
|
|
89
|
-
load()
|
|
90
|
-
}}
|
|
91
|
-
/>
|
|
92
|
-
)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
107
|
return (
|
|
96
108
|
<Card height="fill" tone="transparent">
|
|
97
109
|
<Box padding={5}>
|
|
98
110
|
<Stack space={5}>
|
|
111
|
+
|
|
99
112
|
{/* ── Header ──────────────────────────────────────────────── */}
|
|
100
|
-
<Flex align="center" justify="space-between">
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
<Heading size={2}>Deploy</Heading>
|
|
104
|
-
</Flex>
|
|
105
|
-
<Flex align="center" gap={3}>
|
|
106
|
-
<Badge tone="positive" mode="outline">Connected</Badge>
|
|
113
|
+
<Flex align="center" justify="space-between" className="dvfs-header">
|
|
114
|
+
<Heading size={2}>Deploy with Vercel</Heading>
|
|
115
|
+
<Flex align="center" gap={3} className="dvfs-header-actions">
|
|
107
116
|
<Button
|
|
108
|
-
text=
|
|
117
|
+
text={token ? 'Token connected' : 'Connect API token'}
|
|
109
118
|
mode="ghost"
|
|
110
119
|
icon={TokenIcon}
|
|
111
120
|
fontSize={1}
|
|
121
|
+
tone={token ? 'positive' : 'caution'}
|
|
112
122
|
onClick={() => setShowTokenSetup(true)}
|
|
123
|
+
style={{ cursor: 'pointer' }}
|
|
124
|
+
/>
|
|
125
|
+
<Button
|
|
126
|
+
text="Add target"
|
|
127
|
+
mode="ghost"
|
|
128
|
+
icon={AddIcon}
|
|
129
|
+
fontSize={1}
|
|
130
|
+
onClick={() => setShowCreateForm(true)}
|
|
131
|
+
style={{ cursor: 'pointer' }}
|
|
113
132
|
/>
|
|
114
133
|
</Flex>
|
|
115
134
|
</Flex>
|
|
116
135
|
|
|
136
|
+
{/* ── No-token upgrade banner ──────────────────────────────── */}
|
|
137
|
+
{!token && (
|
|
138
|
+
<Card padding={4} radius={2} tone="caution" shadow={1}>
|
|
139
|
+
<Flex align="center" justify="space-between" gap={4}>
|
|
140
|
+
<Stack space={2}>
|
|
141
|
+
<Text size={1} weight="semibold">Deploy status is not connected</Text>
|
|
142
|
+
<Text size={1} muted>
|
|
143
|
+
You can trigger deploys now. Connect a Vercel API token to also see
|
|
144
|
+
deployment status, build logs, history, and commit metadata.
|
|
145
|
+
</Text>
|
|
146
|
+
</Stack>
|
|
147
|
+
<Button
|
|
148
|
+
text="Connect"
|
|
149
|
+
tone="caution"
|
|
150
|
+
fontSize={1}
|
|
151
|
+
onClick={() => setShowTokenSetup(true)}
|
|
152
|
+
style={{ cursor: 'pointer', flexShrink: 0 }}
|
|
153
|
+
/>
|
|
154
|
+
</Flex>
|
|
155
|
+
</Card>
|
|
156
|
+
)}
|
|
157
|
+
|
|
117
158
|
{/* ── No targets ──────────────────────────────────────────── */}
|
|
118
159
|
{targets.length === 0 && (
|
|
119
160
|
<Card padding={5} radius={2} tone="transparent" shadow={1}>
|
|
120
|
-
<Stack space={
|
|
161
|
+
<Stack space={4} style={{ textAlign: 'center' }}>
|
|
121
162
|
<Text size={2} weight="semibold">No deploy targets configured</Text>
|
|
122
163
|
<Text size={1} muted>
|
|
123
|
-
|
|
124
|
-
|
|
164
|
+
Add a deploy target using the button above, or create a{' '}
|
|
165
|
+
<code>vercel_deploy</code> document directly in the dataset.
|
|
125
166
|
</Text>
|
|
167
|
+
<Flex justify="center">
|
|
168
|
+
<Button
|
|
169
|
+
text="Add deploy target"
|
|
170
|
+
tone="primary"
|
|
171
|
+
icon={AddIcon}
|
|
172
|
+
onClick={() => setShowCreateForm(true)}
|
|
173
|
+
style={{ cursor: 'pointer' }}
|
|
174
|
+
/>
|
|
175
|
+
</Flex>
|
|
126
176
|
</Stack>
|
|
127
177
|
</Card>
|
|
128
178
|
)}
|
|
129
179
|
|
|
130
180
|
{/* ── Deploy targets — responsive 2-col grid ──────────────── */}
|
|
131
181
|
{targets.length > 0 && (
|
|
132
|
-
<div style={{
|
|
182
|
+
<div className="dvfs-grid" style={{
|
|
133
183
|
display: 'grid',
|
|
134
184
|
gridTemplateColumns: 'repeat(auto-fill, minmax(540px, 1fr))',
|
|
135
185
|
gap: '16px',
|
|
186
|
+
alignItems: 'start',
|
|
136
187
|
}}>
|
|
137
188
|
{targets.map(target => (
|
|
138
189
|
<DeployItem
|
|
139
190
|
key={target._id}
|
|
140
191
|
target={target}
|
|
141
|
-
token={token}
|
|
192
|
+
token={token ?? ''}
|
|
142
193
|
onDelete={setPendingDelete}
|
|
194
|
+
onEdit={setPendingEdit}
|
|
143
195
|
/>
|
|
144
196
|
))}
|
|
145
197
|
</div>
|
|
146
198
|
)}
|
|
199
|
+
|
|
147
200
|
</Stack>
|
|
148
201
|
</Box>
|
|
149
202
|
|
|
203
|
+
{/* ── Version watermark ──────────────────────────────────────────── */}
|
|
204
|
+
<Box
|
|
205
|
+
style={{
|
|
206
|
+
position: 'fixed',
|
|
207
|
+
bottom: 12,
|
|
208
|
+
right: 16,
|
|
209
|
+
opacity: 0.25,
|
|
210
|
+
pointerEvents: 'none',
|
|
211
|
+
userSelect: 'none',
|
|
212
|
+
}}
|
|
213
|
+
>
|
|
214
|
+
<Text size={0} muted>v{VERSION}</Text>
|
|
215
|
+
</Box>
|
|
216
|
+
|
|
217
|
+
{/* ── Token setup dialog ──────────────────────────────────────────── */}
|
|
218
|
+
{showTokenSetup && (
|
|
219
|
+
<TokenSetup
|
|
220
|
+
onSaved={() => { setShowTokenSetup(false); load() }}
|
|
221
|
+
onCancel={token ? () => setShowTokenSetup(false) : undefined}
|
|
222
|
+
/>
|
|
223
|
+
)}
|
|
224
|
+
|
|
225
|
+
{/* ── Create form ─────────────────────────────────────────────────── */}
|
|
226
|
+
{showCreateForm && (
|
|
227
|
+
<DeployTargetForm
|
|
228
|
+
onSaved={() => { setShowCreateForm(false); toast.push({ status: 'success', title: 'Deploy target added' }) }}
|
|
229
|
+
onClose={() => setShowCreateForm(false)}
|
|
230
|
+
/>
|
|
231
|
+
)}
|
|
232
|
+
|
|
233
|
+
{/* ── Edit form ───────────────────────────────────────────────────── */}
|
|
234
|
+
{pendingEdit && (
|
|
235
|
+
<DeployTargetForm
|
|
236
|
+
initial={pendingEdit}
|
|
237
|
+
onSaved={() => { setPendingEdit(null); toast.push({ status: 'success', title: 'Deploy target updated' }) }}
|
|
238
|
+
onClose={() => setPendingEdit(null)}
|
|
239
|
+
/>
|
|
240
|
+
)}
|
|
241
|
+
|
|
150
242
|
{/* ── Delete confirmation ─────────────────────────────────────────── */}
|
|
151
243
|
{pendingDelete && (
|
|
152
244
|
<Dialog
|
|
@@ -160,6 +252,7 @@ export function DeployTool() {
|
|
|
160
252
|
text="Cancel"
|
|
161
253
|
mode="ghost"
|
|
162
254
|
onClick={() => setPendingDelete(null)}
|
|
255
|
+
style={{ cursor: 'pointer' }}
|
|
163
256
|
/>
|
|
164
257
|
<Button
|
|
165
258
|
text="Delete"
|
|
@@ -168,6 +261,7 @@ export function DeployTool() {
|
|
|
168
261
|
loading={deleting}
|
|
169
262
|
disabled={deleting}
|
|
170
263
|
onClick={confirmDelete}
|
|
264
|
+
style={{ cursor: 'pointer' }}
|
|
171
265
|
/>
|
|
172
266
|
</Flex>
|
|
173
267
|
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
// Vercel API token
|
|
1
|
+
// Vercel API token form — rendered inside a Dialog by DeployTool
|
|
2
2
|
import { useState, useCallback } from 'react'
|
|
3
3
|
import { useClient } from 'sanity'
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
} from '@sanity/ui'
|
|
7
|
-
import { TokenIcon, CheckmarkCircleIcon } from '@sanity/icons'
|
|
4
|
+
import { Stack, Text, TextInput, Button, Card, Dialog, Flex } from '@sanity/ui'
|
|
5
|
+
import { CheckmarkCircleIcon } from '@sanity/icons'
|
|
8
6
|
|
|
9
7
|
interface TokenSetupProps {
|
|
10
8
|
/** Called after the token is successfully saved */
|
|
11
9
|
onSaved: () => void
|
|
10
|
+
/** Called when the user dismisses — only available when a token already exists */
|
|
11
|
+
onCancel?: () => void
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
const TOKEN_DOC_ID = 'config.vercelDeploy'
|
|
15
15
|
|
|
16
|
-
export function TokenSetup({ onSaved }: TokenSetupProps) {
|
|
16
|
+
export function TokenSetup({ onSaved, onCancel }: TokenSetupProps) {
|
|
17
17
|
const client = useClient({ apiVersion: '2025-01-01' })
|
|
18
18
|
const [token, setToken] = useState('')
|
|
19
19
|
const [saving, setSaving] = useState(false)
|
|
@@ -38,55 +38,58 @@ export function TokenSetup({ onSaved }: TokenSetupProps) {
|
|
|
38
38
|
}, [client, token, onSaved])
|
|
39
39
|
|
|
40
40
|
return (
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
<Dialog
|
|
42
|
+
header="Connect Vercel API token"
|
|
43
|
+
id="token-setup"
|
|
44
|
+
onClose={onCancel}
|
|
45
|
+
width={1}
|
|
46
|
+
footer={
|
|
47
|
+
<Flex padding={3} gap={2} justify="flex-end">
|
|
48
|
+
{onCancel && (
|
|
49
|
+
<Button text="Cancel" mode="ghost" onClick={onCancel} style={{ cursor: 'pointer' }} />
|
|
50
|
+
)}
|
|
51
|
+
<Button
|
|
52
|
+
text="Save and connect"
|
|
53
|
+
tone="primary"
|
|
54
|
+
icon={CheckmarkCircleIcon}
|
|
55
|
+
loading={saving}
|
|
56
|
+
disabled={!token.trim() || saving}
|
|
57
|
+
onClick={save}
|
|
58
|
+
style={{ cursor: 'pointer' }}
|
|
59
|
+
/>
|
|
60
|
+
</Flex>
|
|
61
|
+
}
|
|
62
|
+
>
|
|
63
|
+
<Stack space={4} padding={4}>
|
|
64
|
+
<Stack space={3}>
|
|
65
|
+
<Text size={1} muted>
|
|
66
|
+
A Vercel API token lets this tool read deployment status, history, build logs,
|
|
67
|
+
and branch metadata. Without it you can still trigger deploys — you just won't
|
|
68
|
+
see any feedback.
|
|
69
|
+
</Text>
|
|
70
|
+
<Text size={1} muted>
|
|
71
|
+
Create one at <strong>vercel.com → Settings → Tokens</strong> with{' '}
|
|
72
|
+
<strong>Full Account</strong> scope. The token is stored in your Sanity dataset
|
|
73
|
+
and shared across all authenticated studio users.
|
|
74
|
+
</Text>
|
|
75
|
+
</Stack>
|
|
49
76
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
Choose <strong>Full Account</strong> scope.
|
|
60
|
-
</Text>
|
|
61
|
-
</Stack>
|
|
77
|
+
<Stack space={2}>
|
|
78
|
+
<Text size={1} weight="semibold">Vercel API Token</Text>
|
|
79
|
+
<TextInput
|
|
80
|
+
value={token}
|
|
81
|
+
onChange={e => setToken((e.target as HTMLInputElement).value)}
|
|
82
|
+
placeholder="xxxxxxxxxxxxxxxxxxxxxxxx"
|
|
83
|
+
type="password"
|
|
84
|
+
/>
|
|
85
|
+
</Stack>
|
|
62
86
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
/>
|
|
71
|
-
</Stack>
|
|
72
|
-
|
|
73
|
-
{error && (
|
|
74
|
-
<Card tone="critical" padding={3} radius={2}>
|
|
75
|
-
<Text size={1}>{error}</Text>
|
|
76
|
-
</Card>
|
|
77
|
-
)}
|
|
78
|
-
|
|
79
|
-
<Button
|
|
80
|
-
text="Save and connect"
|
|
81
|
-
tone="primary"
|
|
82
|
-
icon={CheckmarkCircleIcon}
|
|
83
|
-
loading={saving}
|
|
84
|
-
disabled={!token.trim() || saving}
|
|
85
|
-
onClick={save}
|
|
86
|
-
/>
|
|
87
|
-
</Stack>
|
|
88
|
-
</Card>
|
|
89
|
-
</Flex>
|
|
90
|
-
</Card>
|
|
87
|
+
{error && (
|
|
88
|
+
<Card tone="critical" padding={3} radius={2}>
|
|
89
|
+
<Text size={1}>{error}</Text>
|
|
90
|
+
</Card>
|
|
91
|
+
)}
|
|
92
|
+
</Stack>
|
|
93
|
+
</Dialog>
|
|
91
94
|
)
|
|
92
95
|
}
|
package/src/version.ts
ADDED