Skip to content

Commit f7bda48

Browse files
committed
chore: upload button, work with zero actions
1 parent 42f657a commit f7bda48

File tree

9 files changed

+233
-46
lines changed

9 files changed

+233
-46
lines changed

pkg/overlay/validate.go

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,13 @@ func (o *Overlay) Validate() error {
4343
}
4444
}
4545

46-
if len(o.Actions) == 0 {
47-
errs = append(errs, fmt.Errorf("overlay must define at least one action"))
48-
} else {
49-
for i, action := range o.Actions {
50-
if action.Target == "" {
51-
errs = append(errs, fmt.Errorf("overlay action at index %d target must be defined", i))
52-
}
53-
54-
if action.Remove && !action.Update.IsZero() {
55-
errs = append(errs, fmt.Errorf("overlay action at index %d should not both set remove and define update", i))
56-
}
46+
for i, action := range o.Actions {
47+
if action.Target == "" {
48+
errs = append(errs, fmt.Errorf("overlay action at index %d target must be defined", i))
49+
}
50+
51+
if action.Remove && !action.Update.IsZero() {
52+
errs = append(errs, fmt.Errorf("overlay action at index %d should not both set remove and define update", i))
5753
}
5854
}
5955

web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"@radix-ui/react-dropdown-menu": "^2.1.4",
1717
"@radix-ui/react-progress": "^1.1.1",
1818
"@radix-ui/react-slot": "^1.1.1",
19-
"@speakeasy-api/moonshine": "^0.71.0",
19+
"@speakeasy-api/moonshine": "^0.87.0",
2020
"class-variance-authority": "^0.7.1",
2121
"clsx": "^2.1.1",
2222
"jotai": "^2.11.0",

web/pnpm-lock.yaml

Lines changed: 75 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/src/Playground.tsx

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,10 @@ import { Editor } from "./components/Editor";
1111
import { editor, MarkerSeverity } from "monaco-editor";
1212
import { ApplyOverlay, CalculateOverlay, GetInfo } from "./bridge";
1313
import { Alert } from "@speakeasy-api/moonshine";
14-
import { blankOverlay, petstore } from "./defaults";
14+
import { blankOverlay, emptyOverlay, petstore } from "./defaults";
1515
import speakeasyWhiteLogo from "./assets/speakeasy-white.svg";
1616
import openapiLogo from "./assets/openapi.svg";
1717
import { compress, decompress } from "@/compress";
18-
import { Button } from "@/components/ui/button";
1918
import {
2019
ImperativePanelGroupHandle,
2120
Panel,
@@ -24,6 +23,8 @@ import {
2423
} from "react-resizable-panels";
2524
import posthog from "posthog-js";
2625
import { useDebounceCallback, useMediaQuery } from "usehooks-ts";
26+
import FileUpload from "@/components/FileUpload";
27+
import ShareButton from "@/components/ShareButton";
2728
import {
2829
arraysEqual,
2930
formatDocument,
@@ -66,6 +67,7 @@ function Playground() {
6667
const [ready, setReady] = useState(false);
6768

6869
const original = useRef(petstore);
70+
const implicitShare = useRef(false);
6971
const originalLang = useRef<DocumentLanguage>("yaml");
7072
const changed = useRef("");
7173
const [changedLoading, setChangedLoading] = useState(false);
@@ -185,9 +187,11 @@ function Playground() {
185187
currentUrl.hash = "";
186188
currentUrl.searchParams.set("s", base64Data);
187189

188-
lastSharedStart.current = start;
189-
shareDialogRef.current.setUrl(currentUrl.toString());
190-
shareDialogRef.current.setOpen(true);
190+
if (!implicitShare.current) {
191+
lastSharedStart.current = start;
192+
shareDialogRef.current.setUrl(currentUrl.toString());
193+
shareDialogRef.current.setOpen(true);
194+
}
191195

192196
history.pushState(null, "", currentUrl.toString());
193197
posthog.capture("overlay.speakeasy.com:share", {
@@ -331,6 +335,25 @@ function Playground() {
331335
panelGroup.setLayout(desiredWidths);
332336
}, []);
333337

338+
const onFileUpload = useCallback(
339+
async (content: string) => {
340+
setResultLoading(true);
341+
implicitShare.current = true;
342+
original.current = content;
343+
changed.current = content;
344+
result.current = emptyOverlay;
345+
await getShareUrl();
346+
setResultLoading(false);
347+
setOverlayMarkers([]);
348+
},
349+
[original, changed, result],
350+
);
351+
352+
const clickShareButton = useCallback(async () => {
353+
implicitShare.current = false;
354+
await getShareUrl();
355+
}, [getShareUrl, implicitShare]);
356+
334357
if (!ready) {
335358
return "";
336359
}
@@ -403,26 +426,12 @@ function Playground() {
403426
</Link>
404427
</span>
405428
</div>
406-
<div className="flex gap-x-2 justify-end">
407-
<Button
408-
className="border-b border-transparent hover:border-current"
409-
style={{
410-
color: "#FBE331",
411-
backgroundColor: "#1E1E1E",
412-
}}
413-
onClick={getShareUrl}
414-
disabled={shareUrlLoading}
415-
>
416-
{shareUrlLoading ? (
417-
<Loader2Icon
418-
className="animate-spin"
419-
style={{ height: "75%" }}
420-
/>
421-
) : (
422-
<ShareIcon style={{ height: "75%" }} />
423-
)}
424-
Share
425-
</Button>
429+
<div className="flex gap-x-2 justify-evenly ">
430+
<FileUpload onFileUpload={onFileUpload} />
431+
<ShareButton
432+
onClick={clickShareButton}
433+
loading={shareUrlLoading}
434+
/>
426435
<ShareDialog ref={shareDialogRef} />
427436
</div>
428437
</div>

web/src/assets/wasm/lib.wasm

-302 KB
Binary file not shown.

web/src/components/CopyButton.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ export function CopyButton({
4141
<Button
4242
size="icon"
4343
variant={variant}
44-
className={cn("flex grow relative border-none", className)}
44+
className={cn(
45+
"flex grow relative border-none bg-foreground/5 hover:bg-foreground/10 text-foreground/80 cursor-pointer select-none flex-row items-center gap-1.5 whitespace-nowrap rounded-md border px-2.5 py-2 text-sm tracking-tight",
46+
className,
47+
)}
4548
onClick={() => {
4649
copyToClipboardWithMeta(value);
4750
setHasCopied(true);

web/src/components/FileUpload.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { useCallback, useRef } from "react";
2+
import { Icon } from "@speakeasy-api/moonshine";
3+
import type { Attachment } from "@speakeasy-api/moonshine/dist/components/PromptInput";
4+
import { cn } from "@/lib/utils";
5+
6+
export default function FileUpload(props: {
7+
onFileUpload: (content: string) => void;
8+
}) {
9+
const fileInputRef = useRef<HTMLInputElement>(null);
10+
11+
const handleFileUpload = useCallback(
12+
(files: Attachment[]) => {
13+
if (files.length > 0) {
14+
// Convert bytes to string
15+
const buffer = new Uint8Array(files[0].bytes);
16+
const content = new TextDecoder().decode(buffer);
17+
props.onFileUpload(content);
18+
}
19+
},
20+
[props.onFileUpload],
21+
);
22+
23+
const handleFiles = useCallback(
24+
async (files: File[]) => {
25+
const attachments: Attachment[] = await Promise.all(
26+
files.map(async (file) => ({
27+
id: crypto.randomUUID(),
28+
name: file.name,
29+
type: file.type,
30+
size: file.size,
31+
bytes: await file.arrayBuffer(),
32+
})),
33+
);
34+
handleFileUpload(attachments);
35+
},
36+
[handleFileUpload],
37+
);
38+
39+
return (
40+
<div className="mt-4 flex flex-shrink flex-row flex-wrap gap-3 items-end">
41+
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions,jsx-a11y/click-events-have-key-events */}
42+
<div
43+
key={"Upload file"}
44+
className={cn(
45+
"bg-foreground/5 hover:bg-foreground/10 text-foreground/80 relative flex cursor-pointer select-none flex-row items-center gap-1.5 whitespace-nowrap rounded-md border px-2.5 py-2 text-sm tracking-tight",
46+
)}
47+
onClick={() => fileInputRef.current?.click()}
48+
>
49+
<input
50+
type="file"
51+
ref={fileInputRef}
52+
className="absolute inset-0 hidden h-full w-full"
53+
onChange={(e) => handleFiles(Array.from(e.target.files ?? []))}
54+
accept={[
55+
"application/yaml",
56+
"application/x-yaml",
57+
"application/json",
58+
].join(",")}
59+
/>
60+
<Icon
61+
name={"paperclip"}
62+
className={cn("stroke-primary relative size-4", "stroke-emerald-400")}
63+
strokeWidth={1}
64+
/>
65+
Upload file
66+
</div>
67+
</div>
68+
);
69+
}

web/src/components/ShareButton.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Icon } from "@speakeasy-api/moonshine";
2+
import { cn } from "@/lib/utils";
3+
import { Loader2Icon } from "lucide-react";
4+
5+
export default function ShareButton(props: {
6+
onClick: () => void;
7+
loading: boolean;
8+
}) {
9+
return (
10+
<div className="mt-4 flex flex-shrink flex-row flex-wrap gap-3 items-stretch">
11+
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
12+
<div
13+
key={"Share"}
14+
className={cn(
15+
"bg-foreground/5 hover:bg-foreground/10 text-foreground/80 relative flex cursor-pointer select-none flex-row items-center gap-1.5 whitespace-nowrap rounded-md border px-2.5 py-2 text-sm tracking-tight",
16+
props.loading && "cursor-not-allowed",
17+
)}
18+
onClick={props.loading ? undefined : props.onClick}
19+
>
20+
{props.loading ? (
21+
<Loader2Icon className="animate-spin" style={{ height: "75%" }} />
22+
) : (
23+
<Icon
24+
name={"share"}
25+
className={cn(
26+
"stroke-primary relative size-4",
27+
"stroke-emerald-400",
28+
)}
29+
strokeWidth={1}
30+
/>
31+
)}
32+
Share
33+
</div>
34+
</div>
35+
);
36+
}

0 commit comments

Comments
 (0)