Skip to content

Commit

Permalink
feat: visual editor
Browse files Browse the repository at this point in the history
  • Loading branch information
yongenaelf committed Aug 15, 2024
1 parent 215a2ae commit adb5c8a
Show file tree
Hide file tree
Showing 11 changed files with 631 additions and 0 deletions.
27 changes: 27 additions & 0 deletions app/visual-editor/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {
ResizablePanelGroup,
ResizablePanel,
ResizableHandle,
} from "@/components/ui/resizable";
import { VisualEditorProvider } from "@/components/visual/context";
import { Preview } from "@/components/visual/preview";
import { SidePanel } from "@/components/visual/side-panel";

export default function Page() {
return (
<div className="container px-4 py-12 md:px-6 lg:py-16">
<h1 className="text-2xl mb-2">Visual Editor</h1>
<VisualEditorProvider>
<ResizablePanelGroup direction="horizontal">
<ResizablePanel defaultSize={40} className="pr-3">
<SidePanel />
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={60}>
<Preview />
</ResizablePanel>
</ResizablePanelGroup>
</VisualEditorProvider>
</div>
);
}
1 change: 1 addition & 0 deletions components/top-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default function TopMenu() {
{ href: "/workspaces", children: "Workspaces" },
{ href: "/tutorials", children: "Tutorials" },
{ href: "/deployments", children: "Deployments" },
{ href: "/visual-editor", children: "Visual Editor" },
{ href: "https://github.com/AElfProject", children: "GitHub" },
];

Expand Down
55 changes: 55 additions & 0 deletions components/ui/tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use client"

import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"

import { cn } from "@/lib/utils"

const Tabs = TabsPrimitive.Root

const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName

const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName

const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName

export { Tabs, TabsList, TabsTrigger, TabsContent }
52 changes: 52 additions & 0 deletions components/visual/context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"use client";

import {
createContext,
Dispatch,
PropsWithChildren,
SetStateAction,
useContext,
useState,
} from "react";

interface IMethods {
name: string;
type: string;
}

interface IViewMethods extends IMethods {
outputType: string;
}

interface IVisualEditorState {
name: string;
viewMethods: IViewMethods[];
sendMethods: IMethods[];
}

const defaultState: IVisualEditorState = {
name: "Hello World",
viewMethods: [],
sendMethods: [],
};

const VisualEditorContext = createContext<{
state: IVisualEditorState;
setState: Dispatch<SetStateAction<IVisualEditorState>>;
}>({ state: defaultState, setState: () => {} });

const VisualEditorProvider = ({ children }: PropsWithChildren) => {
const [state, setState] = useState<IVisualEditorState>(defaultState);

return (
<VisualEditorContext.Provider value={{ state, setState }}>
{children}
</VisualEditorContext.Provider>
);
};

const useVisualEditorContext = () => {
return useContext(VisualEditorContext);
};

export { VisualEditorProvider, useVisualEditorContext };
31 changes: 31 additions & 0 deletions components/visual/input-output-options.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
} from "@/components/ui/select";

export function InputOutputOptions({ options = [] }: { options?: string[] }) {
return (
<SelectContent>
{options.length > 0 ? (
<SelectGroup>
<SelectLabel>Custom</SelectLabel>
{options.map((i) => (
<SelectItem key={i} value={i}>
{i}
</SelectItem>
))}
</SelectGroup>
) : null}
<SelectGroup>
<SelectLabel>Google Protobuf</SelectLabel>
{["empty", "timestamp"].map((i) => (
<SelectItem key={i} value={i}>
{i}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
);
}
71 changes: 71 additions & 0 deletions components/visual/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"use client";

import CodeMirror from "@uiw/react-codemirror";
import { useVisualEditorContext } from "./context";
import { useTheme } from "next-themes";
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
import { StreamLanguage } from "@codemirror/language";
import { protobuf } from "@codemirror/legacy-modes/mode/protobuf";
import { useMemo } from "react";

const getType = (value: string) => {
switch (value) {
case "empty":
return "google.protobuf.Empty";
case "timestamp":
return "google.protobuf.Timestamp";
}

return value;
};

export function Preview() {
const { state } = useVisualEditorContext();
const { theme, systemTheme } = useTheme();

const currentTheme = theme !== "system" ? theme : systemTheme;
const editorTheme = currentTheme === "light" ? githubLight : githubDark;

const value = useMemo(() => {
const name = state.name.split(" ").join("") || "HelloWorld";

return `syntax = "proto3";
import "aelf/core.proto";
import "aelf/options.proto";
import "google/protobuf/empty.proto";
import "Protobuf/reference/acs12.proto";
import public "google/protobuf/timestamp.proto";
option csharp_namespace = "AElf.Contracts.${name}";
service ${name} {
option (aelf.csharp_state) = "AElf.Contracts.${name}.${name}State";
option (aelf.base) = "Protobuf/reference/acs12.proto";
${state.sendMethods
.map(
(i) => `
rpc ${i.name}(${getType(i.type)}) returns (google.protobuf.Empty);`
)
.join("\n")}
${state.viewMethods
.map(
(i) => `
rpc ${i.name} (${getType(i.type)}) returns (${getType(i.outputType)}) {
option (aelf.is_view) = true;
}`
)
.join("\n")}
}`;
}, [state]);

return (
<CodeMirror
value={value}
theme={editorTheme}
readOnly={true}
editable={false}
extensions={[StreamLanguage.define(protobuf)]}
/>
);
}
133 changes: 133 additions & 0 deletions components/visual/send-method-editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"use client";

import { useForm, useFieldArray, useWatch } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { useEffect } from "react";
import { z } from "zod";
import { Plus, Trash2 } from "lucide-react";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Select, SelectTrigger, SelectValue } from "@/components/ui/select";
import { InputOutputOptions } from "./input-output-options";
import clsx from "clsx";

const schema = z.object({
method: z.array(
z.object({
name: z
.string({ required_error: "Name is required." })
.trim()
.min(1, "Name is required."),
type: z.string(),
})
),
});

type FormValues = z.infer<typeof schema>;

export function SendMethodEditor({
value = [],
onChange,
}: {
value?: FormValues["method"];
onChange?: (val: FormValues["method"]) => void;
}) {
const form = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
method: value,
},
mode: "onBlur",
});
const { control } = form;

const { fields, append, remove } = useFieldArray({
name: "method",
control,
});

const { method } = useWatch({ control });

useEffect(() => {
try {
const value = schema.parse({ method });
if (!!onChange) onChange(value?.method || []);
} catch (err) {
// console.log(err);
}
}, [method]);

return (
<Form {...form}>
{fields.map((field, index) => {
return (
<section key={field.id} className="flex gap-2">
<FormField
control={control}
name={`method.${index}.name`}
render={({ field }) => (
<FormItem className="flex-1">
{index === 0 ? <FormLabel>Name</FormLabel> : null}
<FormControl>
<Input placeholder="name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name={`method.${index}.type`}
render={({ field }) => (
<FormItem className="flex-1">
{index === 0 ? <FormLabel>Input</FormLabel> : null}
<FormControl>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select an input type" />
</SelectTrigger>
<InputOutputOptions />
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="button"
variant="ghost"
onClick={() => remove(index)}
className={clsx({ "mt-8": index === 0 })}
>
<Trash2 className="w-4 h-4" />
</Button>
</section>
);
})}

<Button
variant="ghost"
type="button"
onClick={() =>
append({
name: "test",
type: "empty",
})
}
>
<Plus className="w-4 h-4" />
</Button>
</Form>
);
}
Loading

0 comments on commit adb5c8a

Please sign in to comment.