Skip to content

Commit

Permalink
Merge pull request #101 from gridaco/forms
Browse files Browse the repository at this point in the history
Grida Forms Playground
  • Loading branch information
softmarshmallow committed May 13, 2024
2 parents 8c29488 + fae53d0 commit d611b93
Show file tree
Hide file tree
Showing 52 changed files with 8,875 additions and 7,359 deletions.
2 changes: 1 addition & 1 deletion apps/forms/app/(api)/private/editor/ai/schema/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ ${interface_txt}

try {
const response = await openai.chat.completions.create({
model: "gpt-4-1106-preview",
model: "gpt-4-turbo-2024-04-09",
messages: [
{
role: "system",
Expand Down
4 changes: 2 additions & 2 deletions apps/forms/app/(api)/private/editor/fields/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
createRouteHandlerClient,
} from "@/lib/supabase/server";
import { GridaCommerceClient } from "@/services/commerce";
import { FormFieldDataSchema, FormFieldType, PaymentFieldData } from "@/types";
import { FormFieldDataSchema, FormInputType, PaymentFieldData } from "@/types";
import { FormFieldUpsert } from "@/types/private/api";
import assert from "assert";
import { cookies } from "next/headers";
Expand Down Expand Up @@ -194,7 +194,7 @@ function safe_data_field({
type,
data,
}: {
type: FormFieldType;
type: FormInputType;
data?: FormFieldDataSchema;
}): FormFieldDataSchema | undefined | null {
switch (type) {
Expand Down
16 changes: 12 additions & 4 deletions apps/forms/app/(api)/v1/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { is_uuid_v4 } from "@/utils/is";
import i18next from "i18next";
import { cookies } from "next/headers";
import { notFound } from "next/navigation";
import { FormRenderer, type ClientRenderBlock } from "@/lib/forms";
import { FormRenderTree, type ClientRenderBlock } from "@/lib/forms";
import type { FormFieldDefinition, FormPage, Option } from "@/types";

export const revalidate = 0;
Expand Down Expand Up @@ -167,6 +167,7 @@ export async function GET(

const {
title,
description,
default_page,
fields,
is_powered_by_branding_enabled,
Expand Down Expand Up @@ -227,9 +228,16 @@ export async function GET(
};
}

const renderer = new FormRenderer(id, fields, page_blocks, {
option_renderer: mkoption,
});
const renderer = new FormRenderTree(
id,
title,
description,
fields,
page_blocks,
{
option_renderer: mkoption,
}
);

const required_hidden_fields = fields.filter(
(f) => f.type === "hidden" && f.required
Expand Down
1 change: 0 additions & 1 deletion apps/forms/app/(d)/d/[id]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { cookies } from "next/headers";
import { createServerComponentClient } from "@/lib/supabase/server";
import { GridaLogo } from "@/components/grida-logo";
import { EyeOpenIcon, SlashIcon } from "@radix-ui/react-icons";
import { Toaster } from "react-hot-toast";
import { Tabs } from "@/scaffolds/d/tabs";
import { FormEditorProvider } from "@/scaffolds/editor";
import type { Metadata } from "next";
Expand Down
3 changes: 3 additions & 0 deletions apps/forms/app/(dev)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { ThemeProvider } from "@/components/theme-provider";
import { Toaster } from "react-hot-toast";

import "../editor.css";

const inter = Inter({ subsets: ["latin"] });
Expand All @@ -17,6 +19,7 @@ export default function RootLayout({
return (
<html lang="en">
<body className={inter.className}>
<Toaster />
<ThemeProvider
attribute="class"
defaultTheme="system"
Expand Down
40 changes: 40 additions & 0 deletions apps/forms/app/(dev)/playground/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { createServerComponentClient } from "@/lib/supabase/server";
import { cookies } from "next/headers";
import { notFound, redirect } from "next/navigation";
import Playground from "@/scaffolds/playground";

export default async function SharedPlaygroundPage({
params,
}: {
params: {
slug: string;
};
}) {
const { slug } = params;
const cookieStore = cookies();
const supabase = createServerComponentClient(cookieStore);

const { data: _gist } = await supabase
.from("gist")
.select()
.eq("slug", slug)
.single();

if (!_gist) {
return redirect("/playground");
}

const { data, prompt } = _gist;

return (
<main>
<Playground
initial={{
src: (data as any)?.["form.json"],
prompt: prompt || undefined,
slug: slug,
}}
/>
</main>
);
}
258 changes: 3 additions & 255 deletions apps/forms/app/(dev)/playground/page.tsx
Original file line number Diff line number Diff line change
@@ -1,261 +1,9 @@
"use client";

import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { FormView } from "@/scaffolds/e/form";
import { Editor as MonacoEditor, useMonaco } from "@monaco-editor/react";
import { useEffect, useMemo, useState } from "react";
import { nanoid } from "nanoid";
import { JSONForm } from "@/types/schema";
import resources from "@/k/i18n";
import Ajv from "ajv";
import { FormRenderer } from "@/lib/forms";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { GridaLogo } from "@/components/grida-logo";
import { FormFieldAutocompleteType } from "@/types";

const HOST = process.env.NEXT_PUBLIC_HOST_NAME || "http://localhost:3000";

const examples = [
{
id: "001-hello-world",
name: "Hello World",
template: {
schema: {
src: `${HOST}/schema/examples/001-hello-world/form.json`,
},
},
},
{
id: "002-iphone-pre-order",
name: "iPhone Pre-Order",
template: {
schema: {
src: `${HOST}/schema/examples/002-iphone-pre-order/form.json`,
},
},
},
] as const;

type MaybeArray<T> = T | T[];

function toArrayOf<T>(value: MaybeArray<T>, nofalsy = true): NonNullable<T>[] {
return (
Array.isArray(value) ? value : nofalsy && value ? [value] : []
) as NonNullable<T>[];
}

function parse(txt?: string): JSONForm | null {
try {
return txt ? JSON.parse(txt) : null;
} catch (error) {
return null;
}
}

function compile(txt?: string) {
const schema = parse(txt);
if (!schema) {
return;
}

const renderer = new FormRenderer(
nanoid(),
schema.fields?.map((f, i) => ({
...f,
id: f.name,
autocomplete: toArrayOf<FormFieldAutocompleteType | undefined>(
f.autocomplete
),
required: f.required || false,
local_index: i,
options:
f.options?.map((o) => ({
...o,
id: o.value,
})) || [],
})) || [],
[]
);

return renderer;
}
import Playground from "@/scaffolds/playground";

export default function FormsPlayground() {
const [action, setAction] = useState<string>("");
const [method, setMethod] = useState<string>("get");
const [exampleId, setExampleId] = useState<string>(examples[0].id);
const [__schema_txt, __set_schema_txt] = useState<string | undefined>();

const renderer: FormRenderer | undefined = useMemo(
() => compile(__schema_txt),
[__schema_txt]
);

useEffect(() => {
if (exampleId) {
fetch(examples.find((e) => e.id === exampleId)!.template.schema.src)
.then((res) => res.text())
.then((schema) => {
__set_schema_txt(schema);
});
}
}, [exampleId]);

return (
<main className="w-screen h-screen flex flex-col overflow-hidden">
<header className="p-4 flex justify-between">
<div className="flex gap-4">
<h1 className="text-xl font-black flex items-center gap-2">
<GridaLogo />
Forms
<span className="font-mono text-sm px-3 py-1 rounded-md bg-black/45 text-white">
Playground
</span>
</h1>
<div className="ms-10">
<Select
value={exampleId}
onValueChange={(value) => setExampleId(value)}
>
<SelectTrigger id="method" aria-label="select method">
<SelectValue placeholder="Select method" />
</SelectTrigger>
<SelectContent>
{examples.map((example) => (
<SelectItem key={example.id} value={example.id}>
{example.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</header>
<div className="flex-1 flex">
<section className="flex-1">
<div className="w-full h-full p-4">
<div className="w-full h-full rounded-md overflow-hidden shadow">
<Editor value={__schema_txt} onChange={__set_schema_txt} />
</div>
<details className="bg-white absolute bottom-0 left-0 max-h-96 overflow-scroll z-10">
<summary>Renderer JSON</summary>
<pre>
<code>{JSON.stringify(renderer, null, 2)}</code>
</pre>
</details>
</div>
</section>
<section className="flex-1">
<div className="px-4">
<header className="py-4 flex flex-col">
<div className="flex items-end gap-2">
<Label>
Method
<Select
value={method}
onValueChange={(value) => setMethod(value)}
>
<SelectTrigger id="method" aria-label="select method">
<SelectValue placeholder="Select method" />
</SelectTrigger>
<SelectContent>
<SelectItem value="get">GET</SelectItem>
<SelectItem value="post">POST</SelectItem>
<SelectItem value="put">PUT</SelectItem>
<SelectItem value="delete">DELETE</SelectItem>
</SelectContent>
</Select>
</Label>
<Label>
Action
<Input
type="text"
placeholder="https://forms.grida.co/submit/..."
value={action}
onChange={(e) => setAction(e.target.value)}
/>
</Label>
<Button>Submit</Button>
</div>
</header>
<div className="w-full min-h-40 rounded-lg shadow-md border-dashed flex flex-col items-center">
{renderer ? (
<FormView
title={"Form"}
form_id={renderer.id}
fields={renderer.fields()}
blocks={renderer.blocks()}
tree={renderer.tree()}
translation={resources.en.translation as any}
options={{
is_powered_by_branding_enabled: false,
}}
/>
) : (
<div className="grow flex items-center justify-center p-4 text-center text-gray-500">
Invalid schema
</div>
)}
</div>
<form action={action} method={method}>
{/* */}
</form>
</div>
</section>
</div>
<main>
<Playground />
</main>
);
}

const schema = {
uri: "https://forms.grida.co/schema/form.schema.json",
fileMatch: ["*"], // Associate with all JSON files
};

function Editor({
value,
onChange,
}: {
value?: string;
onChange?: (value?: string) => void;
}) {
const monaco = useMonaco();

useEffect(() => {
monaco?.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
enableSchemaRequest: true,
schemas: [schema],
});
}, [monaco]);

return (
<div className="font-mono flex-1 flex flex-col w-full h-full">
<header className="p-2">
<h2 className="">form.json</h2>
</header>
<MonacoEditor
height={"100%"}
defaultLanguage="json"
onChange={onChange}
value={value}
options={{
padding: {
top: 16,
},
minimap: {
enabled: false,
},
}}
/>
</div>
);
}

0 comments on commit d611b93

Please sign in to comment.