Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Grida Forms Playground #101

Merged
merged 31 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6f41b9e
form props
softmarshmallow May 10, 2024
04b6788
mv
softmarshmallow May 10, 2024
53d9a65
toggle type
softmarshmallow May 10, 2024
01a7c2d
sync types
softmarshmallow May 10, 2024
b7a9412
add toggle
softmarshmallow May 10, 2024
ebf12b8
mv
softmarshmallow May 10, 2024
1d8e84f
add toggle group
softmarshmallow May 10, 2024
b227a2f
add shadcn overrides
softmarshmallow May 10, 2024
2c76f1f
add array support in shcema
softmarshmallow May 10, 2024
c6732da
poc - array field rendering
softmarshmallow May 10, 2024
e3038f0
mv
softmarshmallow May 10, 2024
3edb496
schema
softmarshmallow May 11, 2024
df84030
playground prep
softmarshmallow May 11, 2024
97361a9
playground sharing
softmarshmallow May 11, 2024
c8a0903
form default styles
softmarshmallow May 11, 2024
69f71d6
rename
softmarshmallow May 12, 2024
e1ac1e1
wip playground ai generate
softmarshmallow May 12, 2024
693b2cc
update pages
softmarshmallow May 12, 2024
9cee30b
add prompt handling
softmarshmallow May 12, 2024
f02ec99
ai page
softmarshmallow May 12, 2024
87dcd79
update gist
softmarshmallow May 12, 2024
308487c
slug
softmarshmallow May 12, 2024
adeb2c1
disable on gen
softmarshmallow May 12, 2024
dc5e628
fix form scrolling
softmarshmallow May 13, 2024
a9a02f5
fix form field checkboxes
softmarshmallow May 13, 2024
ceee147
ai page
softmarshmallow May 13, 2024
348589d
zod description
softmarshmallow May 13, 2024
938a466
layout & theme
softmarshmallow May 13, 2024
df2734f
fix index theme
softmarshmallow May 13, 2024
50488e5
update editor theme
softmarshmallow May 13, 2024
fae53d0
schema const
softmarshmallow May 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
);
}
Loading