mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-01 19:59:35 -05:00
from pull
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { Fragment } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { TailSpin } from "react-loader-spinner";
|
||||
|
||||
export default function LoadingModal({ isLoading }) {
|
||||
return (
|
||||
<Transition.Root show={isLoading} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
static
|
||||
className="fixed inset-0 z-10 overflow-y-auto"
|
||||
open={isLoading}
|
||||
onClose={() => {}}
|
||||
>
|
||||
<div className="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Dialog.Overlay className="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-20" />
|
||||
</Transition.Child>
|
||||
|
||||
{/* This element is to trick the browser into centering the modal contents. */}
|
||||
<span
|
||||
className="hidden sm:inline-block sm:align-middle sm:h-screen"
|
||||
aria-hidden="true"
|
||||
>
|
||||
​
|
||||
</span>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<div className="inline-flex items-center justify-center px-4 py-20 pb-4 overflow-hidden text-left align-bottom transition-all transform rounded-lg sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6">
|
||||
<TailSpin color="#000" height={50} width={50} />
|
||||
</div>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
}
|
||||
@@ -1,49 +1,117 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { useNoCodeForm } from "../../lib/noCodeForm";
|
||||
import { persistNoCodeForm, useNoCodeForm } from "../../lib/noCodeForm";
|
||||
import Loading from "../Loading";
|
||||
import Page from "./Page";
|
||||
import ShareModal from "./ShareModal";
|
||||
import UsageIntro from "./UsageIntro";
|
||||
|
||||
export default function Builder({ formId }) {
|
||||
const { noCodeForm, mutateNoCodeForm, isLoadingNoCodeForm } =
|
||||
const { noCodeForm, isLoadingNoCodeForm, mutateNoCodeForm } =
|
||||
useNoCodeForm(formId);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [openShareModal, setOpenShareModal] = useState(false);
|
||||
|
||||
const addPage = useCallback(() => {
|
||||
if (noCodeForm) {
|
||||
const updatedNCF = JSON.parse(JSON.stringify(noCodeForm));
|
||||
updatedNCF.pages.push({
|
||||
id: uuidv4(),
|
||||
elements: [],
|
||||
});
|
||||
mutateNoCodeForm(updatedNCF, false);
|
||||
const addPage = useCallback(async () => {
|
||||
const newNoCodeForm = JSON.parse(JSON.stringify(noCodeForm));
|
||||
newNoCodeForm.pagesDraft.push({
|
||||
id: uuidv4(),
|
||||
blocks: [],
|
||||
});
|
||||
await persistNoCodeForm(newNoCodeForm);
|
||||
mutateNoCodeForm(newNoCodeForm);
|
||||
}, [noCodeForm, mutateNoCodeForm]);
|
||||
|
||||
const deletePage = async (pageIdx) => {
|
||||
const newNoCodeForm = JSON.parse(JSON.stringify(noCodeForm));
|
||||
newNoCodeForm.pagesDraft.splice(pageIdx, 1);
|
||||
await persistNoCodeForm(newNoCodeForm);
|
||||
mutateNoCodeForm(newNoCodeForm);
|
||||
};
|
||||
|
||||
const initPages = useCallback(async () => {
|
||||
if (!isLoadingNoCodeForm && !isInitialized) {
|
||||
if (noCodeForm.pagesDraft.length === 0) {
|
||||
await addPage();
|
||||
}
|
||||
setIsInitialized(true);
|
||||
}
|
||||
}, [mutateNoCodeForm, noCodeForm]);
|
||||
}, [isLoadingNoCodeForm, noCodeForm, addPage, isInitialized]);
|
||||
|
||||
const publishChanges = async () => {
|
||||
const newNoCodeForm = JSON.parse(JSON.stringify(noCodeForm));
|
||||
newNoCodeForm.pages = newNoCodeForm.pagesDraft;
|
||||
await persistNoCodeForm(newNoCodeForm);
|
||||
mutateNoCodeForm(newNoCodeForm);
|
||||
setOpenShareModal(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (noCodeForm && noCodeForm.pages.length === 0) addPage();
|
||||
}, [noCodeForm]);
|
||||
initPages();
|
||||
}, [isLoadingNoCodeForm, initPages]);
|
||||
|
||||
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 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={() => addPage()}
|
||||
className="px-3 py-2 text-sm font-medium text-gray-600 border border-gray-800 rounded-md hover:text-gray-600"
|
||||
>
|
||||
Add Page
|
||||
</button>
|
||||
<Link href={`/forms/${formId}/preview`}>
|
||||
<a className="px-3 py-2 text-sm font-medium text-gray-600 border border-gray-800 rounded-md hover:text-gray-600">
|
||||
Preview Form
|
||||
</a>
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => publishChanges()}
|
||||
className="px-3 py-2 text-sm font-medium text-gray-600 border border-gray-800 rounded-md hover:text-gray-600"
|
||||
>
|
||||
Publish
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setOpenShareModal(true)}
|
||||
className="px-3 py-2 text-sm font-medium text-gray-600 border border-gray-800 rounded-md hover:text-gray-600"
|
||||
>
|
||||
Share
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-gray-100">
|
||||
<div className="flex justify-center w-full mt-10">
|
||||
<div className="w-full px-4 max-w-7xl">
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
<div className="px-10">
|
||||
<UsageIntro />
|
||||
</div>
|
||||
{noCodeForm.pagesDraft.map((page, pageIdx) => (
|
||||
<Page
|
||||
key={page.id}
|
||||
formId={formId}
|
||||
page={page}
|
||||
pageIdx={pageIdx}
|
||||
deletePageAction={deletePage}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ShareModal
|
||||
open={openShareModal}
|
||||
setOpen={setOpenShareModal}
|
||||
formId={formId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
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;
|
||||
@@ -1,12 +1,60 @@
|
||||
import { TrashIcon } from "@heroicons/react/outline";
|
||||
import dynamic from "next/dynamic";
|
||||
let Editor = dynamic(() => import("./Editor"), {
|
||||
import { persistNoCodeForm, useNoCodeForm } from "../../lib/noCodeForm";
|
||||
import Loading from "../Loading";
|
||||
let Editor = dynamic(() => import("../editorjs/Editor"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export default function Page({}) {
|
||||
export default function Page({ formId, page, pageIdx, deletePageAction }) {
|
||||
const { noCodeForm, isLoadingNoCodeForm, mutateNoCodeForm } =
|
||||
useNoCodeForm(formId);
|
||||
|
||||
const updatePage = async (blocks) => {
|
||||
const newNoCodeForm = JSON.parse(JSON.stringify(noCodeForm));
|
||||
if (pageIdx < newNoCodeForm.pagesDraft.length) {
|
||||
newNoCodeForm.pagesDraft[pageIdx].blocks = blocks;
|
||||
await persistNoCodeForm(newNoCodeForm);
|
||||
mutateNoCodeForm(newNoCodeForm);
|
||||
} else {
|
||||
throw Error(
|
||||
`updatePage error: Page at position ${pageIdx} not found in pagesDraft`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoadingNoCodeForm) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full p-10 bg-white rounded-lg">
|
||||
{Editor && <Editor />}
|
||||
<div className="flex w-full">
|
||||
<div className="flex w-8">
|
||||
{pageIdx !== 0 && (
|
||||
<button
|
||||
className="flex items-center h-full text-gray-400"
|
||||
onClick={() => {
|
||||
if (confirm("Do you really want to delete this page?")) {
|
||||
deletePageAction(pageIdx);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative w-full p-10 bg-white rounded-lg">
|
||||
<div className="relative">
|
||||
{Editor && (
|
||||
<Editor
|
||||
id={`${page.id}-editor`}
|
||||
autofocus={pageIdx === 0}
|
||||
onChange={(blocks) => updatePage(blocks)}
|
||||
value={noCodeForm.pagesDraft[pageIdx]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/* This example requires Tailwind CSS v2.0+ */
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { XIcon } from "@heroicons/react/outline";
|
||||
import { Fragment } from "react";
|
||||
|
||||
export default function ShareModal({ open, setOpen, formId }) {
|
||||
const getPublicFormUrl = () => {
|
||||
if (process.browser) {
|
||||
return `${window.location.protocol}//${window.location.host}/f/${formId}/`;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Transition.Root show={open} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={setOpen}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex items-end justify-center min-h-full p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative px-4 pt-5 pb-4 overflow-hidden text-left transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:max-w-4xl sm:w-full sm:p-6">
|
||||
<div className="absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
|
||||
<button
|
||||
type="button"
|
||||
className="text-gray-400 bg-white rounded-md hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-snoopred-500"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
<XIcon className="w-6 h-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900">
|
||||
Share your form
|
||||
</h3>
|
||||
<div className="max-w-xl mt-2 text-sm text-gray-500">
|
||||
<p>
|
||||
Let your participants fill out your form by accessing it
|
||||
via the public link.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-5 sm:flex sm:items-center">
|
||||
<div className="w-full sm:max-w-xs">
|
||||
<label htmlFor="surveyLink" className="sr-only">
|
||||
Public link
|
||||
</label>
|
||||
<input
|
||||
id="surveyLink"
|
||||
type="text"
|
||||
placeholder="Enter your email"
|
||||
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-snoopred-500 focus:border-snoopred-500 sm:text-sm"
|
||||
value={getPublicFormUrl()}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(getPublicFormUrl());
|
||||
}}
|
||||
className="inline-flex items-center justify-center w-full px-4 py-2 mt-3 font-medium text-white bg-gray-800 border border-transparent rounded-md shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/* This example requires Tailwind CSS v2.0+ */
|
||||
import { InformationCircleIcon } from "@heroicons/react/solid";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function UsageIntro() {
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
return (
|
||||
!dismissed && (
|
||||
<div className="p-4 bg-gray-100 border border-gray-700 rounded-md">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<InformationCircleIcon
|
||||
className="w-5 h-5 text-blue-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 ml-3 md:flex md:justify-between">
|
||||
<p className="text-sm text-gray-700">
|
||||
Welcome to the snoopForms No-Code Editor. Use 'tab' to
|
||||
add new blocks or change their options. You can also drag 'n
|
||||
drop blocks to reorder them.
|
||||
</p>
|
||||
<p className="mt-3 text-sm md:mt-0 md:ml-6">
|
||||
<a
|
||||
onClick={() => setDismissed(true)}
|
||||
className="font-medium text-gray-700 whitespace-nowrap hover:text-gray-600"
|
||||
>
|
||||
Dismiss
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { Fragment, useEffect, useRef, useState } from "react";
|
||||
import EditorJS from "@editorjs/editorjs";
|
||||
import DragDrop from "editorjs-drag-drop";
|
||||
import Undo from "editorjs-undo";
|
||||
import TextQuestion from "./tools/TextQuestion";
|
||||
import SubmitButton from "./tools/SubmitButton";
|
||||
|
||||
const Editor = ({ id, autofocus = false, onChange, value }) => {
|
||||
const [blocks, setBlocks] = useState([]);
|
||||
const ejInstance = useRef<EditorJS | null>();
|
||||
|
||||
useEffect(() => {
|
||||
onChange(blocks);
|
||||
}, [blocks]);
|
||||
|
||||
// This will run only once
|
||||
useEffect(() => {
|
||||
if (!ejInstance.current) {
|
||||
initEditor();
|
||||
}
|
||||
return () => {
|
||||
destroyEditor();
|
||||
};
|
||||
async function destroyEditor() {
|
||||
await ejInstance.current.isReady;
|
||||
ejInstance.current.destroy();
|
||||
ejInstance.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const initEditor = () => {
|
||||
const editor = new EditorJS({
|
||||
minHeight: 0,
|
||||
holder: id,
|
||||
data: value,
|
||||
onReady: () => {
|
||||
ejInstance.current = editor;
|
||||
new DragDrop(editor);
|
||||
new Undo({ editor });
|
||||
},
|
||||
onChange: async () => {
|
||||
let content = await editor.saver.save();
|
||||
setBlocks(content.blocks);
|
||||
},
|
||||
autofocus: autofocus,
|
||||
tools: { textQuestion: TextQuestion, submitButton: SubmitButton },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div id={id} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Editor;
|
||||
@@ -0,0 +1,58 @@
|
||||
import { API, BlockTool, BlockToolData, ToolConfig } from "@editorjs/editorjs";
|
||||
import { ArrowRightIcon } from "@heroicons/react/solid";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
//styles imports in angular.json
|
||||
interface TextQuestionData extends BlockToolData {
|
||||
latexString: string;
|
||||
}
|
||||
|
||||
export default class TextQuestion implements BlockTool {
|
||||
label: string;
|
||||
placeholder: string;
|
||||
api: API;
|
||||
|
||||
static get toolbox(): { icon: string; title?: string } {
|
||||
return {
|
||||
icon: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M18.1 15.3C18 15.4 17.8 15.5 17.7 15.6L15.3 16L17 19.6C17.2 20 17 20.4 16.6 20.6L13.8 21.9C13.7 22 13.6 22 13.5 22C13.2 22 12.9 21.8 12.8 21.6L11.2 18L9.3 19.5C9.2 19.6 9 19.7 8.8 19.7C8.4 19.7 8 19.4 8 18.9V7.5C8 7 8.3 6.7 8.8 6.7C9 6.7 9.2 6.8 9.3 6.9L18 14.3C18.3 14.5 18.4 15 18.1 15.3M6 12H4V4H20V12H18.4L20.6 13.9C21.4 13.6 21.9 12.9 21.9 12V4C21.9 2.9 21 2 19.9 2H4C2.9 2 2 2.9 2 4V12C2 13.1 2.9 14 4 14H6V12Z" />`,
|
||||
title: "Submit Button",
|
||||
};
|
||||
}
|
||||
|
||||
constructor({
|
||||
data,
|
||||
}: {
|
||||
api: API;
|
||||
config?: ToolConfig;
|
||||
data?: TextQuestionData;
|
||||
}) {
|
||||
this.label = data.label || "Submit";
|
||||
this.placeholder = data.placeholder;
|
||||
}
|
||||
|
||||
save(block: HTMLDivElement) {
|
||||
return {
|
||||
label: (block.firstElementChild.firstElementChild as HTMLInputElement)
|
||||
.innerHTML,
|
||||
};
|
||||
}
|
||||
|
||||
render(): HTMLElement {
|
||||
const container = document.createElement("div");
|
||||
const toolView = (
|
||||
<div className="inline-flex items-center px-4 py-2 pb-3 text-sm font-medium text-white bg-gray-700 border border-transparent rounded-md shadow-sm hover:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
<div
|
||||
contentEditable
|
||||
id="label"
|
||||
defaultValue={this.label}
|
||||
className="p-0 bg-transparent border-transparent ring-0 active:ring-0 focus:border-transparent focus:ring-0 focus:outline-none"
|
||||
>
|
||||
{this.label}
|
||||
</div>
|
||||
<ArrowRightIcon className="w-5 h-5 ml-2 -mr-1" aria-hidden="true" />
|
||||
</div>
|
||||
);
|
||||
ReactDOM.render(toolView, container);
|
||||
return container;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { API, BlockTool, BlockToolData, ToolConfig } from "@editorjs/editorjs";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
//styles imports in angular.json
|
||||
interface TextQuestionData extends BlockToolData {
|
||||
latexString: string;
|
||||
}
|
||||
|
||||
export default class TextQuestion implements BlockTool {
|
||||
label: string;
|
||||
placeholder: string;
|
||||
api: API;
|
||||
|
||||
static get toolbox(): { icon: string; title?: string } {
|
||||
return {
|
||||
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-justify"><line x1="21" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="21" y1="18" x2="3" y2="18"></line></svg>`,
|
||||
title: "Text Question",
|
||||
};
|
||||
}
|
||||
|
||||
constructor({
|
||||
data,
|
||||
}: {
|
||||
api: API;
|
||||
config?: ToolConfig;
|
||||
data?: TextQuestionData;
|
||||
}) {
|
||||
this.label = data.label;
|
||||
this.placeholder = data.placeholder;
|
||||
}
|
||||
|
||||
save(block: HTMLDivElement) {
|
||||
return {
|
||||
label: (block.firstElementChild.firstElementChild as HTMLInputElement)
|
||||
.value,
|
||||
placeholder: (
|
||||
block.firstElementChild.lastElementChild as HTMLInputElement
|
||||
).value,
|
||||
};
|
||||
}
|
||||
|
||||
renderSettings(): HTMLElement {
|
||||
return document.createElement("div");
|
||||
}
|
||||
|
||||
render(): HTMLElement {
|
||||
const container = document.createElement("div");
|
||||
const toolView = (
|
||||
<div className="pb-3">
|
||||
<input
|
||||
type="text"
|
||||
id="label"
|
||||
defaultValue={this.label}
|
||||
className="block w-full p-0 text-base font-medium text-gray-700 border-0 border-transparent ring-0 focus:ring-0"
|
||||
placeholder="Your Question"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="block w-full mt-1 text-gray-400 border-gray-300 rounded-md shadow-sm sm:text-base placeholder:text-gray-300"
|
||||
placeholder="optional placeholder"
|
||||
defaultValue={this.placeholder}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
ReactDOM.render(toolView, container);
|
||||
return container;
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ const libs = [
|
||||
},
|
||||
];
|
||||
|
||||
export default function FormCode() {
|
||||
export default function FormCode({ formId }) {
|
||||
const [selectedLib, setSelectedLib] = useState(null);
|
||||
|
||||
return (
|
||||
@@ -54,6 +54,32 @@ export default function FormCode() {
|
||||
programming language or framework.
|
||||
</p>
|
||||
</div>
|
||||
<hr className="my-5 text-gray-600" />
|
||||
<div>
|
||||
<label htmlFor="formId" className="block text-base text-gray-800">
|
||||
Your form ID
|
||||
</label>
|
||||
<div className="mt-5 sm:flex sm:items-center">
|
||||
<div className="w-full sm:max-w-xs">
|
||||
<input
|
||||
id="formId"
|
||||
type="text"
|
||||
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-snoopred-500 focus:border-snoopred-500 sm:text-sm disabled:bg-gray-100"
|
||||
value={formId}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(formId);
|
||||
}}
|
||||
className="inline-flex items-center justify-center w-full px-4 py-2 mt-3 font-medium text-white bg-gray-800 border border-transparent rounded-md shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="my-5 text-gray-600 " />
|
||||
<div className="mt-5">
|
||||
<RadioGroup value={selectedLib} onChange={setSelectedLib}>
|
||||
<RadioGroup.Label className="text-xs font-medium tracking-wide text-gray-500 uppercase">
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { SnoopElement, SnoopForm, SnoopPage } from "@snoopforms/react";
|
||||
import { useMemo } from "react";
|
||||
import { useNoCodeForm } from "../../lib/noCodeForm";
|
||||
import Loading from "../Loading";
|
||||
|
||||
export default function App({ id = "", formId, draft = false }) {
|
||||
const { noCodeForm, isLoadingNoCodeForm } = useNoCodeForm(formId);
|
||||
const pages = useMemo(() => {
|
||||
if (!isLoadingNoCodeForm) {
|
||||
return noCodeForm[draft ? "pagesDraft" : "pages"];
|
||||
}
|
||||
}, [draft, isLoadingNoCodeForm, noCodeForm]);
|
||||
|
||||
if (!pages) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full px-5 py-5">
|
||||
<SnoopForm
|
||||
key={id} // used to reset form
|
||||
domain={window.location.host}
|
||||
protocol={window.location.protocol === "http:" ? "http" : "https"}
|
||||
formId={formId}
|
||||
localOnly={draft}
|
||||
className="w-full max-w-3xl mx-auto space-y-6"
|
||||
>
|
||||
{pages.map((page) => (
|
||||
<SnoopPage key={page.id} name={page.id}>
|
||||
{page.blocks.map((block) =>
|
||||
block.type === "paragraph" ? (
|
||||
<p>{block.data.text}</p>
|
||||
) : block.type === "textQuestion" ? (
|
||||
<SnoopElement
|
||||
type="text"
|
||||
name={"name"}
|
||||
label={block.data.label}
|
||||
classNames={{
|
||||
label: "mt-4 block text-sm font-medium text-gray-800",
|
||||
element:
|
||||
"flex-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full min-w-0 rounded-md sm:text-sm border-gray-300",
|
||||
}}
|
||||
required
|
||||
/>
|
||||
) : block.type === "submitButton" ? (
|
||||
<SnoopElement
|
||||
name="submit"
|
||||
type="submit"
|
||||
label={block.data.label}
|
||||
classNames={{
|
||||
button:
|
||||
"flex justify-center px-4 py-2 mt-5 text-sm font-medium text-white border border-transparent rounded-md shadow-sm bg-gray-700 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-700",
|
||||
}}
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
</SnoopPage>
|
||||
))}
|
||||
</SnoopForm>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -40,18 +40,6 @@ export default function LayoutFormResults({
|
||||
</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 */}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
|
||||
import { ArrowLeftIcon, RefreshIcon } from "@heroicons/react/outline";
|
||||
import { useSession, signIn } from "next-auth/react";
|
||||
import Loading from "../Loading";
|
||||
|
||||
export default function LayoutShare({ formId, resetApp, 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>Form Preview</title>
|
||||
</Head>
|
||||
<div className="flex min-h-screen overflow-hidden bg-gray-50">
|
||||
<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 border-gray-200 shadow-sm">
|
||||
<div className="flex flex-1 px-4 sm:px-6">
|
||||
<div className="flex items-center flex-1">
|
||||
<Link href={`/forms/${formId}/form`}>
|
||||
<a>
|
||||
<ArrowLeftIcon className="w-6 h-6" aria-hidden="true" />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<p className="flex items-center justify-center flex-1 text-gray-600">
|
||||
Preview
|
||||
</p>
|
||||
<div className="flex items-center justify-end flex-1 space-x-2 text-right sm:ml-6 sm:space-x-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => resetApp()}
|
||||
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"
|
||||
>
|
||||
Restart
|
||||
<RefreshIcon
|
||||
className="w-5 h-5 ml-2 -mr-1"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main content */}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -30,3 +30,15 @@ export const createNoCodeForm = async (formId) => {
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const persistNoCodeForm = async (noCodeForm) => {
|
||||
try {
|
||||
await fetch(`/api/forms/${noCodeForm.formId}/nocodeform`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(noCodeForm),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import useSWR from "swr";
|
||||
import { fetcher } from "./utils";
|
||||
|
||||
export const usePipelines = (formId: string) => {
|
||||
const { data, error, mutate } = useSWR(
|
||||
() => `/api/forms/${formId}/pipelines`,
|
||||
fetcher
|
||||
);
|
||||
|
||||
return {
|
||||
pipelines: data,
|
||||
isLoadingPipelines: !error && !data,
|
||||
isErrorPipelines: error,
|
||||
mutatePipeliness: mutate,
|
||||
};
|
||||
};
|
||||
@@ -1,4 +1,6 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
var path = require("path");
|
||||
|
||||
const nextConfig = {
|
||||
reactStrictMode: false,
|
||||
async redirects() {
|
||||
@@ -10,6 +12,11 @@ const nextConfig = {
|
||||
},
|
||||
];
|
||||
},
|
||||
webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
|
||||
config.resolve.alias["react"] = path.resolve("./node_modules/react");
|
||||
// Important: return the modified config
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
||||
Generated
+7279
File diff suppressed because it is too large
Load Diff
+7
-3
@@ -10,22 +10,26 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@editorjs/editorjs": "^2.24.3",
|
||||
"@editorjs/header": "^2.6.2",
|
||||
"@editorjs/paragraph": "^2.8.0",
|
||||
"@headlessui/react": "^1.6.1",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@prisma/client": "^3.15.1",
|
||||
"@snoopforms/react": "^0.0.2",
|
||||
"babel-plugin-superjson-next": "^0.4.3",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"date-fns": "^2.28.0",
|
||||
"editorjs-drag-drop": "^1.1.2",
|
||||
"editorjs-undo": "^2.0.3",
|
||||
"json2csv": "^5.0.7",
|
||||
"next": "12.1.6",
|
||||
"next-auth": "^4.3.4",
|
||||
"nextjs-cors": "^2.1.1",
|
||||
"nodemailer": "^6.7.5",
|
||||
"react": "18.1.0",
|
||||
"react-dom": "18.1.0",
|
||||
"react-editor-js": "^2.0.6",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-icons": "^4.4.0",
|
||||
"react-loader-spinner": "^5.1.5",
|
||||
"superjson": "^1.9.1",
|
||||
"swr": "^1.3.0"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
import NextCors from "nextjs-cors";
|
||||
import { formHasOwnership } from "../../../../../lib/api";
|
||||
import { prisma } from "../../../../../lib/prisma";
|
||||
|
||||
export default async function handle(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
const formId = req.query.id.toString();
|
||||
|
||||
await NextCors(req, res, {
|
||||
// Options
|
||||
methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"],
|
||||
origin: "*",
|
||||
optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
|
||||
});
|
||||
|
||||
// check if session exist
|
||||
const session = await getSession({ req: req });
|
||||
if (!session) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
// check if user is form owner
|
||||
const ownership = await formHasOwnership(session, formId);
|
||||
if (!ownership) {
|
||||
return res
|
||||
.status(401)
|
||||
.json({ message: "You are not authorized to change this noCodeForm" });
|
||||
}
|
||||
|
||||
// GET /api/forms/[formId]/pipelines
|
||||
// Get all pipelines for a specific form
|
||||
if (req.method === "GET") {
|
||||
const pipelinesData = await prisma.pipeline.findMany({
|
||||
where: {
|
||||
form: { id: formId },
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
createdAt: "desc",
|
||||
},
|
||||
],
|
||||
});
|
||||
return res.json(pipelinesData);
|
||||
}
|
||||
|
||||
// POST /api/forms/:id/pipelines
|
||||
// Creates a new submission session
|
||||
// Required fields in body: -
|
||||
// Optional fields in body: -
|
||||
if (req.method === "POST") {
|
||||
const { type, data } = req.body;
|
||||
if (!["WEBHOOK"].includes(type)) {
|
||||
return res.status(400).json({ message: "Unknown pipeline type" });
|
||||
}
|
||||
const prismaRes = await prisma.pipeline.create({
|
||||
data: { type, data, 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.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
import NextCors from "nextjs-cors";
|
||||
import { formHasOwnership } from "../../../../../lib/api";
|
||||
import { prisma } from "../../../../../lib/prisma";
|
||||
|
||||
export default async function handle(
|
||||
@@ -17,6 +19,19 @@ export default async function handle(
|
||||
// GET /api/forms
|
||||
// Gets all forms of a user
|
||||
if (req.method === "GET") {
|
||||
// check if session exist
|
||||
const session = await getSession({ req: req });
|
||||
if (!session) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
// check if user is form owner
|
||||
const ownership = await formHasOwnership(session, formId);
|
||||
if (!ownership) {
|
||||
return res
|
||||
.status(401)
|
||||
.json({ message: "You are not authorized to change this noCodeForm" });
|
||||
}
|
||||
|
||||
const submissionSessionsData = await prisma.submissionSession.findMany({
|
||||
where: {
|
||||
form: { id: formId },
|
||||
@@ -33,6 +48,7 @@ export default async function handle(
|
||||
return res.json(submissionSessionsData);
|
||||
}
|
||||
|
||||
// PUBLIC
|
||||
// POST /api/forms/:id/submissionSessions
|
||||
// Creates a new submission session
|
||||
// Required fields in body: -
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { GetServerSideProps } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import App from "../../components/frontend/App";
|
||||
import Loading from "../../components/Loading";
|
||||
import { useForm } from "../../lib/forms";
|
||||
|
||||
export default function Share({}) {
|
||||
const router = useRouter();
|
||||
const formId = router.query.id.toString();
|
||||
const { form, isLoadingForm } = useForm(formId);
|
||||
|
||||
if (isLoadingForm) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (form.formType !== "NOCODE") {
|
||||
return (
|
||||
<div>
|
||||
Form Frontend is only avaiblable for Forms built with No-Code-Editor
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>SnoopForms</title>
|
||||
</Head>
|
||||
<App formId={formId} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
|
||||
const session = await getSession({ req });
|
||||
if (!session) {
|
||||
res.statusCode = 403;
|
||||
}
|
||||
return { props: {} };
|
||||
};
|
||||
@@ -38,7 +38,7 @@ export default function FormPage() {
|
||||
return (
|
||||
<>
|
||||
<LayoutFormBasics title={form.title} formId={formId} currentStep="form">
|
||||
<FormCode />
|
||||
<FormCode formId={formId} />
|
||||
</LayoutFormBasics>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -76,11 +76,11 @@ export default function PipelinesPage() {
|
||||
</header>
|
||||
<div className="my-4">
|
||||
<p className="text-gray-700">
|
||||
snoopHub automatically stores your data and gives you and overview
|
||||
of your submissions and form analytics. If you want to use your
|
||||
snoopHub automatically stores your data and gives you an overview of
|
||||
your submissions and form analytics. If you want to use your
|
||||
submissions or form events in other systems you can set up pipelines
|
||||
to let snoopHub sent the data to these applications as soon as it
|
||||
arrives.
|
||||
arrives and keep everything in sync.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { GetServerSideProps } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import App from "../../../components/frontend/App";
|
||||
import LayoutPreview from "../../../components/layout/LayoutPreview";
|
||||
import Loading from "../../../components/Loading";
|
||||
import { useForm } from "../../../lib/forms";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export default function Share({}) {
|
||||
const router = useRouter();
|
||||
const formId = router.query.id.toString();
|
||||
const { form, isLoadingForm } = useForm(formId);
|
||||
const [appId, setAppId] = useState(uuidv4());
|
||||
|
||||
const resetApp = () => {
|
||||
setAppId(uuidv4());
|
||||
};
|
||||
|
||||
if (isLoadingForm) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (form.formType !== "NOCODE") {
|
||||
return (
|
||||
<div>Preview is only avaiblable for Forms built with No-Code-Editor</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LayoutPreview formId={formId} resetApp={resetApp}>
|
||||
<App id={appId} formId={formId} draft={true} />
|
||||
</LayoutPreview>
|
||||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
|
||||
const session = await getSession({ req });
|
||||
if (!session) {
|
||||
res.statusCode = 403;
|
||||
}
|
||||
return { props: {} };
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
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";
|
||||
@@ -11,13 +10,6 @@ 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 />;
|
||||
@@ -26,11 +18,12 @@ export default function WelcomePage() {
|
||||
if (!form.finishedOnboarding) {
|
||||
return (
|
||||
<LayoutFormBasics title={form.title} formId={formId} currentStep="form">
|
||||
<FormOnboardingModal open={openOnboardingModal} formId={formId} />
|
||||
<FormOnboardingModal open={true} formId={formId} />
|
||||
</LayoutFormBasics>
|
||||
);
|
||||
} else {
|
||||
router.push(`/forms/${formId}`);
|
||||
return <Loading />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -26,6 +26,7 @@ CREATE TABLE "NoCodeForm" (
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"formId" TEXT NOT NULL,
|
||||
"pages" JSONB NOT NULL DEFAULT '[]',
|
||||
"pagesDraft" JSONB NOT NULL DEFAULT '[]',
|
||||
|
||||
CONSTRAINT "NoCodeForm_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
@@ -39,6 +39,7 @@ model NoCodeForm {
|
||||
form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
|
||||
formId String @unique
|
||||
pages Json @default("[]")
|
||||
pagesDraft Json @default("[]")
|
||||
}
|
||||
|
||||
model Pipeline {
|
||||
|
||||
@@ -82,3 +82,11 @@
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
.ce-block__content,
|
||||
.ce-toolbar__content {
|
||||
max-width: calc(100% - 80px) !important;
|
||||
}
|
||||
.cdx-block {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user