@liiift-studio/deploy-vercel-from-sanity 0.1.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 +139 -0
- package/dist/index.d.mts +75 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +768 -0
- package/dist/index.mjs +791 -0
- package/package.json +42 -0
- package/src/components/DeployHistory.tsx +157 -0
- package/src/components/DeployItem.tsx +298 -0
- package/src/components/DeployTool.tsx +184 -0
- package/src/components/StatusBadge.tsx +23 -0
- package/src/components/TokenSetup.tsx +92 -0
- package/src/index.ts +42 -0
- package/src/lib/api.ts +59 -0
- package/src/lib/helpers.ts +86 -0
- package/src/schema/vercelDeploy.ts +49 -0
- package/src/types.ts +63 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { definePlugin } from "sanity";
|
|
3
|
+
import { RocketIcon as RocketIcon4 } from "@sanity/icons";
|
|
4
|
+
|
|
5
|
+
// src/components/DeployTool.tsx
|
|
6
|
+
import { useState as useState4, useEffect as useEffect3, useCallback as useCallback4 } from "react";
|
|
7
|
+
import { useClient as useClient2 } from "sanity";
|
|
8
|
+
import {
|
|
9
|
+
Card as Card4,
|
|
10
|
+
Box as Box4,
|
|
11
|
+
Stack as Stack4,
|
|
12
|
+
Flex as Flex5,
|
|
13
|
+
Text as Text4,
|
|
14
|
+
Heading as Heading2,
|
|
15
|
+
Spinner as Spinner4,
|
|
16
|
+
Button as Button4,
|
|
17
|
+
Dialog as Dialog2,
|
|
18
|
+
useToast
|
|
19
|
+
} from "@sanity/ui";
|
|
20
|
+
import { RocketIcon as RocketIcon2, KeyIcon as KeyIcon2, TrashIcon as TrashIcon2, WarningOutlineIcon } from "@sanity/icons";
|
|
21
|
+
|
|
22
|
+
// src/components/DeployItem.tsx
|
|
23
|
+
import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2, useRef } from "react";
|
|
24
|
+
import {
|
|
25
|
+
Card as Card2,
|
|
26
|
+
Box as Box2,
|
|
27
|
+
Stack as Stack2,
|
|
28
|
+
Flex as Flex3,
|
|
29
|
+
Text as Text2,
|
|
30
|
+
Button as Button2,
|
|
31
|
+
Tooltip,
|
|
32
|
+
Badge as Badge3,
|
|
33
|
+
Spinner as Spinner3,
|
|
34
|
+
MenuButton,
|
|
35
|
+
Menu,
|
|
36
|
+
MenuItem
|
|
37
|
+
} from "@sanity/ui";
|
|
38
|
+
import {
|
|
39
|
+
RocketIcon,
|
|
40
|
+
ClockIcon,
|
|
41
|
+
HistoryIcon,
|
|
42
|
+
TrashIcon,
|
|
43
|
+
EllipsisVerticalIcon,
|
|
44
|
+
LaunchIcon as LaunchIcon2
|
|
45
|
+
} from "@sanity/icons";
|
|
46
|
+
|
|
47
|
+
// src/lib/api.ts
|
|
48
|
+
var BASE = "https://api.vercel.com";
|
|
49
|
+
async function vercelFetch(path, token, init) {
|
|
50
|
+
const res = await fetch(`${BASE}${path}`, {
|
|
51
|
+
...init,
|
|
52
|
+
headers: {
|
|
53
|
+
Authorization: `Bearer ${token}`,
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
...init?.headers
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
if (!res.ok) {
|
|
59
|
+
const text = await res.text().catch(() => res.statusText);
|
|
60
|
+
throw new Error(`Vercel API ${res.status}: ${text}`);
|
|
61
|
+
}
|
|
62
|
+
return res.json();
|
|
63
|
+
}
|
|
64
|
+
async function listDeployments(opts) {
|
|
65
|
+
const params = new URLSearchParams({
|
|
66
|
+
projectId: opts.projectId,
|
|
67
|
+
"meta-deployHookId": opts.hookId,
|
|
68
|
+
limit: String(opts.limit ?? 10)
|
|
69
|
+
});
|
|
70
|
+
if (opts.teamId) params.set("teamId", opts.teamId);
|
|
71
|
+
const data = await vercelFetch(
|
|
72
|
+
`/v6/deployments?${params}`,
|
|
73
|
+
opts.token
|
|
74
|
+
);
|
|
75
|
+
return data.deployments ?? [];
|
|
76
|
+
}
|
|
77
|
+
async function cancelDeployment(opts) {
|
|
78
|
+
const params = opts.teamId ? `?teamId=${opts.teamId}` : "";
|
|
79
|
+
await vercelFetch(`/v12/deployments/${opts.deploymentId}/cancel${params}`, opts.token, {
|
|
80
|
+
method: "PATCH"
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
async function triggerDeploy(hookUrl) {
|
|
84
|
+
const res = await fetch(hookUrl, { method: "POST" });
|
|
85
|
+
if (!res.ok) throw new Error(`Deploy hook returned ${res.status}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/lib/helpers.ts
|
|
89
|
+
function parseHookUrl(url) {
|
|
90
|
+
try {
|
|
91
|
+
const path = new URL(url).pathname;
|
|
92
|
+
const parts = path.split("/").filter(Boolean);
|
|
93
|
+
return {
|
|
94
|
+
projectId: parts[3] ?? "",
|
|
95
|
+
hookId: parts[4] ?? ""
|
|
96
|
+
};
|
|
97
|
+
} catch {
|
|
98
|
+
return { projectId: "", hookId: "" };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
var ACTIVE_STATES = /* @__PURE__ */ new Set([
|
|
102
|
+
"QUEUED",
|
|
103
|
+
"INITIALIZING",
|
|
104
|
+
"BUILDING"
|
|
105
|
+
]);
|
|
106
|
+
function isActiveState(state) {
|
|
107
|
+
return !!state && ACTIVE_STATES.has(state);
|
|
108
|
+
}
|
|
109
|
+
function formatDuration(seconds) {
|
|
110
|
+
if (seconds < 60) return `${seconds}s`;
|
|
111
|
+
const m = Math.floor(seconds / 60);
|
|
112
|
+
const s = seconds % 60;
|
|
113
|
+
return `${m}m ${s}s`;
|
|
114
|
+
}
|
|
115
|
+
function timeAgo(ms) {
|
|
116
|
+
const diff = Math.floor((Date.now() - ms) / 1e3);
|
|
117
|
+
if (diff < 60) return `${diff}s ago`;
|
|
118
|
+
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
|
119
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
|
120
|
+
return `${Math.floor(diff / 86400)}d ago`;
|
|
121
|
+
}
|
|
122
|
+
function safeHref(url) {
|
|
123
|
+
if (!url) return void 0;
|
|
124
|
+
try {
|
|
125
|
+
const parsed = new URL(url);
|
|
126
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return void 0;
|
|
127
|
+
return url;
|
|
128
|
+
} catch {
|
|
129
|
+
return void 0;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function shortSha(sha) {
|
|
133
|
+
return sha ? sha.slice(0, 7) : "";
|
|
134
|
+
}
|
|
135
|
+
function stateLabel(state) {
|
|
136
|
+
switch (state) {
|
|
137
|
+
case "READY":
|
|
138
|
+
return { label: "Ready", tone: "positive" };
|
|
139
|
+
case "BUILDING":
|
|
140
|
+
return { label: "Building", tone: "caution" };
|
|
141
|
+
case "QUEUED":
|
|
142
|
+
return { label: "Queued", tone: "caution" };
|
|
143
|
+
case "INITIALIZING":
|
|
144
|
+
return { label: "Initializing", tone: "caution" };
|
|
145
|
+
case "ERROR":
|
|
146
|
+
return { label: "Error", tone: "critical" };
|
|
147
|
+
case "CANCELED":
|
|
148
|
+
return { label: "Canceled", tone: "default" };
|
|
149
|
+
case "LOADING":
|
|
150
|
+
return { label: "Loading\u2026", tone: "default" };
|
|
151
|
+
default:
|
|
152
|
+
return { label: "Unknown", tone: "default" };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/components/StatusBadge.tsx
|
|
157
|
+
import { Badge, Spinner, Flex } from "@sanity/ui";
|
|
158
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
159
|
+
function StatusBadge({ state, showSpinner }) {
|
|
160
|
+
const { label, tone } = stateLabel(state);
|
|
161
|
+
const spinning = showSpinner && (state === "QUEUED" || state === "INITIALIZING" || state === "BUILDING");
|
|
162
|
+
return /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
|
|
163
|
+
spinning && /* @__PURE__ */ jsx(Spinner, { muted: true }),
|
|
164
|
+
/* @__PURE__ */ jsx(Badge, { tone, mode: "outline", children: label })
|
|
165
|
+
] });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/components/DeployHistory.tsx
|
|
169
|
+
import { useEffect, useState, useCallback } from "react";
|
|
170
|
+
import {
|
|
171
|
+
Dialog,
|
|
172
|
+
Card,
|
|
173
|
+
Box,
|
|
174
|
+
Stack,
|
|
175
|
+
Flex as Flex2,
|
|
176
|
+
Text,
|
|
177
|
+
Badge as Badge2,
|
|
178
|
+
Spinner as Spinner2,
|
|
179
|
+
Button
|
|
180
|
+
} from "@sanity/ui";
|
|
181
|
+
import { LaunchIcon, CloseIcon } from "@sanity/icons";
|
|
182
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
183
|
+
function DeployHistory({ target, token, onClose }) {
|
|
184
|
+
const [deployments, setDeployments] = useState([]);
|
|
185
|
+
const [loading, setLoading] = useState(true);
|
|
186
|
+
const [error, setError] = useState(null);
|
|
187
|
+
const { projectId, hookId } = parseHookUrl(target.url);
|
|
188
|
+
const load = useCallback(async () => {
|
|
189
|
+
setLoading(true);
|
|
190
|
+
setError(null);
|
|
191
|
+
try {
|
|
192
|
+
const data = await listDeployments({ projectId, hookId, token, teamId: target.teamId, limit: 10 });
|
|
193
|
+
setDeployments(data);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
setError(err instanceof Error ? err.message : "Failed to load history");
|
|
196
|
+
} finally {
|
|
197
|
+
setLoading(false);
|
|
198
|
+
}
|
|
199
|
+
}, [projectId, hookId, token, target.teamId]);
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
load();
|
|
202
|
+
}, [load]);
|
|
203
|
+
return /* @__PURE__ */ jsx2(
|
|
204
|
+
Dialog,
|
|
205
|
+
{
|
|
206
|
+
header: `${target.name} \u2014 Deployment History`,
|
|
207
|
+
id: "deploy-history",
|
|
208
|
+
onClose,
|
|
209
|
+
width: 2,
|
|
210
|
+
footer: /* @__PURE__ */ jsx2(Box, { padding: 3, children: /* @__PURE__ */ jsx2(Button, { text: "Close", icon: CloseIcon, mode: "ghost", onClick: onClose }) }),
|
|
211
|
+
children: /* @__PURE__ */ jsxs2(Box, { padding: 4, children: [
|
|
212
|
+
loading && /* @__PURE__ */ jsx2(Flex2, { justify: "center", padding: 6, children: /* @__PURE__ */ jsx2(Spinner2, { muted: true }) }),
|
|
213
|
+
error && /* @__PURE__ */ jsx2(Card, { tone: "critical", padding: 4, radius: 2, children: /* @__PURE__ */ jsx2(Text, { size: 1, children: error }) }),
|
|
214
|
+
!loading && !error && deployments.length === 0 && /* @__PURE__ */ jsx2(Card, { tone: "transparent", padding: 4, children: /* @__PURE__ */ jsx2(Text, { size: 1, muted: true, align: "center", children: "No deployments found for this hook." }) }),
|
|
215
|
+
!loading && deployments.length > 0 && /* @__PURE__ */ jsxs2(Stack, { space: 2, children: [
|
|
216
|
+
/* @__PURE__ */ jsx2(Card, { padding: 3, radius: 2, tone: "transparent", children: /* @__PURE__ */ jsxs2(Flex2, { gap: 3, children: [
|
|
217
|
+
/* @__PURE__ */ jsx2(Box, { flex: 2, children: /* @__PURE__ */ jsx2(Text, { size: 0, weight: "semibold", muted: true, children: "Preview URL" }) }),
|
|
218
|
+
/* @__PURE__ */ jsx2(Box, { flex: 1, children: /* @__PURE__ */ jsx2(Text, { size: 0, weight: "semibold", muted: true, children: "Status" }) }),
|
|
219
|
+
/* @__PURE__ */ jsx2(Box, { flex: 2, children: /* @__PURE__ */ jsx2(Text, { size: 0, weight: "semibold", muted: true, children: "Branch \xB7 Commit" }) }),
|
|
220
|
+
/* @__PURE__ */ jsx2(Box, { flex: 1, children: /* @__PURE__ */ jsx2(Text, { size: 0, weight: "semibold", muted: true, children: "Deployed" }) }),
|
|
221
|
+
/* @__PURE__ */ jsx2(Box, { style: { width: 64 }, children: /* @__PURE__ */ jsx2(Text, { size: 0, weight: "semibold", muted: true, children: "Logs" }) })
|
|
222
|
+
] }) }),
|
|
223
|
+
deployments.map((d) => {
|
|
224
|
+
const { label, tone } = stateLabel(d.state);
|
|
225
|
+
const branch = d.meta?.githubCommitRef ?? "\u2014";
|
|
226
|
+
const sha = shortSha(d.meta?.githubCommitSha);
|
|
227
|
+
const message = d.meta?.githubCommitMessage?.split("\n")[0] ?? "";
|
|
228
|
+
return /* @__PURE__ */ jsx2(Card, { padding: 3, radius: 2, shadow: 1, tone: "default", children: /* @__PURE__ */ jsxs2(Flex2, { gap: 3, align: "center", children: [
|
|
229
|
+
/* @__PURE__ */ jsx2(Box, { flex: 2, style: { overflow: "hidden" }, children: d.url ? /* @__PURE__ */ jsx2(
|
|
230
|
+
"a",
|
|
231
|
+
{
|
|
232
|
+
href: `https://${d.url}`,
|
|
233
|
+
target: "_blank",
|
|
234
|
+
rel: "noreferrer",
|
|
235
|
+
style: { color: "inherit" },
|
|
236
|
+
children: /* @__PURE__ */ jsx2(Text, { size: 1, style: { textDecoration: "underline" }, children: d.url.length > 36 ? `${d.url.slice(0, 36)}\u2026` : d.url })
|
|
237
|
+
}
|
|
238
|
+
) : /* @__PURE__ */ jsx2(Text, { size: 1, muted: true, children: "\u2014" }) }),
|
|
239
|
+
/* @__PURE__ */ jsx2(Box, { flex: 1, children: /* @__PURE__ */ jsx2(Badge2, { tone, mode: "outline", children: label }) }),
|
|
240
|
+
/* @__PURE__ */ jsx2(Box, { flex: 2, style: { overflow: "hidden" }, children: /* @__PURE__ */ jsxs2(Stack, { space: 1, children: [
|
|
241
|
+
/* @__PURE__ */ jsx2(Text, { size: 1, children: branch }),
|
|
242
|
+
sha && /* @__PURE__ */ jsxs2(Text, { size: 0, muted: true, children: [
|
|
243
|
+
sha,
|
|
244
|
+
message ? ` \xB7 ${message.slice(0, 40)}${message.length > 40 ? "\u2026" : ""}` : ""
|
|
245
|
+
] })
|
|
246
|
+
] }) }),
|
|
247
|
+
/* @__PURE__ */ jsx2(Box, { flex: 1, children: /* @__PURE__ */ jsx2(Text, { size: 1, muted: true, children: timeAgo(d.created) }) }),
|
|
248
|
+
/* @__PURE__ */ jsx2(Box, { style: { width: 64 }, children: safeHref(d.inspectorUrl) ? /* @__PURE__ */ jsx2("a", { href: safeHref(d.inspectorUrl), target: "_blank", rel: "noreferrer", children: /* @__PURE__ */ jsx2(
|
|
249
|
+
Button,
|
|
250
|
+
{
|
|
251
|
+
text: "Logs",
|
|
252
|
+
mode: "ghost",
|
|
253
|
+
tone: "default",
|
|
254
|
+
icon: LaunchIcon,
|
|
255
|
+
style: { fontSize: "12px" }
|
|
256
|
+
}
|
|
257
|
+
) }) : /* @__PURE__ */ jsx2(Text, { size: 1, muted: true, children: "\u2014" }) })
|
|
258
|
+
] }) }, d.uid);
|
|
259
|
+
})
|
|
260
|
+
] })
|
|
261
|
+
] })
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/components/DeployItem.tsx
|
|
267
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
268
|
+
var POLL_INTERVAL_MS = 5e3;
|
|
269
|
+
function DeployItem({ target, token, onDelete }) {
|
|
270
|
+
const { projectId, hookId } = parseHookUrl(target.url);
|
|
271
|
+
const [deployments, setDeployments] = useState2([]);
|
|
272
|
+
const [loadingInitial, setLoadingInitial] = useState2(true);
|
|
273
|
+
const [triggering, setTriggering] = useState2(false);
|
|
274
|
+
const [canceling, setCanceling] = useState2(false);
|
|
275
|
+
const [deployError, setDeployError] = useState2(null);
|
|
276
|
+
const [showHistory, setShowHistory] = useState2(false);
|
|
277
|
+
const [elapsed, setElapsed] = useState2(0);
|
|
278
|
+
const latest = deployments[0];
|
|
279
|
+
const isActive = triggering || isActiveState(latest?.state);
|
|
280
|
+
const fetchDeployments = useCallback2(async () => {
|
|
281
|
+
if (!projectId || !hookId || !token) return;
|
|
282
|
+
try {
|
|
283
|
+
const data = await listDeployments({ projectId, hookId, token, teamId: target.teamId });
|
|
284
|
+
setDeployments(data);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
console.error("deploy-vercel-from-sanity: fetch error", err);
|
|
287
|
+
}
|
|
288
|
+
}, [projectId, hookId, token, target.teamId]);
|
|
289
|
+
useEffect2(() => {
|
|
290
|
+
fetchDeployments().finally(() => setLoadingInitial(false));
|
|
291
|
+
}, [fetchDeployments]);
|
|
292
|
+
useEffect2(() => {
|
|
293
|
+
if (!isActive) return;
|
|
294
|
+
const id = setInterval(fetchDeployments, POLL_INTERVAL_MS);
|
|
295
|
+
return () => clearInterval(id);
|
|
296
|
+
}, [isActive, fetchDeployments]);
|
|
297
|
+
useEffect2(() => {
|
|
298
|
+
if (triggering && latest && latest.state !== void 0) {
|
|
299
|
+
setTriggering(false);
|
|
300
|
+
}
|
|
301
|
+
}, [triggering, latest]);
|
|
302
|
+
const timerRef = useRef(null);
|
|
303
|
+
useEffect2(() => {
|
|
304
|
+
if (isActive) {
|
|
305
|
+
const start = latest?.created ?? Date.now();
|
|
306
|
+
setElapsed(Math.floor((Date.now() - start) / 1e3));
|
|
307
|
+
timerRef.current = setInterval(() => {
|
|
308
|
+
setElapsed(Math.floor((Date.now() - start) / 1e3));
|
|
309
|
+
}, 1e3);
|
|
310
|
+
} else {
|
|
311
|
+
setElapsed(0);
|
|
312
|
+
if (timerRef.current) clearInterval(timerRef.current);
|
|
313
|
+
}
|
|
314
|
+
return () => {
|
|
315
|
+
if (timerRef.current) clearInterval(timerRef.current);
|
|
316
|
+
};
|
|
317
|
+
}, [isActive, latest?.created]);
|
|
318
|
+
const deploy = useCallback2(async () => {
|
|
319
|
+
setDeployError(null);
|
|
320
|
+
setTriggering(true);
|
|
321
|
+
try {
|
|
322
|
+
await triggerDeploy(target.url);
|
|
323
|
+
setTimeout(fetchDeployments, 2e3);
|
|
324
|
+
} catch (err) {
|
|
325
|
+
setTriggering(false);
|
|
326
|
+
setDeployError(err instanceof Error ? err.message : "Deploy failed");
|
|
327
|
+
}
|
|
328
|
+
}, [target.url, fetchDeployments]);
|
|
329
|
+
const cancel = useCallback2(async () => {
|
|
330
|
+
if (!latest?.uid) return;
|
|
331
|
+
setCanceling(true);
|
|
332
|
+
try {
|
|
333
|
+
await cancelDeployment({ deploymentId: latest.uid, token, teamId: target.teamId });
|
|
334
|
+
await fetchDeployments();
|
|
335
|
+
} catch (err) {
|
|
336
|
+
console.error("deploy-vercel-from-sanity: cancel error", err);
|
|
337
|
+
} finally {
|
|
338
|
+
setCanceling(false);
|
|
339
|
+
}
|
|
340
|
+
}, [latest?.uid, token, target.teamId, fetchDeployments]);
|
|
341
|
+
const branch = latest?.meta?.githubCommitRef;
|
|
342
|
+
const commitMsg = latest?.meta?.githubCommitMessage?.split("\n")[0];
|
|
343
|
+
const sha = shortSha(latest?.meta?.githubCommitSha);
|
|
344
|
+
const creator = latest?.creator?.username;
|
|
345
|
+
const deployedAt = latest?.created ? timeAgo(latest.created) : null;
|
|
346
|
+
return /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
347
|
+
/* @__PURE__ */ jsx3(Card2, { padding: 4, radius: 2, shadow: 1, tone: "default", children: /* @__PURE__ */ jsxs3(Stack2, { space: 4, children: [
|
|
348
|
+
/* @__PURE__ */ jsxs3(Flex3, { align: "flex-start", justify: "space-between", gap: 3, children: [
|
|
349
|
+
/* @__PURE__ */ jsxs3(Stack2, { space: 2, flex: 1, children: [
|
|
350
|
+
/* @__PURE__ */ jsx3(Text2, { size: 2, weight: "semibold", children: target.name }),
|
|
351
|
+
/* @__PURE__ */ jsxs3(Flex3, { gap: 2, wrap: "wrap", children: [
|
|
352
|
+
/* @__PURE__ */ jsx3(Text2, { size: 0, muted: true, children: projectId ? `${projectId.slice(0, 18)}\u2026` : "\u2014" }),
|
|
353
|
+
/* @__PURE__ */ jsx3(Text2, { size: 0, muted: true, children: "\xB7" }),
|
|
354
|
+
/* @__PURE__ */ jsxs3(Text2, { size: 0, muted: true, children: [
|
|
355
|
+
"Hook: ",
|
|
356
|
+
hookId || "\u2014"
|
|
357
|
+
] }),
|
|
358
|
+
target.teamId && /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
359
|
+
/* @__PURE__ */ jsx3(Text2, { size: 0, muted: true, children: "\xB7" }),
|
|
360
|
+
/* @__PURE__ */ jsxs3(Text2, { size: 0, muted: true, children: [
|
|
361
|
+
"Team: ",
|
|
362
|
+
target.teamId
|
|
363
|
+
] })
|
|
364
|
+
] })
|
|
365
|
+
] })
|
|
366
|
+
] }),
|
|
367
|
+
/* @__PURE__ */ jsx3(
|
|
368
|
+
MenuButton,
|
|
369
|
+
{
|
|
370
|
+
button: /* @__PURE__ */ jsx3(Button2, { mode: "ghost", icon: EllipsisVerticalIcon }),
|
|
371
|
+
id: `menu-${target._id}`,
|
|
372
|
+
menu: /* @__PURE__ */ jsxs3(Menu, { children: [
|
|
373
|
+
/* @__PURE__ */ jsx3(
|
|
374
|
+
MenuItem,
|
|
375
|
+
{
|
|
376
|
+
text: "History",
|
|
377
|
+
icon: HistoryIcon,
|
|
378
|
+
onClick: () => setShowHistory(true)
|
|
379
|
+
}
|
|
380
|
+
),
|
|
381
|
+
safeHref(latest?.inspectorUrl) && /* @__PURE__ */ jsx3(
|
|
382
|
+
MenuItem,
|
|
383
|
+
{
|
|
384
|
+
text: "Build logs",
|
|
385
|
+
icon: LaunchIcon2,
|
|
386
|
+
as: "a",
|
|
387
|
+
href: safeHref(latest?.inspectorUrl),
|
|
388
|
+
target: "_blank",
|
|
389
|
+
rel: "noreferrer"
|
|
390
|
+
}
|
|
391
|
+
),
|
|
392
|
+
!target.disableDeleteAction && /* @__PURE__ */ jsx3(
|
|
393
|
+
MenuItem,
|
|
394
|
+
{
|
|
395
|
+
text: "Delete",
|
|
396
|
+
icon: TrashIcon,
|
|
397
|
+
tone: "critical",
|
|
398
|
+
onClick: () => onDelete(target)
|
|
399
|
+
}
|
|
400
|
+
)
|
|
401
|
+
] }),
|
|
402
|
+
popover: { placement: "bottom-end" }
|
|
403
|
+
}
|
|
404
|
+
)
|
|
405
|
+
] }),
|
|
406
|
+
loadingInitial ? /* @__PURE__ */ jsxs3(Flex3, { align: "center", gap: 2, children: [
|
|
407
|
+
/* @__PURE__ */ jsx3(Spinner3, { muted: true }),
|
|
408
|
+
/* @__PURE__ */ jsx3(Text2, { size: 1, muted: true, children: "Loading\u2026" })
|
|
409
|
+
] }) : /* @__PURE__ */ jsxs3(Stack2, { space: 3, children: [
|
|
410
|
+
/* @__PURE__ */ jsxs3(Flex3, { align: "center", gap: 3, wrap: "wrap", children: [
|
|
411
|
+
triggering ? /* @__PURE__ */ jsxs3(Flex3, { align: "center", gap: 2, children: [
|
|
412
|
+
/* @__PURE__ */ jsx3(Spinner3, { muted: true }),
|
|
413
|
+
/* @__PURE__ */ jsx3(Badge3, { tone: "caution", mode: "outline", children: "Triggering\u2026" })
|
|
414
|
+
] }) : /* @__PURE__ */ jsx3(StatusBadge, { state: latest?.state, showSpinner: true }),
|
|
415
|
+
isActive && elapsed > 0 && /* @__PURE__ */ jsxs3(Flex3, { align: "center", gap: 1, children: [
|
|
416
|
+
/* @__PURE__ */ jsx3(ClockIcon, {}),
|
|
417
|
+
/* @__PURE__ */ jsx3(Text2, { size: 1, muted: true, children: formatDuration(elapsed) })
|
|
418
|
+
] }),
|
|
419
|
+
!isActive && deployedAt && /* @__PURE__ */ jsx3(Text2, { size: 1, muted: true, children: deployedAt }),
|
|
420
|
+
branch && /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
421
|
+
/* @__PURE__ */ jsx3(Text2, { size: 1, muted: true, children: "\xB7" }),
|
|
422
|
+
/* @__PURE__ */ jsx3(Badge3, { tone: "default", mode: "outline", children: branch })
|
|
423
|
+
] }),
|
|
424
|
+
sha && /* @__PURE__ */ jsx3(
|
|
425
|
+
Tooltip,
|
|
426
|
+
{
|
|
427
|
+
content: /* @__PURE__ */ jsx3(Box2, { padding: 2, children: /* @__PURE__ */ jsx3(Text2, { size: 1, children: commitMsg ?? sha }) }),
|
|
428
|
+
portal: true,
|
|
429
|
+
children: /* @__PURE__ */ jsx3(Text2, { size: 1, muted: true, style: { cursor: "default" }, children: sha })
|
|
430
|
+
}
|
|
431
|
+
),
|
|
432
|
+
creator && /* @__PURE__ */ jsxs3(Text2, { size: 1, muted: true, children: [
|
|
433
|
+
"by ",
|
|
434
|
+
creator
|
|
435
|
+
] }),
|
|
436
|
+
latest?.url && latest.state === "READY" && /* @__PURE__ */ jsx3(
|
|
437
|
+
"a",
|
|
438
|
+
{
|
|
439
|
+
href: `https://${latest.url}`,
|
|
440
|
+
target: "_blank",
|
|
441
|
+
rel: "noreferrer",
|
|
442
|
+
style: { color: "inherit" },
|
|
443
|
+
children: /* @__PURE__ */ jsxs3(Flex3, { align: "center", gap: 1, children: [
|
|
444
|
+
/* @__PURE__ */ jsx3(LaunchIcon2, {}),
|
|
445
|
+
/* @__PURE__ */ jsx3(Text2, { size: 1, children: "Preview" })
|
|
446
|
+
] })
|
|
447
|
+
}
|
|
448
|
+
)
|
|
449
|
+
] }),
|
|
450
|
+
deployError && /* @__PURE__ */ jsx3(Card2, { tone: "critical", padding: 2, radius: 2, children: /* @__PURE__ */ jsx3(Text2, { size: 1, children: deployError }) })
|
|
451
|
+
] }),
|
|
452
|
+
/* @__PURE__ */ jsxs3(Flex3, { align: "center", justify: "flex-end", gap: 2, children: [
|
|
453
|
+
isActiveState(latest?.state) && /* @__PURE__ */ jsx3(
|
|
454
|
+
Button2,
|
|
455
|
+
{
|
|
456
|
+
text: "Cancel",
|
|
457
|
+
mode: "ghost",
|
|
458
|
+
tone: "critical",
|
|
459
|
+
loading: canceling,
|
|
460
|
+
disabled: canceling,
|
|
461
|
+
onClick: cancel
|
|
462
|
+
}
|
|
463
|
+
),
|
|
464
|
+
/* @__PURE__ */ jsx3(
|
|
465
|
+
Button2,
|
|
466
|
+
{
|
|
467
|
+
text: "Deploy",
|
|
468
|
+
tone: "primary",
|
|
469
|
+
icon: RocketIcon,
|
|
470
|
+
loading: triggering,
|
|
471
|
+
disabled: isActive,
|
|
472
|
+
onClick: deploy
|
|
473
|
+
}
|
|
474
|
+
)
|
|
475
|
+
] })
|
|
476
|
+
] }) }),
|
|
477
|
+
showHistory && /* @__PURE__ */ jsx3(
|
|
478
|
+
DeployHistory,
|
|
479
|
+
{
|
|
480
|
+
target,
|
|
481
|
+
token,
|
|
482
|
+
onClose: () => setShowHistory(false)
|
|
483
|
+
}
|
|
484
|
+
)
|
|
485
|
+
] });
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// src/components/TokenSetup.tsx
|
|
489
|
+
import { useState as useState3, useCallback as useCallback3 } from "react";
|
|
490
|
+
import { useClient } from "sanity";
|
|
491
|
+
import {
|
|
492
|
+
Card as Card3,
|
|
493
|
+
Stack as Stack3,
|
|
494
|
+
Text as Text3,
|
|
495
|
+
Heading,
|
|
496
|
+
TextInput,
|
|
497
|
+
Button as Button3,
|
|
498
|
+
Flex as Flex4
|
|
499
|
+
} from "@sanity/ui";
|
|
500
|
+
import { KeyIcon, CheckmarkCircleIcon } from "@sanity/icons";
|
|
501
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
502
|
+
var TOKEN_DOC_ID = "secrets.vercelDeploy";
|
|
503
|
+
function TokenSetup({ onSaved }) {
|
|
504
|
+
const client = useClient({ apiVersion: "2025-01-01" });
|
|
505
|
+
const [token, setToken] = useState3("");
|
|
506
|
+
const [saving, setSaving] = useState3(false);
|
|
507
|
+
const [error, setError] = useState3(null);
|
|
508
|
+
const save = useCallback3(async () => {
|
|
509
|
+
if (!token.trim()) return;
|
|
510
|
+
setSaving(true);
|
|
511
|
+
setError(null);
|
|
512
|
+
try {
|
|
513
|
+
await client.createOrReplace({
|
|
514
|
+
_id: TOKEN_DOC_ID,
|
|
515
|
+
_type: "vercelDeploy.config",
|
|
516
|
+
accessToken: token.trim()
|
|
517
|
+
});
|
|
518
|
+
onSaved();
|
|
519
|
+
} catch (err) {
|
|
520
|
+
setError(err instanceof Error ? err.message : "Failed to save token");
|
|
521
|
+
} finally {
|
|
522
|
+
setSaving(false);
|
|
523
|
+
}
|
|
524
|
+
}, [client, token, onSaved]);
|
|
525
|
+
return /* @__PURE__ */ jsx4(Card3, { height: "fill", tone: "transparent", children: /* @__PURE__ */ jsx4(Flex4, { align: "center", justify: "center", height: "fill", padding: 6, children: /* @__PURE__ */ jsx4(Card3, { padding: 5, radius: 3, shadow: 1, style: { maxWidth: 480, width: "100%" }, children: /* @__PURE__ */ jsxs4(Stack3, { space: 5, children: [
|
|
526
|
+
/* @__PURE__ */ jsxs4(Flex4, { align: "center", gap: 3, children: [
|
|
527
|
+
/* @__PURE__ */ jsx4(Text3, { size: 3, children: /* @__PURE__ */ jsx4(KeyIcon, {}) }),
|
|
528
|
+
/* @__PURE__ */ jsx4(Heading, { size: 2, children: "Connect to Vercel" })
|
|
529
|
+
] }),
|
|
530
|
+
/* @__PURE__ */ jsxs4(Stack3, { space: 3, children: [
|
|
531
|
+
/* @__PURE__ */ jsxs4(Text3, { size: 1, muted: true, children: [
|
|
532
|
+
"A Vercel API token is required to read deployment status, history, and build logs. Your token is stored securely in the Sanity dataset under a",
|
|
533
|
+
" ",
|
|
534
|
+
/* @__PURE__ */ jsx4("code", { children: "secrets.*" }),
|
|
535
|
+
" document ID that is not publicly readable."
|
|
536
|
+
] }),
|
|
537
|
+
/* @__PURE__ */ jsxs4(Text3, { size: 1, muted: true, children: [
|
|
538
|
+
"Create a token at",
|
|
539
|
+
" ",
|
|
540
|
+
/* @__PURE__ */ jsx4("strong", { children: "vercel.com \u2192 Settings \u2192 Tokens" }),
|
|
541
|
+
". Choose ",
|
|
542
|
+
/* @__PURE__ */ jsx4("strong", { children: "Full Account" }),
|
|
543
|
+
" scope."
|
|
544
|
+
] })
|
|
545
|
+
] }),
|
|
546
|
+
/* @__PURE__ */ jsxs4(Stack3, { space: 3, children: [
|
|
547
|
+
/* @__PURE__ */ jsx4(Text3, { size: 1, weight: "semibold", children: "Vercel API Token" }),
|
|
548
|
+
/* @__PURE__ */ jsx4(
|
|
549
|
+
TextInput,
|
|
550
|
+
{
|
|
551
|
+
value: token,
|
|
552
|
+
onChange: (e) => setToken(e.target.value),
|
|
553
|
+
placeholder: "xxxxxxxxxxxxxxxxxxxxxxxx",
|
|
554
|
+
type: "password"
|
|
555
|
+
}
|
|
556
|
+
)
|
|
557
|
+
] }),
|
|
558
|
+
error && /* @__PURE__ */ jsx4(Card3, { tone: "critical", padding: 3, radius: 2, children: /* @__PURE__ */ jsx4(Text3, { size: 1, children: error }) }),
|
|
559
|
+
/* @__PURE__ */ jsx4(
|
|
560
|
+
Button3,
|
|
561
|
+
{
|
|
562
|
+
text: "Save and connect",
|
|
563
|
+
tone: "primary",
|
|
564
|
+
icon: CheckmarkCircleIcon,
|
|
565
|
+
loading: saving,
|
|
566
|
+
disabled: !token.trim() || saving,
|
|
567
|
+
onClick: save
|
|
568
|
+
}
|
|
569
|
+
)
|
|
570
|
+
] }) }) }) });
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// src/components/DeployTool.tsx
|
|
574
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
575
|
+
var TOKEN_QUERY = `*[_id == "secrets.vercelDeploy"][0].accessToken`;
|
|
576
|
+
var TARGETS_QUERY = `*[_type == "vercel_deploy"] | order(_createdAt asc)`;
|
|
577
|
+
function DeployTool() {
|
|
578
|
+
const client = useClient2({ apiVersion: "2025-01-01" });
|
|
579
|
+
const toast = useToast();
|
|
580
|
+
const [token, setToken] = useState4(null);
|
|
581
|
+
const [targets, setTargets] = useState4([]);
|
|
582
|
+
const [loading, setLoading] = useState4(true);
|
|
583
|
+
const [showTokenSetup, setShowTokenSetup] = useState4(false);
|
|
584
|
+
const [pendingDelete, setPendingDelete] = useState4(null);
|
|
585
|
+
const [deleting, setDeleting] = useState4(false);
|
|
586
|
+
const load = useCallback4(async () => {
|
|
587
|
+
setLoading(true);
|
|
588
|
+
try {
|
|
589
|
+
const [fetchedToken, fetchedTargets] = await Promise.all([
|
|
590
|
+
client.fetch(TOKEN_QUERY),
|
|
591
|
+
client.fetch(TARGETS_QUERY)
|
|
592
|
+
]);
|
|
593
|
+
setToken(fetchedToken ?? null);
|
|
594
|
+
setTargets(fetchedTargets);
|
|
595
|
+
} catch (err) {
|
|
596
|
+
console.error("deploy-vercel-from-sanity: load error", err);
|
|
597
|
+
} finally {
|
|
598
|
+
setLoading(false);
|
|
599
|
+
}
|
|
600
|
+
}, [client]);
|
|
601
|
+
useEffect3(() => {
|
|
602
|
+
load();
|
|
603
|
+
}, [load]);
|
|
604
|
+
useEffect3(() => {
|
|
605
|
+
const sub = client.listen(TARGETS_QUERY).subscribe(() => {
|
|
606
|
+
client.fetch(TARGETS_QUERY).then(setTargets).catch((err) => {
|
|
607
|
+
console.error("deploy-vercel-from-sanity: subscription refresh error", err);
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
return () => sub.unsubscribe();
|
|
611
|
+
}, [client]);
|
|
612
|
+
const confirmDelete = useCallback4(async () => {
|
|
613
|
+
if (!pendingDelete) return;
|
|
614
|
+
setDeleting(true);
|
|
615
|
+
try {
|
|
616
|
+
await client.delete(pendingDelete._id);
|
|
617
|
+
setTargets((prev) => prev.filter((t) => t._id !== pendingDelete._id));
|
|
618
|
+
toast.push({ status: "success", title: `Deleted "${pendingDelete.name}"` });
|
|
619
|
+
} catch (err) {
|
|
620
|
+
toast.push({ status: "error", title: "Delete failed", description: String(err) });
|
|
621
|
+
} finally {
|
|
622
|
+
setDeleting(false);
|
|
623
|
+
setPendingDelete(null);
|
|
624
|
+
}
|
|
625
|
+
}, [client, pendingDelete, toast]);
|
|
626
|
+
if (loading) {
|
|
627
|
+
return /* @__PURE__ */ jsx5(Card4, { height: "fill", tone: "transparent", children: /* @__PURE__ */ jsx5(Flex5, { align: "center", justify: "center", height: "fill", children: /* @__PURE__ */ jsx5(Spinner4, { muted: true }) }) });
|
|
628
|
+
}
|
|
629
|
+
if (!token && !showTokenSetup) {
|
|
630
|
+
return /* @__PURE__ */ jsx5(TokenSetup, { onSaved: () => {
|
|
631
|
+
setShowTokenSetup(false);
|
|
632
|
+
load();
|
|
633
|
+
} });
|
|
634
|
+
}
|
|
635
|
+
if (showTokenSetup) {
|
|
636
|
+
return /* @__PURE__ */ jsx5(
|
|
637
|
+
TokenSetup,
|
|
638
|
+
{
|
|
639
|
+
onSaved: () => {
|
|
640
|
+
setShowTokenSetup(false);
|
|
641
|
+
load();
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
return /* @__PURE__ */ jsxs5(Card4, { height: "fill", tone: "transparent", children: [
|
|
647
|
+
/* @__PURE__ */ jsx5(Box4, { padding: 5, children: /* @__PURE__ */ jsxs5(Stack4, { space: 5, children: [
|
|
648
|
+
/* @__PURE__ */ jsxs5(Flex5, { align: "center", justify: "space-between", children: [
|
|
649
|
+
/* @__PURE__ */ jsxs5(Flex5, { align: "center", gap: 3, children: [
|
|
650
|
+
/* @__PURE__ */ jsx5(RocketIcon2, {}),
|
|
651
|
+
/* @__PURE__ */ jsx5(Heading2, { size: 2, children: "Deploy" })
|
|
652
|
+
] }),
|
|
653
|
+
/* @__PURE__ */ jsx5(
|
|
654
|
+
Button4,
|
|
655
|
+
{
|
|
656
|
+
text: "API Token",
|
|
657
|
+
mode: "ghost",
|
|
658
|
+
icon: KeyIcon2,
|
|
659
|
+
fontSize: 1,
|
|
660
|
+
onClick: () => setShowTokenSetup(true)
|
|
661
|
+
}
|
|
662
|
+
)
|
|
663
|
+
] }),
|
|
664
|
+
targets.length === 0 && /* @__PURE__ */ jsx5(Card4, { padding: 5, radius: 2, tone: "transparent", shadow: 1, children: /* @__PURE__ */ jsxs5(Stack4, { space: 3, style: { textAlign: "center" }, children: [
|
|
665
|
+
/* @__PURE__ */ jsx5(Text4, { size: 2, weight: "semibold", children: "No deploy targets configured" }),
|
|
666
|
+
/* @__PURE__ */ jsxs5(Text4, { size: 1, muted: true, children: [
|
|
667
|
+
"Create a ",
|
|
668
|
+
/* @__PURE__ */ jsx5("code", { children: "vercel_deploy" }),
|
|
669
|
+
" document in the dataset with a Vercel deploy hook URL, or add one via the Sanity CLI."
|
|
670
|
+
] })
|
|
671
|
+
] }) }),
|
|
672
|
+
token && targets.map((target) => /* @__PURE__ */ jsx5(
|
|
673
|
+
DeployItem,
|
|
674
|
+
{
|
|
675
|
+
target,
|
|
676
|
+
token,
|
|
677
|
+
onDelete: setPendingDelete
|
|
678
|
+
},
|
|
679
|
+
target._id
|
|
680
|
+
))
|
|
681
|
+
] }) }),
|
|
682
|
+
pendingDelete && /* @__PURE__ */ jsx5(
|
|
683
|
+
Dialog2,
|
|
684
|
+
{
|
|
685
|
+
header: "Delete deploy target?",
|
|
686
|
+
id: "confirm-delete",
|
|
687
|
+
onClose: () => setPendingDelete(null),
|
|
688
|
+
width: 1,
|
|
689
|
+
footer: /* @__PURE__ */ jsxs5(Flex5, { padding: 3, gap: 2, justify: "flex-end", children: [
|
|
690
|
+
/* @__PURE__ */ jsx5(
|
|
691
|
+
Button4,
|
|
692
|
+
{
|
|
693
|
+
text: "Cancel",
|
|
694
|
+
mode: "ghost",
|
|
695
|
+
onClick: () => setPendingDelete(null)
|
|
696
|
+
}
|
|
697
|
+
),
|
|
698
|
+
/* @__PURE__ */ jsx5(
|
|
699
|
+
Button4,
|
|
700
|
+
{
|
|
701
|
+
text: "Delete",
|
|
702
|
+
tone: "critical",
|
|
703
|
+
icon: TrashIcon2,
|
|
704
|
+
loading: deleting,
|
|
705
|
+
disabled: deleting,
|
|
706
|
+
onClick: confirmDelete
|
|
707
|
+
}
|
|
708
|
+
)
|
|
709
|
+
] }),
|
|
710
|
+
children: /* @__PURE__ */ jsx5(Box4, { padding: 4, children: /* @__PURE__ */ jsxs5(Stack4, { space: 3, children: [
|
|
711
|
+
/* @__PURE__ */ jsxs5(Flex5, { align: "center", gap: 2, children: [
|
|
712
|
+
/* @__PURE__ */ jsx5(WarningOutlineIcon, {}),
|
|
713
|
+
/* @__PURE__ */ jsx5(Text4, { size: 2, weight: "semibold", children: pendingDelete.name })
|
|
714
|
+
] }),
|
|
715
|
+
/* @__PURE__ */ jsx5(Text4, { size: 1, muted: true, children: "This removes the deploy target from the dataset. The Vercel deploy hook itself is not affected." })
|
|
716
|
+
] }) })
|
|
717
|
+
}
|
|
718
|
+
)
|
|
719
|
+
] });
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/schema/vercelDeploy.ts
|
|
723
|
+
import { defineField, defineType } from "sanity";
|
|
724
|
+
import { RocketIcon as RocketIcon3 } from "@sanity/icons";
|
|
725
|
+
var vercelDeploySchema = defineType({
|
|
726
|
+
name: "vercel_deploy",
|
|
727
|
+
title: "Deploy Target",
|
|
728
|
+
type: "document",
|
|
729
|
+
icon: RocketIcon3,
|
|
730
|
+
fields: [
|
|
731
|
+
defineField({
|
|
732
|
+
name: "name",
|
|
733
|
+
title: "Name",
|
|
734
|
+
type: "string",
|
|
735
|
+
description: 'Display label shown in the Deploy tool (e.g. "Production", "Staging")',
|
|
736
|
+
validation: (Rule) => Rule.required()
|
|
737
|
+
}),
|
|
738
|
+
defineField({
|
|
739
|
+
name: "url",
|
|
740
|
+
title: "Deploy Hook URL",
|
|
741
|
+
type: "url",
|
|
742
|
+
description: "From Vercel \u2192 Project Settings \u2192 Git \u2192 Deploy Hooks",
|
|
743
|
+
validation: (Rule) => Rule.required().uri({ scheme: ["https"] }).custom((url) => {
|
|
744
|
+
if (typeof url !== "string") return true;
|
|
745
|
+
if (!url.includes("api.vercel.com/v1/integrations/deploy/")) {
|
|
746
|
+
return "Must be a Vercel deploy hook URL (api.vercel.com/v1/integrations/deploy/\u2026)";
|
|
747
|
+
}
|
|
748
|
+
return true;
|
|
749
|
+
})
|
|
750
|
+
}),
|
|
751
|
+
defineField({
|
|
752
|
+
name: "teamId",
|
|
753
|
+
title: "Vercel Team ID",
|
|
754
|
+
type: "string",
|
|
755
|
+
description: "Required for team-owned projects \u2014 find it in Vercel Team Settings"
|
|
756
|
+
}),
|
|
757
|
+
defineField({
|
|
758
|
+
name: "disableDeleteAction",
|
|
759
|
+
title: "Prevent deletion",
|
|
760
|
+
type: "boolean",
|
|
761
|
+
description: "Lock this target so it cannot be deleted from the Studio",
|
|
762
|
+
initialValue: false
|
|
763
|
+
})
|
|
764
|
+
],
|
|
765
|
+
preview: {
|
|
766
|
+
select: { title: "name", subtitle: "url" }
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// src/index.ts
|
|
771
|
+
var vercelDeploy = definePlugin((options) => {
|
|
772
|
+
const config = options ?? {};
|
|
773
|
+
return {
|
|
774
|
+
name: "deploy-vercel-from-sanity",
|
|
775
|
+
schema: {
|
|
776
|
+
types: [vercelDeploySchema]
|
|
777
|
+
},
|
|
778
|
+
tools: [
|
|
779
|
+
{
|
|
780
|
+
name: config.name ?? "vercel-deploy",
|
|
781
|
+
title: config.title ?? "Deploy",
|
|
782
|
+
icon: config.icon ?? RocketIcon4,
|
|
783
|
+
component: DeployTool
|
|
784
|
+
}
|
|
785
|
+
]
|
|
786
|
+
};
|
|
787
|
+
});
|
|
788
|
+
export {
|
|
789
|
+
vercelDeploy,
|
|
790
|
+
vercelDeploySchema
|
|
791
|
+
};
|