Skip to content

Commit

Permalink
feat: Actions menu wip + initial template file tab
Browse files Browse the repository at this point in the history
  • Loading branch information
astahmer committed Nov 11, 2022
1 parent 8bc4679 commit 270d7ab
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 69 deletions.
1 change: 0 additions & 1 deletion playground/src/components/SplitPane/SplitPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ export function SplitPane({
role="presentation"
userSelect="none"
data-pane-resizer
zIndex={1}
minHeight={0}
/>
<Box
Expand Down
168 changes: 103 additions & 65 deletions playground/src/routes/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@ import {
Menu,
MenuButton,
MenuItem,
MenuItemOption,
MenuList,
MenuOptionGroup,
ModalFooter,
Popover,
PopoverBody,
PopoverContent,
PopoverTrigger,
Tab,
TabList,
Tabs,
Expand All @@ -29,12 +35,11 @@ import { editor } from "monaco-editor";
import type { TemplateContextOptions } from "openapi-zod-client";
import { getHandlebars, getZodClientTemplateContext, maybePretty } from "openapi-zod-client";
import { removeAtIndex, safeJSONParse, updateAtIndex } from "pastable";
import { MouseEventHandler, useDeferredValue, useEffect, useMemo, useRef, useState } from "react";
import { useDeferredValue, useEffect, useMemo, useRef, useState } from "react";
import { parse } from "yaml";
import { default as petstoreYaml } from "../../../examples/petstore.yaml?raw";
import { default as baseOutputTemplate } from "../../../lib/src/template.hbs?raw";
import { defaultOptionValues, OptionsForm } from "../components/OptionsForm";
import { SplitPane } from "../components/SplitPane/SplitPane";
import { presets } from "./presets";

// TODO: Add a way to pass in a custom template.
// template context explorer
Expand All @@ -43,6 +48,9 @@ import { SplitPane } from "../components/SplitPane/SplitPane";
// localStorage persistence for input
// TODO diff editor + collect warnings
// test with json input
// Save/share = generate link like ts playground
// display openapi-zod-client version
// use extension to determine input type (json|yaml = openapi doc, hbs = template)

const useOpenApiZodClient = (input: string, options: TemplateContextOptions) => {
const deferredInput = useDeferredValue(input);
Expand All @@ -58,7 +66,13 @@ const useOpenApiZodClient = (input: string, options: TemplateContextOptions) =>

const ctx = useMemo(() => {
if (!openApiDoc) return;
return getZodClientTemplateContext(openApiDoc, options);
const ctx = getZodClientTemplateContext(openApiDoc, options);
// logs the template context to the browser console so users can explore it
if (typeof window !== "undefined") {
console.log(ctx);
}

return ctx;
}, [openApiDoc, options]);

return useMemo(() => {
Expand All @@ -67,7 +81,8 @@ const useOpenApiZodClient = (input: string, options: TemplateContextOptions) =>
const groupStrategy = options?.groupStrategy ?? "none";

const hbs = getHandlebars();
const template = hbs.compile(baseOutputTemplate);
// TODO select template
const template = hbs.compile(presets.defaultTemplate);

const output = template({ ...ctx, options: { ...options, apiClientName: options?.apiClientName ?? "api" } });
return maybePretty(output, {
Expand All @@ -82,17 +97,28 @@ const useOpenApiZodClient = (input: string, options: TemplateContextOptions) =>
}, [ctx, options]);
};

const initialInputList: FileTab[] = [
{ name: "api.doc.yaml", content: presets.defaultInput, index: 0, isPreset: true },
{ name: "template.hbs", content: presets.defaultTemplate, index: 1, isPreset: true },
];
const isValidInputName = (name: string) => name.endsWith(".yml") || name.endsWith(".yaml") || name.endsWith(".json");

export const Playground = () => {
const [options, setOptions] = useState<Partial<TemplateContextOptions>>({});
const [previewOptions, setPreviewOptions] = useState<Partial<TemplateContextOptions & { booleans: string[] }>>({});
const [optionsFormKey, setOptionsFormKey] = useState(0);

const [activeInputTab, setActiveInputTab] = useState("openapi.doc.yaml");
const [inputList, setInputList] = useState<FileTab[]>([{ name: activeInputTab, content: petstoreYaml, index: 0 }]);
const [activeInputTab, setActiveInputTab] = useState(initialInputList[0].name);
const [inputList, setInputList] = useState<FileTab[]>(initialInputList);

const inputIndex = inputList.findIndex((tab) => tab.name === activeInputTab);
const activeIndex = inputList.findIndex((tab) => tab.name === activeInputTab);
const inputIndex = isValidInputName(inputList[activeIndex].name)
? activeIndex
: inputList.findIndex((tab) => isValidInputName(tab.name));
const input = inputList[inputIndex]?.content ?? "";
const output = useOpenApiZodClient(input, options);

// update output editor preview value whenever output is re-computed
useEffect(() => {
outputEditorRef.current?.setValue(output);
}, [output]);
Expand All @@ -111,7 +137,7 @@ export const Playground = () => {
const outputEditorRef = useRef<editor.IStandaloneCodeEditor>();

return (
<Flex flexDirection="column" h="100%" pos="relative">
<Flex h="100%" pos="relative">
<Box display="flex" boxSize="100%">
<SplitPane
defaultSize="50%"
Expand All @@ -123,7 +149,8 @@ export const Playground = () => {
}}
>
<Box h="100%" flexGrow={1}>
<Tabs variant="line" size="sm" h="42px" index={inputIndex}>
<Tabs variant="line" size="sm" h="42px" index={activeIndex}>
{/* TODO cursor pointer + onDoubleClick = create file action */}
<TabList
pb="2"
className="scrollbar"
Expand All @@ -133,23 +160,12 @@ export const Playground = () => {
scrollSnapAlign="start"
>
{inputList.map((file, index) => {
const openEditForm: MouseEventHandler = (e) => {
e.stopPropagation();
setFormModalDefaultValues(file);
formModal.onOpen();
};

return (
<Tab
key={file.name}
display="flex"
alignItems="center"
onClick={(e) => {
setActiveInputTab(file.name);
// if (file.name === activeInputTab) {
// openEditForm(e);
// }
}}
onClick={() => setActiveInputTab(file.name)}
border="none"
_selected={{ bg: "gray.100", fontWeight: "bold" }}
borderRadius="md"
Expand All @@ -165,10 +181,16 @@ export const Playground = () => {
padding="0"
borderRadius="0"
minWidth="0"
onClick={openEditForm}
onClick={(e) => {
if (file.isPreset) return;
e.stopPropagation();
setFormModalDefaultValues(file);
formModal.onOpen();
}}
backgroundColor="gray.400"
visibility="hidden"
_groupHover={{ visibility: "visible" }}
isDisabled={file.isPreset}
/>
<Button
as="div"
Expand All @@ -195,35 +217,6 @@ export const Playground = () => {
</Tab>
);
})}
<Menu>
<MenuButton
as={Button}
flexShrink={0}
ml="auto"
mr="4"
size="sm"
variant="outline"
rightIcon={<Box className="i-mdi-chevron-down" boxSize="1.25em" />}
>
Actions
</MenuButton>
<MenuList>
<MenuItem
onClick={() => {
setFormModalDefaultValues({
name: "",
content: "",
index: inputList.length,
});
formModal.onOpen();
}}
>
Create input file
</MenuItem>
<MenuItem>Select handlebars template</MenuItem>
<MenuItem>Use OpenAPI samples</MenuItem>
</MenuList>
</Menu>
</TabList>
</Tabs>
<Editor
Expand Down Expand Up @@ -252,16 +245,59 @@ export const Playground = () => {
{file.name}
</Tab>
))}
<Button
variant="outline"
ml="auto"
mr="4"
mb="2"
size="sm"
onClick={optionsDrawer.onOpen}
>
Edit options
</Button>
<Menu>
<MenuButton
as={Button}
flexShrink={0}
ml="auto"
mr="4"
size="sm"
variant="outline"
rightIcon={<Box className="i-mdi-chevron-down" boxSize="1.25em" />}
>
Actions
</MenuButton>
<MenuList>
<MenuItem
onClick={() => {
setFormModalDefaultValues({
name: "",
content: "",
index: inputList.length,
});
formModal.onOpen();
}}
>
Create file
</MenuItem>
<Popover trigger="hover" placement="left" closeOnBlur={false}>
<PopoverTrigger>
<MenuItem>Select handlebars template</MenuItem>
</PopoverTrigger>
<PopoverContent>
<PopoverBody>
<MenuOptionGroup
defaultValue="default"
title="Template"
type="radio"
>
<MenuItemOption value="default">
Default (zodios)
</MenuItemOption>
<MenuItemOption value="default-grouped">
Grouped (zodios)
</MenuItemOption>
<MenuItemOption value="schemas-only">
Schemas only (& types if circular)
</MenuItemOption>
</MenuOptionGroup>
</PopoverBody>
</PopoverContent>
</Popover>
<MenuItem>Use OpenAPI samples</MenuItem>
<MenuItem onClick={optionsDrawer.onOpen}>Edit options</MenuItem>
</MenuList>
</Menu>
</TabList>
</Tabs>
<Editor
Expand Down Expand Up @@ -320,7 +356,7 @@ export const Playground = () => {
<Field name="content" type="textarea" label="Content" rows={14} />
</FormLayout>
</FormDialog>
<Drawer isOpen={optionsDrawer.isOpen} onClose={optionsDrawer.onClose} size="lg">
<Drawer isOpen={optionsDrawer.isOpen} onClose={optionsDrawer.onClose} size="lg" placement="left">
<DrawerOverlay />
<DrawerContent>
<DrawerCloseButton />
Expand All @@ -333,6 +369,7 @@ export const Playground = () => {
onClick={() => {
setPreviewOptions(defaultOptionValues);
setOptions(defaultOptionValues);
setOptionsFormKey((key) => key + 1);
}}
>
Reset
Expand All @@ -348,6 +385,7 @@ export const Playground = () => {
<SplitPane direction="column" defaultSize="50%">
<Box height="100%" overflow="auto">
<OptionsForm
key={optionsFormKey}
id="options-form"
mb="4"
onChange={setPreviewOptions}
Expand Down Expand Up @@ -406,7 +444,7 @@ const optionNameToCliOptionName = {
defaultStatusBehavior: "--default-status",
} as const;

type FileTab = { name: string; content: string; index: number };
type FileTab = { name: string; content: string; index: number; isPreset?: boolean };

const createPnpmCommand = (outputPath: string, relevantOptions: TemplateContextOptions) => {
return `pnpx openapi-zod-client ./petstore.yaml -o ./${outputPath}
Expand Down Expand Up @@ -443,7 +481,7 @@ const CreateFileFormFooter = () => {
<Button variant="ghost" mr={3} onClick={modal.onClose}>
Cancel
</Button>
<Button variant="outline" onClick={() => form.setValue("content", petstoreYaml)}>
<Button variant="outline" onClick={() => form.setValue("content", presets.defaultInput)}>
Use petstore
</Button>
<Button type="submit">Save file</Button>
Expand Down
19 changes: 16 additions & 3 deletions playground/src/routes/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box, Stack } from "@chakra-ui/react";
import { Box, Code, Stack } from "@chakra-ui/react";
import { Head, Layout, Link, StyledLink } from "rakkasjs";

import "./layout.css";
Expand All @@ -18,8 +18,17 @@ const MainLayout: Layout = ({ children }) => {
py="5"
px="4"
>
<Box as={Link} href="/" fontWeight="bold" fontSize={"2xl"}>
openapi-zod-client playground
<Box fontWeight="bold" fontSize={"2xl"}>
<Code
variant="solid"
fontSize="2xl"
as={Link}
href="https://github.com/astahmer/openapi-zod-client/"
rel="external"
target="_blank"
>
openapi-zod-client
</Code>
</Box>
<Stack direction="row">
<Box
Expand Down Expand Up @@ -47,6 +56,8 @@ const MainLayout: Layout = ({ children }) => {
<Box
as={StyledLink}
href="https://github.com/astahmer/openapi-zod-client/"
rel="external"
target="_blank"
px="2"
py="1"
borderRadius="md"
Expand All @@ -57,6 +68,8 @@ const MainLayout: Layout = ({ children }) => {
<Box
as={StyledLink}
href="https://www.zodios.org/"
rel="external"
target="_blank"
px="2"
py="1"
borderRadius="md"
Expand Down
9 changes: 9 additions & 0 deletions playground/src/routes/presets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { default as defaultOutputTemplate } from "../../../lib/src/templates/default.hbs?raw";
import { default as petstoreYaml } from "../../../examples/petstore.yaml?raw";

export const presets = {
defaultTemplate: defaultOutputTemplate,
defaultInput: petstoreYaml,
getTemplates: () => import.meta.glob("../../../lib/src/template/*.hbs", { as: "raw" }),
getSamples: () => import.meta.glob("../../../samples/**/*.hbs", { as: "raw" }),
};

0 comments on commit 270d7ab

Please sign in to comment.