add new view for empty form list, improve onboarding screen, add basic nocode editor as basis for further development, update prisma schema and api for nocode editor

This commit is contained in:
Matthias Nannt
2022-06-15 22:14:20 +09:00
parent fd8e1f4159
commit b622510dc4
18 changed files with 546 additions and 119 deletions

View File

@@ -2,17 +2,23 @@ import Link from "next/link";
import Router from "next/router";
import { Fragment } from "react";
import { Menu, Transition } from "@headlessui/react";
import { BsFilesAlt } from "react-icons/bs";
import { DotsHorizontalIcon, TrashIcon } from "@heroicons/react/solid";
import {
DotsHorizontalIcon,
PlusIcon,
TrashIcon,
} from "@heroicons/react/solid";
import { classNames } from "../lib/utils";
import { createForm, useForms } from "../lib/forms";
import Image from "next/image";
export default function FormList() {
const { forms, mutateForms } = useForms();
const newForm = async () => {
const form = await createForm();
await Router.push(`/forms/${form.id}/form`);
await Router.push(`/forms/${form.id}/welcome`);
};
const deleteForm = async (form, formIdx) => {
@@ -30,85 +36,128 @@ export default function FormList() {
};
return (
<div>
{forms && (
<ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
<button onClick={() => newForm()}>
<li className="col-span-1">
<div className="overflow-hidden text-white rounded-lg shadow bg-snoopred">
<div className="px-4 py-8 sm:p-10">+ New Form</div>
{forms &&
(forms.length === 0 ? (
<div className="mt-5 text-center">
<Image
src="/img/mascot-face-small.png"
height={200}
width={200}
alt="snoopForms Mascot"
/>
<hr className="mb-8 -mt-2" />
<h1 className="text-xl font-extrabold tracking-tight text-gray-900 sm:text-2xl md:text-3xl">
<span className="block xl:inline">Welcome to snoopForms</span>{" "}
</h1>
<p className="max-w-md mx-auto mt-3 text-base text-gray-500 sm:text-lg md:mt-5 md:text-lg md:max-w-3xl">
Spin up forms in minutes. Pipe your data exactly where you need
it. Maximize your results with juicy analytics.
</p>
{/* <hr className="my-8" /> */}
<div className="max-w-md p-8 mx-auto mt-8 rounded-lg shadow-inner bg-lightgray-200">
<BsFilesAlt className="w-12 h-12 mx-auto text-gray-400" />
<h3 className="mt-3 text-sm font-medium text-gray-900">
You don&apos;t have any forms yet.
</h3>
<p className="mt-1 text-sm text-gray-500">
It&apos;s time to create your first!
</p>
<div className="mt-6">
<button
type="button"
onClick={() => newForm()}
className="inline-flex items-center px-4 py-2 text-sm font-medium text-white border border-transparent rounded-md shadow-sm bg-snoopred-600 hover:bg-snoopred-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-snoopred-500"
>
<PlusIcon className="w-5 h-5 mr-2 -ml-1" aria-hidden="true" />
New Form
</button>
</div>
</li>
</button>
{forms
.sort((a, b) => b.updatedAt - a.updatedAt)
.map((form, formIdx) => (
<li key={form.id} className="col-span-1 ">
<div className="bg-white divide-y rounded-lg shadow divide-lightgray-200">
<Link href={`/forms/${form.id}`}>
<a>
<div className="px-4 py-5 sm:p-6">{form.name}</div>
</a>
</Link>
<div className="px-4 py-1 text-right sm:px-6">
<Menu as="div" className="relative inline-block text-left">
{({ open }) => (
<>
<div>
<Menu.Button className="flex items-center p-2 -m-2 rounded-full text-darkgray-400 hover:text-darkgray-500-600 focus:outline-none">
<span className="sr-only">Open options</span>
<DotsHorizontalIcon
className="w-5 h-5"
aria-hidden="true"
/>
</Menu.Button>
</div>
<Transition
show={open}
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
static
className="absolute left-0 w-56 mt-2 origin-top-right bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<div className="py-1">
<Menu.Item>
{({ active }) => (
<button
onClick={() => deleteForm(form, formIdx)}
className={classNames(
active
? "bg-lightgray-100 text-darkgray-700"
: "text-darkgray-500",
"flex px-4 py-2 text-sm w-full"
)}
>
<TrashIcon
className="w-5 h-5 mr-3 text-darkgray-400"
aria-hidden="true"
/>
<span>Delete Form</span>
</button>
)}
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</>
)}
</Menu>
</div>
</div>
</div>
) : (
<ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
<button onClick={() => newForm()}>
<li className="col-span-1">
<div className="overflow-hidden text-white rounded-lg shadow bg-snoopred">
<div className="px-4 py-8 sm:p-10">+ New Form</div>
</div>
</li>
))}
</ul>
)}
</button>
{forms
.sort((a, b) => b.updatedAt - a.updatedAt)
.map((form, formIdx) => (
<li key={form.id} className="col-span-1 ">
<div className="bg-white divide-y rounded-lg shadow divide-lightgray-200">
<Link href={`/forms/${form.id}`}>
<a>
<div className="px-4 py-5 sm:p-6">{form.name}</div>
</a>
</Link>
<div className="px-4 py-1 text-right sm:px-6">
<Menu
as="div"
className="relative inline-block text-left"
>
{({ open }) => (
<>
<div>
<Menu.Button className="flex items-center p-2 -m-2 rounded-full text-darkgray-400 hover:text-darkgray-500-600 focus:outline-none">
<span className="sr-only">Open options</span>
<DotsHorizontalIcon
className="w-5 h-5"
aria-hidden="true"
/>
</Menu.Button>
</div>
<Transition
show={open}
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
static
className="absolute left-0 w-56 mt-2 origin-top-right bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<div className="py-1">
<Menu.Item>
{({ active }) => (
<button
onClick={() =>
deleteForm(form, formIdx)
}
className={classNames(
active
? "bg-lightgray-100 text-darkgray-700"
: "text-darkgray-500",
"flex px-4 py-2 text-sm w-full"
)}
>
<TrashIcon
className="w-5 h-5 mr-3 text-darkgray-400"
aria-hidden="true"
/>
<span>Delete Form</span>
</button>
)}
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</>
)}
</Menu>
</div>
</div>
</li>
))}
</ul>
))}
</div>
);
}

View File

@@ -0,0 +1,49 @@
import { useCallback, useEffect } from "react";
import { v4 as uuidv4 } from "uuid";
import { useNoCodeForm } from "../../lib/noCodeForm";
import Loading from "../Loading";
import Page from "./Page";
export default function Builder({ formId }) {
const { noCodeForm, mutateNoCodeForm, isLoadingNoCodeForm } =
useNoCodeForm(formId);
const addPage = useCallback(() => {
if (noCodeForm) {
const updatedNCF = JSON.parse(JSON.stringify(noCodeForm));
updatedNCF.pages.push({
id: uuidv4(),
elements: [],
});
mutateNoCodeForm(updatedNCF, false);
}
}, [mutateNoCodeForm, noCodeForm]);
useEffect(() => {
if (noCodeForm && noCodeForm.pages.length === 0) addPage();
}, [noCodeForm]);
if (isLoadingNoCodeForm) {
return <Loading />;
}
return (
<div className="w-full bg-gray-100">
<div className="flex justify-center w-full mt-10">
<div className="w-full max-w-5xl">
<div className="grid grid-cols-1 gap-6">
{noCodeForm.pages.map((page) => (
<Page key={page.id} />
))}
</div>
<button
onClick={() => addPage()}
className="inline-flex items-center justify-center w-full px-4 py-2 mt-3 text-sm font-medium text-gray-700 border border-gray-300 border-dashed rounded-md bg-gray-50 hover:bg-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500"
>
+ Add Page
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,38 @@
import { useCallback, useRef } from "react";
import { createReactEditorJS } from "react-editor-js";
const ReactEditorJS = createReactEditorJS();
const Editor = ({}) => {
const editorCore = useRef(null);
const handleInitialize = useCallback((instance) => {
editorCore.current = instance;
}, []);
/* const handleSave = useCallback(async () => {
const savedData = await editorCore.current.save();
console.log(savedData);
}, []);
setTimeout(() => {
// save every ten seconds
handleSave();
}, 10000); */
const EDITOR_JS_TOOLS = {};
// Editor.js This will show block editor in component
// pass EDITOR_JS_TOOLS in tools props to configure tools with editor.js
return (
<ReactEditorJS
onInitialize={handleInitialize}
tools={EDITOR_JS_TOOLS}
minHeight={0}
/>
);
};
// Return the CustomEditor to use by other components.
export default Editor;

View File

@@ -0,0 +1,12 @@
import dynamic from "next/dynamic";
let Editor = dynamic(() => import("./Editor"), {
ssr: false,
});
export default function Page({}) {
return (
<div className="w-full p-10 bg-white rounded-lg">
{Editor && <Editor />}
</div>
);
}

View File

@@ -1,8 +1,10 @@
/* This example requires Tailwind CSS v2.0+ */
import { Dialog, RadioGroup, Transition } from "@headlessui/react";
import { CheckCircleIcon, LightBulbIcon } from "@heroicons/react/solid";
import { useRouter } from "next/router";
import { Fragment, useState } from "react";
import { persistForm, useForm } from "../../lib/forms";
import { createNoCodeForm } from "../../lib/noCodeForm";
import { classNames } from "../../lib/utils";
import Loading from "../Loading";
@@ -23,15 +25,14 @@ const formTypes = [
type FormOnboardingModalProps = {
open: boolean;
setOpen: (o: boolean) => void;
formId: string;
};
export default function FormOnboardingModal({
open,
setOpen,
formId,
}: FormOnboardingModalProps) {
const router = useRouter();
const { form, mutateForm, isLoadingForm } = useForm(formId);
const [name, setName] = useState(form.name);
const [formType, setFormType] = useState(formTypes[0]);
@@ -46,7 +47,10 @@ export default function FormOnboardingModal({
};
await persistForm(updatedForm);
mutateForm(updatedForm);
setOpen(false);
if (updatedForm.formType === "NOCODE") {
await createNoCodeForm(formId);
}
router.push(`/forms/${formId}/form`);
};
if (isLoadingForm) {

View File

@@ -30,8 +30,13 @@ export default function Layout({ children }) {
<div className="px-4 mx-auto max-w-7xl sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex">
<div className="flex items-center flex-shrink-0">
<img src="../../snoopForms_Logo_v4.svg" alt="snoopForms logo"/>
<div className="flex items-center flex-shrink-0 w-48">
<Image
src="/img/snoopforms-logo.svg"
alt="snoopForms logo"
width={300}
height={53}
/>
</div>
</div>
<div className="hidden sm:ml-6 sm:flex sm:items-center">
@@ -86,7 +91,7 @@ export default function Layout({ children }) {
</div>
<div className="flex items-center -mr-2 sm:hidden">
{/* Mobile menu button */}
<Disclosure.Button className="inline-flex items-center justify-center p-2 text-darkgray-400 bg-white rounded-md hover:text-darkgray-500 hover:bg-lightgray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-snoopred">
<Disclosure.Button className="inline-flex items-center justify-center p-2 bg-white rounded-md text-darkgray-400 hover:text-darkgray-500 hover:bg-lightgray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-snoopred">
<span className="sr-only">Open main menu</span>
{open ? (
<XIcon className="block w-6 h-6" aria-hidden="true" />

View File

@@ -0,0 +1,63 @@
import { signIn, useSession } from "next-auth/react";
import Head from "next/head";
import Loading from "../Loading";
import MenuBreadcrumbs from "./MenuBreadcrumbs";
import MenuProfile from "./MenuProfile";
import MenuSteps from "./MenuSteps";
export default function LayoutFormResults({
title,
formId,
currentStep,
children,
}) {
const { data: session, status } = useSession();
if (status === "loading") {
return <Loading />;
}
if (!session) {
signIn();
return <div>You need to be authenticated to view this page.</div>;
}
return (
<>
<Head>
<title>{title}</title>
</Head>
<div className="flex min-h-screen overflow-hidden bg-gray-100">
<div className="flex flex-col flex-1 overflow-hidden">
<header className="w-full">
<div className="relative z-10 flex flex-shrink-0 h-16 bg-white border-b shadow-sm border-lightgray-200">
<div className="flex flex-1 px-4 sm:px-6">
<MenuBreadcrumbs formId={formId} />
<MenuSteps formId={formId} currentStep={currentStep} />
<div className="flex items-center justify-end flex-1 space-x-2 text-right sm:ml-6 sm:space-x-4">
{/* Profile dropdown */}
<MenuProfile />
</div>
</div>
</div>
<div className="relative z-10 flex flex-shrink-0 h-16 border-b border-gray-200 shadow-inner bg-gray-50">
<div className="flex items-center justify-center flex-1 px-4">
<nav className="flex space-x-4" aria-label="resultModes">
<button
onClick={() => {}}
className="px-3 py-2 text-sm font-medium text-gray-600 border border-gray-800 rounded-md hover:text-gray-600"
>
Save
</button>
</nav>
</div>
</div>
</header>
{/* Main content */}
{children}
</div>
</div>
</>
);
}

32
lib/noCodeForm.ts Normal file
View File

@@ -0,0 +1,32 @@
import useSWR from "swr";
import { fetcher } from "./utils";
export const useNoCodeForm = (formId) => {
const { data, error, mutate } = useSWR(
`/api/forms/${formId}/nocodeform`,
fetcher
);
return {
noCodeForm: data,
isLoadingNoCodeForm: !error && !data,
isErrorNoCodeForm: error,
mutateNoCodeForm: mutate,
};
};
export const createNoCodeForm = async (formId) => {
try {
const res = await fetch(`/api/forms/${formId}/nocodeform`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
return await res.json();
} catch (error) {
console.error(error);
throw Error(
`createNoCodeForm: unable to create noCodeForm: ${error.message}`
);
}
};

View File

@@ -9,6 +9,8 @@
"lint": "next lint"
},
"dependencies": {
"@editorjs/editorjs": "^2.24.3",
"@editorjs/paragraph": "^2.8.0",
"@headlessui/react": "^1.6.1",
"@heroicons/react": "^1.0.6",
"@prisma/client": "^3.15.1",
@@ -22,7 +24,7 @@
"nodemailer": "^6.7.5",
"react": "18.1.0",
"react-dom": "18.1.0",
"react-feather": "^2.0.9",
"react-editor-js": "^2.0.6",
"react-icons": "^4.4.0",
"superjson": "^1.9.1",
"swr": "^1.3.0"

View File

@@ -0,0 +1,54 @@
import type { NextApiResponse, NextApiRequest } from "next";
import { getSession } from "next-auth/react";
import { formHasOwnership } from "../../../../../lib/api";
import { prisma } from "../../../../../lib/prisma";
export default async function handle(
req: NextApiRequest,
res: NextApiResponse
) {
// Check Authentication
const session = await getSession({ req: req });
if (!session) {
return res.status(401).json({ message: "Not authenticated" });
}
const formId = req.query.id.toString();
// GET /api/forms/:id/nocodeform
// Get noCodeForm for a form with specific id
if (req.method === "GET") {
const data = await prisma.noCodeForm.findUnique({
where: {
formId: formId,
},
});
return res.json(data);
}
// POST /api/forms/:id/nocodeform
// Updates an existing nocodeform
// Required fields in body: -
// Optional fields in body: title, published, finishedOnboarding, elements, elementsDraft
else if (req.method === "POST") {
const ownership = await formHasOwnership(session, formId);
if (!ownership) {
return res
.status(401)
.json({ message: "You are not authorized to change this noCodeForm" });
}
const data = { ...req.body, updatedAt: new Date() };
// create or update record
const prismaRes = await prisma.noCodeForm.upsert({
where: { formId },
update: data,
create: { form: { connect: { id: formId } } },
});
return res.json(prismaRes);
}
// Unknown HTTP Method
else {
throw new Error(
`The HTTP ${req.method} method is not supported by this route.`
);
}
}

View File

@@ -2,6 +2,7 @@ import { getCsrfToken } from "next-auth/react";
import { useRouter } from "next/router";
import { XCircleIcon } from "@heroicons/react/solid";
import { GetServerSideProps } from "next";
import Image from "next/image";
interface props {
csrfToken: string;
@@ -34,9 +35,14 @@ export default function SignIn({ csrfToken }: props) {
</div>
)}
<div className="w-full bg-white rounded-xl p-8 shadow-cont max-w-sm mx-auto lg:w-96">
<div className="w-full max-w-sm p-8 mx-auto bg-white rounded-xl shadow-cont lg:w-96">
<div>
<img src="../../snoopForms_Logo_v4.svg" alt="snoopForms logo"/>
<Image
src="/img/snoopforms-logo.svg"
alt="snoopForms logo"
width={500}
height={89}
/>
</div>
<div className="mt-8">
@@ -65,7 +71,7 @@ export default function SignIn({ csrfToken }: props) {
type="email"
autoComplete="email"
required
className="block w-full px-3 py-2 placeholder-lightgray-400 border border-lightgray-300 rounded-md shadow-sm appearance-none focus:outline-none focus:ring-snoopred-500 focus:border-snoopred-500 sm:text-sm"
className="block w-full px-3 py-2 border rounded-md shadow-sm appearance-none placeholder-lightgray-400 border-lightgray-300 focus:outline-none focus:ring-snoopred-500 focus:border-snoopred-500 sm:text-sm"
/>
</div>
</div>
@@ -83,7 +89,7 @@ export default function SignIn({ csrfToken }: props) {
type="password"
autoComplete="current-password"
required
className="block w-full px-3 py-2 placeholder-lightgray-400 border border-lightgray-300 rounded-md shadow-sm appearance-none focus:outline-none focus:ring-snoopred-500 focus:border-snoopred-500 sm:text-sm"
className="block w-full px-3 py-2 border rounded-md shadow-sm appearance-none placeholder-lightgray-400 border-lightgray-300 focus:outline-none focus:ring-snoopred-500 focus:border-snoopred-500 sm:text-sm"
/>
</div>
</div>
@@ -91,12 +97,18 @@ export default function SignIn({ csrfToken }: props) {
<div>
<button
type="submit"
className="flex justify-center w-full px-4 py-2 text-sm font-medium text-white bg-snoopred border border-transparent rounded-md shadow-sm hover:bg-snoopred-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-snoopred-500"
className="flex justify-center w-full px-4 py-2 text-sm font-medium text-white border border-transparent rounded-md shadow-sm bg-snoopred hover:bg-snoopred-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-snoopred-500"
>
Sign in
</button>
<div className="text-center">
<a href="" className="text-snoopred hover:text-snoopred-600 text-xs">Create an account</a></div>
<a
href=""
className="text-xs text-snoopred hover:text-snoopred-600"
>
Create an account
</a>
</div>
</div>
</form>
</div>

View File

@@ -1,10 +1,10 @@
import { GetServerSideProps } from "next";
import { getSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import Builder from "../../../components/builder/Builder";
import FormCode from "../../../components/form/FormCode";
import FormOnboardingModal from "../../../components/form/FormOnboardingModal";
import LayoutFormBasics from "../../../components/layout/LayoutFormBasic";
import LayoutFormBuilder from "../../../components/layout/LayoutFormBuilder";
import Loading from "../../../components/Loading";
import { useForm } from "../../../lib/forms";
@@ -12,28 +12,26 @@ export default function FormPage() {
const router = useRouter();
const formId = router.query.id.toString();
const { form, isLoadingForm } = useForm(router.query.id);
const [openOnboardingModal, setOpenOnboardingModal] = useState(false);
useEffect(() => {
if (form && !form.finishedOnboarding) {
setOpenOnboardingModal(true);
}
}, [isLoadingForm]);
if (isLoadingForm) {
return <Loading />;
}
if (!form.finishedOnboarding) {
router.push(`/forms/${formId}/welcome`);
return <div></div>;
}
if (form.formType === "NOCODE") {
return (
<>
<LayoutFormBasics title={form.title} formId={formId} currentStep="form">
<FormOnboardingModal
open={openOnboardingModal}
setOpen={setOpenOnboardingModal}
formId={formId}
/>
</LayoutFormBasics>
<LayoutFormBuilder
title={form.title}
formId={formId}
currentStep="form"
>
<Builder formId={formId} />
</LayoutFormBuilder>
</>
);
} else {
@@ -41,11 +39,6 @@ export default function FormPage() {
<>
<LayoutFormBasics title={form.title} formId={formId} currentStep="form">
<FormCode />
<FormOnboardingModal
open={openOnboardingModal}
setOpen={setOpenOnboardingModal}
formId={formId}
/>
</LayoutFormBasics>
</>
);

View File

@@ -0,0 +1,43 @@
import { GetServerSideProps } from "next";
import { getSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import FormOnboardingModal from "../../../components/form/FormOnboardingModal";
import LayoutFormBasics from "../../../components/layout/LayoutFormBasic";
import Loading from "../../../components/Loading";
import { useForm } from "../../../lib/forms";
export default function WelcomePage() {
const router = useRouter();
const formId = router.query.id.toString();
const { form, isLoadingForm } = useForm(router.query.id);
const [openOnboardingModal, setOpenOnboardingModal] = useState(false);
useEffect(() => {
if (form && !form.finishedOnboarding) {
setOpenOnboardingModal(true);
}
}, [isLoadingForm]);
if (isLoadingForm) {
return <Loading />;
}
if (!form.finishedOnboarding) {
return (
<LayoutFormBasics title={form.title} formId={formId} currentStep="form">
<FormOnboardingModal open={openOnboardingModal} formId={formId} />
</LayoutFormBasics>
);
} else {
router.push(`/forms/${formId}`);
}
}
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
const session = await getSession({ req });
if (!session) {
res.statusCode = 403;
}
return { props: {} };
};

View File

@@ -19,6 +19,17 @@ CREATE TABLE "Form" (
CONSTRAINT "Form_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "NoCodeForm" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"formId" TEXT NOT NULL,
"pages" JSONB NOT NULL DEFAULT '[]',
CONSTRAINT "NoCodeForm_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Pipeline" (
"id" TEXT NOT NULL,
@@ -79,6 +90,9 @@ CREATE TABLE "verification_requests" (
CONSTRAINT "verification_requests_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "NoCodeForm_formId_key" ON "NoCodeForm"("formId");
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
@@ -88,6 +102,9 @@ CREATE UNIQUE INDEX "verification_requests_token_key" ON "verification_requests"
-- AddForeignKey
ALTER TABLE "Form" ADD CONSTRAINT "Form_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "NoCodeForm" ADD CONSTRAINT "NoCodeForm_formId_fkey" FOREIGN KEY ("formId") REFERENCES "Form"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Pipeline" ADD CONSTRAINT "Pipeline_formId_fkey" FOREIGN KEY ("formId") REFERENCES "Form"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -29,6 +29,16 @@ model Form {
schema Json
submissionSessions SubmissionSession[]
pipelines Pipeline[]
noCodeForm NoCodeForm?
}
model NoCodeForm {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
formId String @unique
pages Json @default("[]")
}
model Pipeline {

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -44,6 +44,20 @@
dependencies:
"@jridgewell/trace-mapping" "0.3.9"
"@editorjs/editorjs@^2.24.3":
version "2.24.3"
resolved "https://registry.yarnpkg.com/@editorjs/editorjs/-/editorjs-2.24.3.tgz#60ee6dd37d57b870ef29754355d77f9c61f30e79"
integrity sha512-VzrWaQ7mggNUAPTDGcqXJNIlBZH3S2IqsIUGA43UM2Q9VFaeS5KuVFVOTrFJvAzF7G+vZTO52ocm+hrDhTwvyw==
dependencies:
codex-notifier "^1.1.2"
codex-tooltip "^1.0.5"
nanoid "^3.1.22"
"@editorjs/paragraph@^2.8.0":
version "2.8.0"
resolved "https://registry.yarnpkg.com/@editorjs/paragraph/-/paragraph-2.8.0.tgz#11cc381fcafaf8b9160517ce65d59eee93fc4af9"
integrity sha512-z6w5ZR0ru3p/IjxJW/tb7OcSnVttkZukQMIsnBMX1FIKc1BNdr7NwM1YoCyTl4OnC90YfL0xgES6/20/W267pw==
"@eslint/eslintrc@^1.2.3":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.0.tgz#29f92c30bb3e771e4a2048c95fa6855392dfac4f"
@@ -216,6 +230,25 @@
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz#f691893df506b93e3cb1ccc15ec6e5ac64e8e570"
integrity sha512-NHlojO1DFTsSi3FtEleL9QWXeSF/UjhCW0fgpi7bumnNZ4wj/eQ+BJJ5n2pgoOliTOGv9nX2qXvmHap7rJMNmg==
"@react-editor-js/client@2.0.6":
version "2.0.6"
resolved "https://registry.yarnpkg.com/@react-editor-js/client/-/client-2.0.6.tgz#be9a2704b58322bc37dc6d2acb014f0ff28fe43c"
integrity sha512-LMMJLAXAwk1kVMy7fxTRFK6OdouvoseqJbmVUygJb2EcfuT84nC9OAtvGEL4vsVLUcnzEV400+F9t5OKa77FGQ==
dependencies:
"@react-editor-js/core" "2.0.6"
"@react-editor-js/core@2.0.6":
version "2.0.6"
resolved "https://registry.yarnpkg.com/@react-editor-js/core/-/core-2.0.6.tgz#3f20c0668d1f8502489ed7e354ff26461b270dce"
integrity sha512-mvHM2I+gT3AnvFpFhTZI0EFLKD9pRpgXDf286uwv6n6tngwLfnCCmtCbgiGI9ICph2GJvRZfaQubE+MHQ6YV8g==
"@react-editor-js/server@2.0.6":
version "2.0.6"
resolved "https://registry.yarnpkg.com/@react-editor-js/server/-/server-2.0.6.tgz#237f11002b4db9fe754fd9a89ff76f131f8a21fb"
integrity sha512-soW/bV5auciYr8gEYISWK4fuIblAcc4bcwPuCKnDBj9W9r/nAxMmNgCG+z9rs9Gnroa0Ko3Hzwzs9d5MdOShzg==
dependencies:
"@react-editor-js/core" "2.0.6"
"@rushstack/eslint-patch@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.3.tgz#6801033be7ff87a6b7cadaf5b337c9f366a3c4b0"
@@ -581,6 +614,16 @@ chokidar@^3.5.3:
optionalDependencies:
fsevents "~2.3.2"
codex-notifier@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/codex-notifier/-/codex-notifier-1.1.2.tgz#a733079185f4c927fa296f1d71eb8753fe080895"
integrity sha512-DCp6xe/LGueJ1N5sXEwcBc3r3PyVkEEDNWCVigfvywAkeXcZMk9K41a31tkEFBW0Ptlwji6/JlAb49E3Yrxbtg==
codex-tooltip@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/codex-tooltip/-/codex-tooltip-1.0.5.tgz#ba25fd5b3a58ba2f73fd667c2b46987ffd1edef2"
integrity sha512-IuA8LeyLU5p1B+HyhOsqR6oxyFQ11k3i9e9aXw40CrHFTRO2Y1npNBVU3W1SvhKAbUU7R/YikUBdcYFP0RcJag==
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
@@ -1567,7 +1610,7 @@ ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
nanoid@^3.1.30, nanoid@^3.3.4:
nanoid@^3.1.22, nanoid@^3.1.30, nanoid@^3.3.4:
version "3.3.4"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
@@ -1908,7 +1951,7 @@ prisma@^3.15.1:
dependencies:
"@prisma/engines" "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e"
prop-types@^15.7.2, prop-types@^15.8.1:
prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -1940,12 +1983,13 @@ react-dom@18.1.0:
loose-envify "^1.1.0"
scheduler "^0.22.0"
react-feather@^2.0.9:
version "2.0.10"
resolved "https://registry.yarnpkg.com/react-feather/-/react-feather-2.0.10.tgz#0e9abf05a66754f7b7bb71757ac4da7fb6be3b68"
integrity sha512-BLhukwJ+Z92Nmdcs+EMw6dy1Z/VLiJTzEQACDUEnWMClhYnFykJCGWQx+NmwP/qQHGX/5CzQ+TGi8ofg2+HzVQ==
react-editor-js@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/react-editor-js/-/react-editor-js-2.0.6.tgz#34771596986d79513e12e5f4990da46b9e0f2430"
integrity sha512-8u47IbhExiFB2kGNdJYlsX5iVlSzac38A3oJ7bmnTz3Lp7Slys1xreoYdG71+KiOcfX0dEgOIavV4e6TJeB5eg==
dependencies:
prop-types "^15.7.2"
"@react-editor-js/client" "2.0.6"
"@react-editor-js/server" "2.0.6"
react-icons@^4.4.0:
version "4.4.0"