sanity-plugin-dashboard-widget-vercel 2.0.0 → 3.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/lib/index.mjs ADDED
@@ -0,0 +1,980 @@
1
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
+ import { LinkIcon, UploadIcon, EditIcon, ErrorOutlineIcon, AddIcon } from "@sanity/icons";
3
+ import { useTheme, Box, Label, Flex, Text, Stack, useToast, Button, Tooltip, Inline, TextInput, Dialog, ToastProvider, Card } from "@sanity/ui";
4
+ import { useMachine } from "@xstate/react";
5
+ import { useQuery, QueryClient, QueryClientProvider } from "react-query";
6
+ import { Machine, assign } from "xstate";
7
+ import { useRef, useMemo, useEffect, forwardRef } from "react";
8
+ import useDeepCompareEffect from "use-deep-compare-effect";
9
+ import hash from "object-hash";
10
+ import fetch from "unfetch";
11
+ import ReactTimeAgo from "react-time-ago";
12
+ import { yupResolver } from "@hookform/resolvers/yup";
13
+ import { uuid } from "@sanity/uuid";
14
+ import { useForm } from "react-hook-form";
15
+ import * as yup from "yup";
16
+ import { styled } from "styled-components";
17
+ import { useClient } from "sanity";
18
+ import TimeAgo from "javascript-time-ago";
19
+ import en from "javascript-time-ago/locale/en";
20
+ const API_ENDPOINT_DEPLOYMENTS = "https://api.vercel.com/v5/now/deployments", API_ENDPOINT_ALIASES = "https://api.vercel.com/v3/now/aliases", API_VERSION = "1", DEPLOYMENT_TARGET_DOCUMENT_TYPE = "vercel.deploymentTarget", VERCEL_STATUS_COLORS = {
21
+ BUILDING: "#f5a623",
22
+ CANCELED: "#ff0000",
23
+ ERROR: "#ff0000",
24
+ READY: "#50e3c2",
25
+ QUEUED: "#333"
26
+ }, WIDGET_NAME = "Vercel (dashboard)", Z_INDEX_DIALOG = 600001, Z_INDEX_TOAST_PROVIDER = 600002, StateDebug = (props) => null, sortByTargetName = (items) => items.sort((a, b) => a.name > b.name ? 1 : a.name < b.name ? -1 : 0), deploymentTargetListMachine = () => Machine(
27
+ {
28
+ context: {
29
+ message: "",
30
+ results: []
31
+ },
32
+ initial: "pending",
33
+ states: {
34
+ pending: {
35
+ invoke: {
36
+ src: "fetchDataService",
37
+ onDone: { actions: ["setResults"], target: "ready" },
38
+ onError: { actions: ["setMessage"], target: "failed" }
39
+ }
40
+ },
41
+ ready: {
42
+ initial: "unknown",
43
+ on: {
44
+ CREATE: { actions: ["targetCreate"] },
45
+ DELETE: { actions: ["targetDelete"] },
46
+ UPDATE: { actions: ["targetUpdate"] }
47
+ },
48
+ states: {
49
+ unknown: {
50
+ always: [
51
+ { cond: "hasData", target: "withData" },
52
+ { cond: "hasNoData", target: "withoutData" }
53
+ ]
54
+ },
55
+ withData: {
56
+ always: [{ cond: "hasNoData", target: "withoutData" }]
57
+ },
58
+ withoutData: {
59
+ always: [{ cond: "hasData", target: "withData" }]
60
+ }
61
+ }
62
+ },
63
+ failed: {
64
+ type: "final"
65
+ }
66
+ }
67
+ },
68
+ {
69
+ actions: {
70
+ setMessage: assign((_context, event) => ({
71
+ message: event.data.details.description
72
+ })),
73
+ setResults: assign((_context, event) => ({
74
+ results: event.data
75
+ })),
76
+ targetCreate: assign((context, event) => ({
77
+ results: sortByTargetName([...context.results, event.deploymentTarget])
78
+ })),
79
+ targetDelete: assign((context, event) => ({
80
+ results: context.results.filter((target) => target._id !== event.id)
81
+ })),
82
+ targetUpdate: assign((context, event) => {
83
+ const { deploymentTarget } = event, index = context.results.findIndex((target) => target._id === deploymentTarget._id), updatedResults = Object.assign([], context.results, {
84
+ [index]: event.deploymentTarget
85
+ });
86
+ return {
87
+ results: sortByTargetName(updatedResults)
88
+ };
89
+ })
90
+ },
91
+ guards: {
92
+ hasData: (context) => context?.results?.length > 0,
93
+ hasNoData: (context) => context?.results?.length === 0
94
+ }
95
+ }
96
+ ), fetcher = (deploymentTarget) => async (url, extraParams) => {
97
+ const params = new URLSearchParams();
98
+ if (params.set("projectId", deploymentTarget.projectId), deploymentTarget.teamId && params.set("teamId", deploymentTarget.teamId), extraParams)
99
+ for (const [k, v] of extraParams.entries())
100
+ params.append(k, v);
101
+ const response = await fetch(`${url}?${params.toString()}`, {
102
+ headers: {
103
+ Authorization: `Bearer ${deploymentTarget.token}`
104
+ }
105
+ });
106
+ if (!response.ok)
107
+ throw new Error("Response not OK");
108
+ try {
109
+ return response.json();
110
+ } catch (err) {
111
+ throw new Error(err);
112
+ }
113
+ }, useDeployments = (deploymentTarget, options) => {
114
+ const fetchUrl = fetcher(deploymentTarget), deployParams = new URLSearchParams();
115
+ deployParams.set("limit", String(deploymentTarget?.deployLimit));
116
+ const {
117
+ data: deploymentsData,
118
+ isFetching: deploymentsIsFetching,
119
+ isSuccess: deploymentsIsSuccess,
120
+ error: deploymentsError,
121
+ refetch
122
+ } = useQuery(
123
+ hash(deploymentTarget),
124
+ // key
125
+ () => fetchUrl(API_ENDPOINT_DEPLOYMENTS, deployParams),
126
+ {
127
+ enabled: options?.enabled ?? !0,
128
+ refetchInterval: 2e4,
129
+ // ms
130
+ refetchIntervalInBackground: !1,
131
+ refetchOnMount: !0,
132
+ refetchOnReconnect: "always",
133
+ refetchOnWindowFocus: !1,
134
+ retry: !1
135
+ }
136
+ ), aliasParams = new URLSearchParams();
137
+ aliasParams.set("limit", "20");
138
+ const {
139
+ data: aliasesData,
140
+ isFetching: aliasesIsFetching,
141
+ isSuccess: aliasesIsSuccess,
142
+ error: aliasesError
143
+ } = useQuery(
144
+ `${hash(deploymentTarget)}-aliases`,
145
+ // key
146
+ () => fetchUrl(API_ENDPOINT_ALIASES, aliasParams),
147
+ {
148
+ enabled: !!deploymentsData,
149
+ refetchOnMount: !1,
150
+ refetchOnReconnect: !1,
151
+ refetchOnWindowFocus: !1,
152
+ retry: !1
153
+ }
154
+ ), aliases = aliasesData?.aliases;
155
+ let deploymentsWithAlias;
156
+ return aliases && (deploymentsWithAlias = deploymentsData?.deployments?.map((val) => {
157
+ const alias = aliases.find((a) => a.deploymentId === val.uid);
158
+ return {
159
+ ...val,
160
+ alias: alias?.alias
161
+ };
162
+ })), {
163
+ deployments: deploymentsWithAlias,
164
+ error: aliasesError || deploymentsError,
165
+ isFetching: aliasesIsFetching || deploymentsIsFetching,
166
+ isSuccess: aliasesIsSuccess && deploymentsIsSuccess,
167
+ refetch
168
+ };
169
+ }, refreshMachine = Machine({
170
+ initial: "idle",
171
+ states: {
172
+ idle: {
173
+ on: {
174
+ REFRESH: "refreshing"
175
+ }
176
+ },
177
+ refreshing: {
178
+ on: {
179
+ ERROR: "error",
180
+ REFRESHED: "refreshed"
181
+ }
182
+ },
183
+ refreshed: {
184
+ on: {
185
+ REFRESH: "refreshing"
186
+ }
187
+ },
188
+ error: {
189
+ on: {
190
+ REFRESH: "refreshing"
191
+ }
192
+ }
193
+ }
194
+ });
195
+ function useCardColor() {
196
+ return useTheme().sanity.color.card.enabled;
197
+ }
198
+ const TableCell = (props) => {
199
+ const { children, colSpan, header, variant } = props;
200
+ let display = "table-cell", cellWidth = "auto";
201
+ switch (variant) {
202
+ case "age":
203
+ cellWidth = "50px";
204
+ break;
205
+ case "branch":
206
+ cellWidth = "300px", display = ["none", "none", "none", "table-cell"];
207
+ break;
208
+ case "creator":
209
+ cellWidth = "80px";
210
+ break;
211
+ case "state":
212
+ cellWidth = "110px", display = ["none", "none", "none", "none", "table-cell"];
213
+ break;
214
+ }
215
+ const { border } = useCardColor();
216
+ return header ? /* @__PURE__ */ jsx(
217
+ Box,
218
+ {
219
+ as: "th",
220
+ colSpan,
221
+ display,
222
+ paddingX: 3,
223
+ paddingY: 2,
224
+ style: {
225
+ maxWidth: cellWidth,
226
+ position: "relative",
227
+ textAlign: "left",
228
+ width: cellWidth
229
+ },
230
+ children: /* @__PURE__ */ jsx(Label, { size: 0, children })
231
+ }
232
+ ) : /* @__PURE__ */ jsx(
233
+ Box,
234
+ {
235
+ as: "td",
236
+ colSpan,
237
+ display,
238
+ paddingX: 3,
239
+ paddingY: [2, 2, 3],
240
+ style: {
241
+ borderTop: `1px solid ${border}`,
242
+ maxWidth: cellWidth,
243
+ position: "relative",
244
+ textAlign: "left",
245
+ width: cellWidth
246
+ },
247
+ children
248
+ }
249
+ );
250
+ }, StatusDot = ({ state }) => /* @__PURE__ */ jsx(
251
+ Box,
252
+ {
253
+ style: {
254
+ backgroundColor: `${VERCEL_STATUS_COLORS[state]}`,
255
+ borderRadius: "20px",
256
+ height: "9px",
257
+ width: "9px"
258
+ }
259
+ }
260
+ ), Deployment = (props) => {
261
+ const { deployment } = props, date = useRef(new Date(deployment.created)), commitMessage = deployment?.meta?.githubCommitMessage, commitRef = deployment?.meta?.githubCommitRef, targetUrl = deployment.alias ?? deployment.url;
262
+ return /* @__PURE__ */ jsxs("tr", { children: [
263
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsxs(Flex, { align: "center", children: [
264
+ /* @__PURE__ */ jsx(
265
+ Box,
266
+ {
267
+ display: ["block", "block", "block", "block", "none"],
268
+ marginRight: 3,
269
+ style: { flexShrink: 0 },
270
+ children: /* @__PURE__ */ jsx(StatusDot, { state: deployment.state })
271
+ }
272
+ ),
273
+ targetUrl ? /* @__PURE__ */ jsxs(Fragment, { children: [
274
+ deployment.alias && /* @__PURE__ */ jsx(LinkIcon, {}),
275
+ /* @__PURE__ */ jsx(Box, { marginLeft: deployment.alias ? 1 : 0, children: /* @__PURE__ */ jsx(
276
+ Text,
277
+ {
278
+ muted: deployment.state !== "READY",
279
+ size: 1,
280
+ style: {
281
+ textDecoration: deployment.state === "CANCELED" || deployment.state === "ERROR" ? "line-through" : "normal"
282
+ },
283
+ textOverflow: "ellipsis",
284
+ children: deployment.state === "READY" ? /* @__PURE__ */ jsx("a", { href: `https://${targetUrl}`, rel: "noopener noreferrer", target: "_blank", children: targetUrl }) : targetUrl
285
+ }
286
+ ) })
287
+ ] }) : /* @__PURE__ */ jsx(Text, { size: 1, children: "Uploading..." })
288
+ ] }) }),
289
+ /* @__PURE__ */ jsx(TableCell, { variant: "state", children: /* @__PURE__ */ jsxs(Flex, { align: "center", children: [
290
+ /* @__PURE__ */ jsx(StatusDot, { state: deployment.state }),
291
+ /* @__PURE__ */ jsx(Box, { marginLeft: 2, children: /* @__PURE__ */ jsx(Text, { size: 1, children: deployment.state.trim().toLowerCase().replace(/^[a-z]/i, (t) => t.toUpperCase()) }) })
292
+ ] }) }),
293
+ /* @__PURE__ */ jsx(TableCell, { variant: "branch", children: /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
294
+ /* @__PURE__ */ jsx(Text, { size: 1, textOverflow: "ellipsis", children: commitRef }),
295
+ commitMessage && /* @__PURE__ */ jsx(Text, { muted: !0, size: 1, textOverflow: "ellipsis", children: commitMessage })
296
+ ] }) }),
297
+ /* @__PURE__ */ jsx(TableCell, { variant: "age", children: /* @__PURE__ */ jsx(Flex, { align: "center", children: /* @__PURE__ */ jsx(Text, { size: 1, children: /* @__PURE__ */ jsx(ReactTimeAgo, { date: date.current, locale: "en-US", timeStyle: "mini" }) }) }) }),
298
+ /* @__PURE__ */ jsx(TableCell, { variant: "creator", children: /* @__PURE__ */ jsx(Flex, { align: "center", justify: "center", children: /* @__PURE__ */ jsx(
299
+ "img",
300
+ {
301
+ draggable: !1,
302
+ src: `https://vercel.com/api/www/avatar/${deployment?.creator?.uid}?&s=48`,
303
+ style: {
304
+ borderRadius: "20px",
305
+ height: "20px",
306
+ width: "20px"
307
+ }
308
+ }
309
+ ) }) })
310
+ ] });
311
+ }, deployMachine = (deployHook) => Machine(
312
+ // Machine
313
+ {
314
+ id: "deploy",
315
+ initial: "idle",
316
+ context: {
317
+ disabled: !1,
318
+ feedback: void 0,
319
+ label: void 0,
320
+ error: void 0
321
+ },
322
+ states: {
323
+ idle: {
324
+ entry: assign({
325
+ feedback: () => {
326
+ },
327
+ label: () => "Deploy"
328
+ }),
329
+ on: {
330
+ DEPLOY: "deploying"
331
+ }
332
+ },
333
+ deploying: {
334
+ entry: assign({
335
+ disabled: () => !0,
336
+ label: () => "Deploying"
337
+ }),
338
+ exit: assign({
339
+ disabled: () => !1,
340
+ label: () => "Deploy"
341
+ }),
342
+ invoke: {
343
+ onDone: {
344
+ target: "success"
345
+ },
346
+ onError: {
347
+ target: "error",
348
+ actions: assign({
349
+ error: (_context, event) => event.data
350
+ })
351
+ },
352
+ src: "deploy"
353
+ }
354
+ },
355
+ success: {
356
+ entry: [assign({ feedback: () => "Succesfully started!" })],
357
+ exit: assign({
358
+ feedback: () => {
359
+ }
360
+ }),
361
+ on: {
362
+ DEPLOY: "deploying"
363
+ }
364
+ },
365
+ error: {
366
+ on: {
367
+ DEPLOY: "deploying"
368
+ }
369
+ }
370
+ }
371
+ },
372
+ // Config
373
+ {
374
+ services: {
375
+ deploy: () => new Promise(async (resolve, reject) => {
376
+ try {
377
+ if (!deployHook)
378
+ return reject(new Error("No deployHook URL defined"));
379
+ const res = await fetch(deployHook, { method: "POST" }), data = await res.json();
380
+ if (!res.ok) {
381
+ const errorMessage = (data?.error).message || res.statusText;
382
+ return reject(errorMessage);
383
+ }
384
+ return resolve();
385
+ } catch (err) {
386
+ return console.error("Unable to deploy with error:", err), reject(new Error("Please check the developer console for more information"));
387
+ }
388
+ })
389
+ }
390
+ }
391
+ ), DeployButton = (props) => {
392
+ const { deployHook, onDeploySuccess, targetName } = props, machine = useMemo(() => deployMachine(deployHook), [deployHook]), [deployState, deployStateTransition, deployStateInterpreter] = useMachine(machine), toast = useToast(), isError = deployState.matches("error"), isSuccess = deployState.matches("success"), handleDeploy = () => {
393
+ deployStateTransition({ type: "DEPLOY" });
394
+ };
395
+ return useEffect(() => {
396
+ isError && toast.push({
397
+ closable: !0,
398
+ description: `Unable to queue deploy for ${targetName}: ${deployState.context.error}`,
399
+ duration: 8e3,
400
+ status: "error",
401
+ title: WIDGET_NAME
402
+ }), isSuccess && toast.push({
403
+ closable: !0,
404
+ description: `Deploy queued for ${targetName}`,
405
+ duration: 8e3,
406
+ status: "success",
407
+ title: WIDGET_NAME
408
+ });
409
+ }, [isError, isSuccess, toast, targetName, deployState.context.error]), useEffect(() => {
410
+ deployStateInterpreter.onTransition((state) => {
411
+ state.value === "success" && onDeploySuccess && onDeploySuccess();
412
+ });
413
+ }, [deployStateInterpreter, onDeploySuccess]), /* @__PURE__ */ jsxs(Box, { padding: 3, style: { position: "relative" }, children: [
414
+ /* @__PURE__ */ jsx(StateDebug, { name: "Deploy", state: deployState }),
415
+ /* @__PURE__ */ jsx(
416
+ Button,
417
+ {
418
+ disabled: deployState.context.disabled,
419
+ fontSize: 1,
420
+ icon: UploadIcon,
421
+ mode: "ghost",
422
+ onClick: handleDeploy,
423
+ padding: 3,
424
+ text: `${deployState.context.label} ${targetName}`,
425
+ tone: "default"
426
+ }
427
+ )
428
+ ] });
429
+ }, PlaceholderAvatar = () => {
430
+ const { border } = useCardColor();
431
+ return /* @__PURE__ */ jsx(
432
+ Box,
433
+ {
434
+ style: {
435
+ backgroundColor: border,
436
+ borderRadius: "20px",
437
+ height: "20px",
438
+ userSelect: "none",
439
+ width: "20px"
440
+ }
441
+ }
442
+ );
443
+ }, PlaceholderText = (props) => {
444
+ const { rows } = props, { border } = useCardColor();
445
+ return /* @__PURE__ */ jsx(
446
+ Box,
447
+ {
448
+ style: {
449
+ backgroundColor: border,
450
+ borderRadius: "3px",
451
+ userSelect: "none",
452
+ width: "100%"
453
+ },
454
+ children: /* @__PURE__ */ jsx(Stack, { space: 2, children: new Array(rows).fill(void 0).map((_, index) => /* @__PURE__ */ jsx(Text, { size: 1, children: "\xA0" }, index)) })
455
+ }
456
+ );
457
+ }, DeploymentPlaceholder = () => /* @__PURE__ */ jsxs("tr", { children: [
458
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(PlaceholderText, { rows: 1 }) }),
459
+ /* @__PURE__ */ jsx(TableCell, { variant: "state", children: /* @__PURE__ */ jsx(PlaceholderText, { rows: 1 }) }),
460
+ /* @__PURE__ */ jsx(TableCell, { variant: "branch", children: /* @__PURE__ */ jsx(PlaceholderText, { rows: 2 }) }),
461
+ /* @__PURE__ */ jsx(TableCell, { variant: "age", children: /* @__PURE__ */ jsx(PlaceholderText, { rows: 1 }) }),
462
+ /* @__PURE__ */ jsx(TableCell, { variant: "creator", children: /* @__PURE__ */ jsx(Flex, { justify: "center", children: /* @__PURE__ */ jsx(PlaceholderAvatar, {}) }) })
463
+ ] }), Deployments = (props) => {
464
+ const { deploymentTarget } = props, refTimeout = useRef(null), [refreshState, refreshStateTransition] = useMachine(refreshMachine), { deployments, error, isFetching, isSuccess, refetch } = useDeployments(deploymentTarget, {
465
+ enabled: !refreshState.matches("error")
466
+ }), toast = useToast(), isError = refreshState.matches("error"), handleDeploySuccess = () => {
467
+ refTimeout.current && clearTimeout(refTimeout.current), refTimeout.current = setTimeout(() => {
468
+ refetch({
469
+ cancelRefetch: !0,
470
+ throwOnError: !0
471
+ });
472
+ }, 4e3);
473
+ };
474
+ useEffect(() => () => {
475
+ refTimeout.current && clearTimeout(refTimeout.current);
476
+ }, []), useEffect(() => {
477
+ error && refreshStateTransition({ type: "ERROR" }), isFetching && refreshStateTransition({ type: "REFRESH" }), !isFetching && isSuccess && refreshStateTransition({ type: "REFRESHED" });
478
+ }, [error, isFetching, isSuccess, refreshStateTransition]), useDeepCompareEffect(() => {
479
+ refreshState.matches("refreshing") || refreshStateTransition({ type: "REFRESH" });
480
+ }, [deploymentTarget]), useDeepCompareEffect(() => {
481
+ isError && toast.push({
482
+ closable: !0,
483
+ description: `Unable to fetch deployments for ${deploymentTarget.name}`,
484
+ duration: 8e3,
485
+ status: "error",
486
+ title: WIDGET_NAME
487
+ });
488
+ }, [deploymentTarget, isError]);
489
+ const hasFetched = typeof deployments < "u", hasDeployments = deployments && deployments.length > 0, { border } = useCardColor();
490
+ return /* @__PURE__ */ jsxs(Box, { marginTop: 3, style: { position: "relative" }, children: [
491
+ /* @__PURE__ */ jsx(StateDebug, { name: "Refresh", state: refreshState }),
492
+ !refreshState.matches("error") && /* @__PURE__ */ jsxs(Fragment, { children: [
493
+ /* @__PURE__ */ jsxs(
494
+ Box,
495
+ {
496
+ as: "table",
497
+ style: {
498
+ borderBottom: `1px solid ${border}`,
499
+ borderCollapse: "collapse",
500
+ display: "table",
501
+ tableLayout: "fixed",
502
+ width: "100%"
503
+ },
504
+ children: [
505
+ /* @__PURE__ */ jsx(Box, { as: "thead", style: { display: "table-header-group" }, children: /* @__PURE__ */ jsxs("tr", { children: [
506
+ /* @__PURE__ */ jsx(TableCell, { header: !0, children: "Deployment" }),
507
+ /* @__PURE__ */ jsx(TableCell, { header: !0, variant: "state", children: "State" }),
508
+ /* @__PURE__ */ jsx(TableCell, { header: !0, variant: "branch", children: "Branch" }),
509
+ /* @__PURE__ */ jsx(TableCell, { header: !0, variant: "age", children: "Age" }),
510
+ /* @__PURE__ */ jsx(TableCell, { header: !0, variant: "age", children: "Creator" })
511
+ ] }) }),
512
+ /* @__PURE__ */ jsxs(Box, { as: "tbody", style: { display: "table-header-group" }, children: [
513
+ !deployments && new Array(deploymentTarget?.deployLimit).fill(void 0).map((_, index) => /* @__PURE__ */ jsx(DeploymentPlaceholder, {}, index)),
514
+ hasDeployments && deployments?.map((deployment) => /* @__PURE__ */ jsx(Deployment, { deployment }, deployment.uid))
515
+ ] })
516
+ ]
517
+ }
518
+ ),
519
+ hasFetched && !hasDeployments && /* @__PURE__ */ jsx(Box, { padding: 3, style: { width: "100%" }, children: /* @__PURE__ */ jsx(Text, { muted: !0, size: 1, children: "No deployments found. Don't forget to specify a valid team ID if your project belongs to a team." }) })
520
+ ] }),
521
+ refreshState.matches("error") && /* @__PURE__ */ jsx(Box, { padding: 3, children: /* @__PURE__ */ jsx(Text, { muted: !0, size: 1, children: "Unable to fetch recent deployments. Please check your network and deployment settings." }) }),
522
+ !refreshState.matches("error") && deploymentTarget.deployHook && /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
523
+ DeployButton,
524
+ {
525
+ deployHook: deploymentTarget.deployHook,
526
+ onDeploySuccess: handleDeploySuccess,
527
+ targetName: deploymentTarget.name
528
+ }
529
+ ) })
530
+ ] });
531
+ }, DeploymentTarget = (props) => {
532
+ const { item, onDialogEdit } = props, deploymentTarget = {
533
+ deployHook: item.deployHook,
534
+ deployLimit: item.deployLimit,
535
+ name: item.name,
536
+ projectId: item.projectId,
537
+ teamId: item.teamId,
538
+ token: item.token
539
+ };
540
+ return /* @__PURE__ */ jsxs(Box, { style: { position: "relative" }, children: [
541
+ /* @__PURE__ */ jsxs(Flex, { align: "center", justify: "space-between", marginTop: 2, paddingX: 3, children: [
542
+ /* @__PURE__ */ jsx(Text, { size: 2, children: item.name }),
543
+ /* @__PURE__ */ jsx(
544
+ Tooltip,
545
+ {
546
+ content: /* @__PURE__ */ jsx(Box, { padding: 2, children: /* @__PURE__ */ jsx(Text, { muted: !0, size: 1, children: "Edit deployment target" }) }),
547
+ placement: "left",
548
+ children: /* @__PURE__ */ jsx(Button, { fontSize: 1, icon: EditIcon, mode: "bleed", onClick: () => onDialogEdit(item) })
549
+ }
550
+ )
551
+ ] }),
552
+ /* @__PURE__ */ jsx(Deployments, { deploymentTarget })
553
+ ] });
554
+ }, DeploymentTargets = (props) => {
555
+ const { items, onDialogEdit } = props;
556
+ return /* @__PURE__ */ jsx(Stack, { space: 5, children: items?.map((item) => /* @__PURE__ */ jsx(DeploymentTarget, { item, onDialogEdit }, item._id)) });
557
+ }, formMachine = Machine(
558
+ {
559
+ context: {
560
+ formData: {},
561
+ message: ""
562
+ },
563
+ initial: "idle",
564
+ states: {
565
+ idle: {
566
+ on: {
567
+ CREATE: {
568
+ actions: ["createDocument"],
569
+ target: "creating"
570
+ },
571
+ DELETE: {
572
+ actions: ["deleteDocument"],
573
+ target: "deleting"
574
+ },
575
+ UPDATE: {
576
+ actions: ["updateDocument"],
577
+ target: "updating"
578
+ }
579
+ }
580
+ },
581
+ creating: {
582
+ invoke: {
583
+ src: "createDocumentService",
584
+ onDone: { target: "success" },
585
+ onError: { actions: ["setMessage"], target: "error" }
586
+ },
587
+ on: {
588
+ RESOLVE: "success",
589
+ REJECT: "error"
590
+ }
591
+ },
592
+ updating: {
593
+ invoke: {
594
+ src: "updateDocumentService",
595
+ onDone: { target: "success" },
596
+ onError: { actions: ["setMessage"], target: "error" }
597
+ },
598
+ on: {
599
+ RESOLVE: "success",
600
+ REJECT: "error"
601
+ }
602
+ },
603
+ deleting: {
604
+ invoke: {
605
+ src: "deleteDocumentService",
606
+ onDone: { target: "success" },
607
+ onError: { actions: ["setMessage"], target: "error" }
608
+ },
609
+ on: {
610
+ RESOLVE: "success",
611
+ REJECT: "error"
612
+ }
613
+ },
614
+ success: {
615
+ invoke: {
616
+ src: "formSubmittedService"
617
+ }
618
+ },
619
+ error: {}
620
+ }
621
+ },
622
+ {
623
+ actions: {
624
+ setMessage: assign((_context, event) => ({
625
+ message: event.data.details.description
626
+ })),
627
+ createDocument: assign((_context, event) => ({
628
+ formData: event.formData
629
+ })),
630
+ deleteDocument: assign(() => ({
631
+ // id: event.id,
632
+ })),
633
+ updateDocument: assign((_context, event) => ({
634
+ formData: event.formData
635
+ }))
636
+ }
637
+ }
638
+ ), sanitizeFormData = (formData) => Object.keys(formData).reduce((acc, key) => {
639
+ const val = formData[key];
640
+ return typeof val == "object" && val !== null && val.constructor !== Array ? acc[key] = sanitizeFormData(val) : val === "" || typeof val > "u" || val?.length === 0 ? acc[key] = null : typeof val == "string" && val ? acc[key] = formData[key].trim() : acc[key] = formData[key], acc;
641
+ }, {}), StyledErrorOutlineIcon = styled(ErrorOutlineIcon)(({ theme }) => ({
642
+ color: theme.sanity.color.spot.red
643
+ })), FormFieldInputLabel = (props) => {
644
+ const { description, error, label, name } = props;
645
+ return /* @__PURE__ */ jsxs(Box, { marginBottom: 3, children: [
646
+ /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
647
+ /* @__PURE__ */ jsx(Text, { as: "label", htmlFor: name, size: 1, weight: "semibold", children: label }),
648
+ error && /* @__PURE__ */ jsx(Text, { size: 1, children: /* @__PURE__ */ jsx(
649
+ Tooltip,
650
+ {
651
+ content: /* @__PURE__ */ jsx(Box, { padding: 2, children: /* @__PURE__ */ jsxs(Text, { muted: !0, size: 1, children: [
652
+ /* @__PURE__ */ jsx(StyledErrorOutlineIcon, { style: { marginRight: "0.1em" } }),
653
+ error.message
654
+ ] }) }),
655
+ fallbackPlacements: ["top", "left"],
656
+ placement: "right",
657
+ portal: !0,
658
+ children: /* @__PURE__ */ jsx(StyledErrorOutlineIcon, {})
659
+ }
660
+ ) })
661
+ ] }),
662
+ description && /* @__PURE__ */ jsx(Box, { marginY: 3, children: /* @__PURE__ */ jsx(Text, { htmlFor: name, muted: !0, size: 1, children: description }) })
663
+ ] });
664
+ }, FormFieldInputText = forwardRef((props, ref) => {
665
+ const { description, disabled, error, label, name, placeholder, value } = props;
666
+ return /* @__PURE__ */ jsxs(Box, { children: [
667
+ /* @__PURE__ */ jsx(FormFieldInputLabel, { description, error, label, name }),
668
+ /* @__PURE__ */ jsx(
669
+ TextInput,
670
+ {
671
+ autoComplete: "off",
672
+ autoFocus: !0,
673
+ defaultValue: value,
674
+ disabled,
675
+ id: name,
676
+ name,
677
+ placeholder,
678
+ ref
679
+ }
680
+ )
681
+ ] });
682
+ });
683
+ function useSanityClient() {
684
+ return useClient({ apiVersion: API_VERSION });
685
+ }
686
+ const formSchema = yup.object().shape({
687
+ deployHook: yup.string().url("Deploy hook must be a valid URL"),
688
+ deployLimit: yup.number().positive().integer().min(1, "Deploy limit must no less than 1").max(15, "Deploy limit must no higher than 15").typeError("Deploy limit must be a number").required("Deploy limit must be a positive integer between 1 and 15"),
689
+ name: yup.string().required("Name cannot be empty"),
690
+ projectId: yup.string().required("Vercel Project ID cannot be empty"),
691
+ teamId: yup.string(),
692
+ token: yup.string().required("Vercel Account Token cannot be empty")
693
+ }), DialogForm = (props) => {
694
+ const { deploymentTarget, onClose, onCreate, onDelete, onUpdate } = props, client = useSanityClient(), [formState, formStateTransition] = useMachine(formMachine, {
695
+ services: {
696
+ formSubmittedService: async () => {
697
+ onClose();
698
+ },
699
+ // TODO: refactor
700
+ createDocumentService: async (_context, event) => {
701
+ let document;
702
+ try {
703
+ return document = await client.create({
704
+ _id: `vercel.${uuid()}`,
705
+ _type: DEPLOYMENT_TARGET_DOCUMENT_TYPE,
706
+ ...event.formData
707
+ }), onCreate && onCreate(document), Promise.resolve();
708
+ } catch (e) {
709
+ return Promise.reject(e);
710
+ }
711
+ },
712
+ // TODO: refactor
713
+ deleteDocumentService: async () => {
714
+ if (deploymentTarget)
715
+ try {
716
+ return await client.delete(deploymentTarget._id), onDelete && onDelete(deploymentTarget._id), Promise.resolve();
717
+ } catch (e) {
718
+ return Promise.reject(e);
719
+ }
720
+ return Promise.resolve();
721
+ },
722
+ // TODO: refactor
723
+ updateDocumentService: async (_context, event) => {
724
+ let document;
725
+ if (deploymentTarget)
726
+ try {
727
+ return document = await client.patch(deploymentTarget._id).set(event.formData).commit(), onUpdate && onUpdate(document), Promise.resolve();
728
+ } catch (e) {
729
+ return Promise.reject(e);
730
+ }
731
+ return Promise.resolve();
732
+ }
733
+ }
734
+ }), formUpdating = formState.matches("creating") || formState.matches("deleting") || formState.matches("updating"), {
735
+ // Read the formState before render to subscribe the form state through Proxy
736
+ formState: { errors, isDirty, isValid },
737
+ handleSubmit,
738
+ register
739
+ } = useForm({
740
+ defaultValues: {
741
+ deployHook: deploymentTarget?.deployHook || "",
742
+ deployLimit: deploymentTarget?.deployLimit || 5,
743
+ name: deploymentTarget?.name,
744
+ projectId: deploymentTarget?.projectId,
745
+ teamId: deploymentTarget?.teamId || "",
746
+ token: deploymentTarget?.token
747
+ },
748
+ mode: "onChange",
749
+ resolver: yupResolver(formSchema)
750
+ }), onSubmit = async (formData) => {
751
+ const sanitizedFormData = sanitizeFormData(formData);
752
+ await formStateTransition(deploymentTarget ? "UPDATE" : "CREATE", {
753
+ formData: sanitizedFormData
754
+ });
755
+ }, handleDelete = () => {
756
+ formStateTransition("DELETE", { id: deploymentTarget?._id });
757
+ };
758
+ return /* @__PURE__ */ jsx(
759
+ Dialog,
760
+ {
761
+ footer: /* @__PURE__ */ jsx(() => /* @__PURE__ */ jsx(Box, { padding: 3, children: /* @__PURE__ */ jsxs(Flex, { justify: deploymentTarget ? "space-between" : "flex-end", children: [
762
+ deploymentTarget && /* @__PURE__ */ jsx(
763
+ Button,
764
+ {
765
+ disabled: formUpdating,
766
+ fontSize: 1,
767
+ mode: "bleed",
768
+ onClick: handleDelete,
769
+ text: "Delete",
770
+ tone: "critical"
771
+ }
772
+ ),
773
+ /* @__PURE__ */ jsx(
774
+ Button,
775
+ {
776
+ disabled: formUpdating || !isDirty || !isValid,
777
+ fontSize: 1,
778
+ onClick: handleSubmit(onSubmit),
779
+ text: deploymentTarget ? "Update and close" : "Create",
780
+ tone: "primary"
781
+ }
782
+ )
783
+ ] }) }), {}),
784
+ header: `${deploymentTarget ? "Edit" : "Create"} deployment target`,
785
+ id: "create",
786
+ onClose,
787
+ width: 1,
788
+ zOffset: Z_INDEX_DIALOG,
789
+ children: /* @__PURE__ */ jsxs(Box, { as: "form", padding: 4, onSubmit: handleSubmit(onSubmit), children: [
790
+ /* @__PURE__ */ jsx("button", { style: { display: "none" }, tabIndex: -1, type: "submit" }),
791
+ /* @__PURE__ */ jsxs(Stack, { space: 5, children: [
792
+ /* @__PURE__ */ jsx(
793
+ FormFieldInputText,
794
+ {
795
+ disabled: formUpdating,
796
+ description: "Name displayed in this plugin (e.g. production, staging)",
797
+ error: errors?.name,
798
+ label: "Name",
799
+ name: "name",
800
+ ref: register
801
+ }
802
+ ),
803
+ /* @__PURE__ */ jsx(
804
+ FormFieldInputText,
805
+ {
806
+ disabled: formUpdating,
807
+ error: errors?.token,
808
+ label: "Vercel Account Token",
809
+ name: "token",
810
+ ref: register
811
+ }
812
+ ),
813
+ /* @__PURE__ */ jsx(
814
+ FormFieldInputText,
815
+ {
816
+ disabled: formUpdating,
817
+ error: errors?.projectId,
818
+ label: "Vercel Project ID",
819
+ name: "projectId",
820
+ ref: register
821
+ }
822
+ ),
823
+ /* @__PURE__ */ jsx(
824
+ FormFieldInputText,
825
+ {
826
+ description: "Required only if your project is owned by a team account",
827
+ disabled: formUpdating,
828
+ error: errors?.teamId,
829
+ label: "Vercel Team ID (optional)",
830
+ name: "teamId",
831
+ ref: register
832
+ }
833
+ ),
834
+ /* @__PURE__ */ jsx(
835
+ FormFieldInputText,
836
+ {
837
+ description: "Enter a valid deploy hook URL to enable manual deploys",
838
+ disabled: formUpdating,
839
+ error: errors?.deployHook,
840
+ label: "Vercel Deploy Hook (optional)",
841
+ name: "deployHook",
842
+ ref: register
843
+ }
844
+ ),
845
+ /* @__PURE__ */ jsx(
846
+ FormFieldInputText,
847
+ {
848
+ disabled: formUpdating,
849
+ error: errors?.deployLimit,
850
+ label: "Number of deploys to display",
851
+ name: "deployLimit",
852
+ ref: register({ valueAsNumber: !0 })
853
+ }
854
+ )
855
+ ] })
856
+ ] })
857
+ }
858
+ );
859
+ }, dialogMachine = () => Machine(
860
+ {
861
+ context: {
862
+ editDeploymentTarget: void 0
863
+ },
864
+ initial: "idle",
865
+ states: {
866
+ idle: {
867
+ entry: assign({
868
+ editDeploymentTarget: () => {
869
+ }
870
+ }),
871
+ on: {
872
+ CREATE: "create",
873
+ EDIT: {
874
+ actions: ["setEditDeploymentTarget"],
875
+ target: "edit"
876
+ }
877
+ }
878
+ },
879
+ edit: {
880
+ on: {
881
+ CLOSE: "idle"
882
+ }
883
+ },
884
+ create: {
885
+ on: {
886
+ CLOSE: "idle"
887
+ }
888
+ }
889
+ }
890
+ },
891
+ {
892
+ actions: {
893
+ setEditDeploymentTarget: assign((_context, event) => ({
894
+ editDeploymentTarget: event.deploymentTarget
895
+ }))
896
+ }
897
+ }
898
+ ), Widget = () => {
899
+ const client = useSanityClient(), [deploymentTargetListState, deploymentTargetListStateTransition] = useMachine(
900
+ deploymentTargetListMachine,
901
+ {
902
+ services: {
903
+ fetchDataService: () => client.fetch(`*[_type == "${DEPLOYMENT_TARGET_DOCUMENT_TYPE}"] | order(name asc)`).then((result) => result)
904
+ }
905
+ }
906
+ ), [dialogState, dialogStateTransition] = useMachine(dialogMachine), queryClient = new QueryClient({
907
+ defaultOptions: {
908
+ queries: {
909
+ cacheTime: 0,
910
+ staleTime: 0
911
+ }
912
+ }
913
+ }), handleDialogClose = () => {
914
+ dialogStateTransition("CLOSE");
915
+ }, handleDialogShowCreate = () => {
916
+ dialogStateTransition("CREATE");
917
+ }, handleDialogShowEdit = (deploymentTarget) => {
918
+ dialogStateTransition("EDIT", { deploymentTarget });
919
+ }, handleTargetCreate = (deploymentTarget) => {
920
+ deploymentTargetListStateTransition("CREATE", { deploymentTarget });
921
+ }, handleTargetDelete = (id) => {
922
+ deploymentTargetListStateTransition("DELETE", { id });
923
+ }, handleTargetUpdate = (deploymentTarget) => {
924
+ deploymentTargetListStateTransition("UPDATE", { deploymentTarget });
925
+ };
926
+ return /* @__PURE__ */ jsx(ToastProvider, { zOffset: Z_INDEX_TOAST_PROVIDER, children: /* @__PURE__ */ jsxs(QueryClientProvider, { client: queryClient, children: [
927
+ /* @__PURE__ */ jsxs(Card, { radius: 2, style: { overflow: "hidden " }, children: [
928
+ /* @__PURE__ */ jsx(StateDebug, { name: "List", state: deploymentTargetListState }),
929
+ /* @__PURE__ */ jsxs(Flex, { align: "center", justify: "space-between", paddingX: 3, paddingY: 2, children: [
930
+ /* @__PURE__ */ jsx(Text, { size: 5, weight: "semibold", children: "Vercel deployments" }),
931
+ /* @__PURE__ */ jsx(
932
+ Tooltip,
933
+ {
934
+ content: /* @__PURE__ */ jsx(Box, { padding: 2, children: /* @__PURE__ */ jsx(Text, { muted: !0, size: 1, children: "Create new deployment target" }) }),
935
+ placement: "left",
936
+ children: /* @__PURE__ */ jsx(Button, { fontSize: 1, icon: AddIcon, onClick: handleDialogShowCreate, mode: "bleed" })
937
+ }
938
+ )
939
+ ] }),
940
+ /* @__PURE__ */ jsxs(Box, { children: [
941
+ deploymentTargetListState.matches("pending") && /* @__PURE__ */ jsx(Box, { paddingX: 3, paddingY: 4, children: /* @__PURE__ */ jsx(Text, { children: "Loading..." }) }),
942
+ deploymentTargetListState.matches("ready.withoutData") && /* @__PURE__ */ jsx(Box, { paddingX: 3, paddingY: 4, children: /* @__PURE__ */ jsxs(Text, { children: [
943
+ "No deployment targets found.",
944
+ " ",
945
+ /* @__PURE__ */ jsx("a", { onClick: handleDialogShowCreate, style: { cursor: "pointer" }, children: "Create a new target?" })
946
+ ] }) }),
947
+ deploymentTargetListState.matches("ready.withData") && /* @__PURE__ */ jsx(
948
+ DeploymentTargets,
949
+ {
950
+ items: deploymentTargetListState.context.results,
951
+ onDialogEdit: handleDialogShowEdit
952
+ }
953
+ ),
954
+ deploymentTargetListState.matches("failed") && /* @__PURE__ */ jsx(Box, { paddingX: 3, paddingY: 4, children: /* @__PURE__ */ jsx(Text, { children: "Failed to retrieve deployment targets. Please check the developer console log for more information." }) })
955
+ ] })
956
+ ] }),
957
+ dialogState.matches("create") && /* @__PURE__ */ jsx(DialogForm, { onClose: handleDialogClose, onCreate: handleTargetCreate }),
958
+ dialogState.matches("edit") && /* @__PURE__ */ jsx(
959
+ DialogForm,
960
+ {
961
+ deploymentTarget: dialogState.context.editDeploymentTarget,
962
+ onClose: handleDialogClose,
963
+ onDelete: handleTargetDelete,
964
+ onUpdate: handleTargetUpdate
965
+ }
966
+ )
967
+ ] }) });
968
+ };
969
+ TimeAgo.addDefaultLocale(en);
970
+ function vercelWidget(config = {}) {
971
+ return {
972
+ name: "vercel",
973
+ component: Widget,
974
+ layout: config.layout ?? { width: "full" }
975
+ };
976
+ }
977
+ export {
978
+ vercelWidget
979
+ };
980
+ //# sourceMappingURL=index.mjs.map