From ae5ae7b46c138f81cb2fb3ea2e87e2eb4cc38cfe Mon Sep 17 00:00:00 2001 From: Matthias Nannt Date: Mon, 20 Jun 2022 20:04:23 +0900 Subject: [PATCH] feat: add first version of nocode editor without preview/publish/share --- components/LoadingModal.tsx | 52 ++++++++ components/builder/Builder.tsx | 113 +++++++++++++----- components/builder/Editor.tsx | 38 ------ components/builder/Page.tsx | 52 +++++++- components/builder/UsageIntro.tsx | 36 ++++++ components/editorjs/Editor.tsx | 57 +++++++++ components/editorjs/tools/TextQuestion.tsx | 77 ++++++++++++ components/form/FormCode.tsx | 28 ++++- components/layout/LayoutFormBuilder.tsx | 12 -- lib/noCodeForm.ts | 12 ++ lib/pipelines.ts | 16 +++ package.json | 9 +- pages/api/forms/[id]/pipelines/index.ts | 69 +++++++++++ .../forms/[id]/submissionSessions/index.ts | 16 +++ pages/forms/[id]/form.tsx | 2 +- pages/forms/[id]/pipelines.tsx | 6 +- .../migration.sql | 1 + prisma/schema.prisma | 1 + styles/globals.css | 8 ++ yarn.lock | 83 ++++++------- 20 files changed, 557 insertions(+), 131 deletions(-) create mode 100644 components/LoadingModal.tsx delete mode 100644 components/builder/Editor.tsx create mode 100644 components/builder/UsageIntro.tsx create mode 100644 components/editorjs/Editor.tsx create mode 100644 components/editorjs/tools/TextQuestion.tsx create mode 100644 lib/pipelines.ts create mode 100644 pages/api/forms/[id]/pipelines/index.ts rename prisma/migrations/{20220615062843_init => 20220619113127_init}/migration.sql (98%) diff --git a/components/LoadingModal.tsx b/components/LoadingModal.tsx new file mode 100644 index 0000000000..4a17479bb0 --- /dev/null +++ b/components/LoadingModal.tsx @@ -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 ( + + {}} + > +
+ + + + + {/* This element is to trick the browser into centering the modal contents. */} + + +
+ +
+
+
+
+
+ ); +} diff --git a/components/builder/Builder.tsx b/components/builder/Builder.tsx index 452ac2036f..dfd23b2fe6 100644 --- a/components/builder/Builder.tsx +++ b/components/builder/Builder.tsx @@ -1,49 +1,104 @@ -import { useCallback, useEffect } from "react"; +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 UsageIntro from "./UsageIntro"; +import LoadingModal from "../LoadingModal"; export default function Builder({ formId }) { - const { noCodeForm, mutateNoCodeForm, isLoadingNoCodeForm } = + const { noCodeForm, isLoadingNoCodeForm, mutateNoCodeForm } = useNoCodeForm(formId); + const [pagesDraft, setPagesDraft] = useState([]); + const [isInitialized, setIsInitialized] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const save = async () => { + setIsLoading(true); + const newNoCodeForm = JSON.parse(JSON.stringify(noCodeForm)); + newNoCodeForm.pagesDraft = pagesDraft; + await persistNoCodeForm(newNoCodeForm); + mutateNoCodeForm(newNoCodeForm); + setIsLoading(false); + }; const addPage = useCallback(() => { - if (noCodeForm) { - const updatedNCF = JSON.parse(JSON.stringify(noCodeForm)); - updatedNCF.pages.push({ - id: uuidv4(), - elements: [], - }); - mutateNoCodeForm(updatedNCF, false); + const newPagesDraft = JSON.parse(JSON.stringify(pagesDraft)); + newPagesDraft.push({ + id: uuidv4(), + blocks: [], + }); + setPagesDraft(newPagesDraft); + }, [pagesDraft, setPagesDraft]); + + const deletePage = (pageIdx) => { + const newPagesDraft = JSON.parse(JSON.stringify(pagesDraft)); + newPagesDraft.splice(pageIdx, 1); + setPagesDraft(newPagesDraft); + }; + + const initPages = useCallback(() => { + if (!isLoadingNoCodeForm && !isInitialized) { + if (noCodeForm.pagesDraft.length === 0) { + addPage(); + } else { + setPagesDraft(noCodeForm.pagesDraft); + } + setIsInitialized(true); } - }, [mutateNoCodeForm, noCodeForm]); + }, [isLoadingNoCodeForm, noCodeForm, addPage, isInitialized]); useEffect(() => { - if (noCodeForm && noCodeForm.pages.length === 0) addPage(); - }, [noCodeForm]); + initPages(); + }, [isLoadingNoCodeForm, initPages]); if (isLoadingNoCodeForm) { - return ; + ; } return ( -
-
-
-
- {noCodeForm.pages.map((page) => ( - - ))} -
- + <> +
+
+
-
+ +
+
+
+
+
+ +
+ {pagesDraft.map((page, pageIdx) => ( + + ))} +
+
+
+
+ + ); } diff --git a/components/builder/Editor.tsx b/components/builder/Editor.tsx deleted file mode 100644 index 3f5b408b13..0000000000 --- a/components/builder/Editor.tsx +++ /dev/null @@ -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 ( - - ); -}; - -// Return the CustomEditor to use by other components. - -export default Editor; diff --git a/components/builder/Page.tsx b/components/builder/Page.tsx index 45dcdbbc1f..b5fbf0df3c 100644 --- a/components/builder/Page.tsx +++ b/components/builder/Page.tsx @@ -1,12 +1,56 @@ +import { TrashIcon } from "@heroicons/react/outline"; import dynamic from "next/dynamic"; -let Editor = dynamic(() => import("./Editor"), { +let Editor = dynamic(() => import("../editorjs/Editor"), { ssr: false, }); -export default function Page({}) { +export default function Page({ + page, + pageIdx, + pagesDraft, + setPagesDraft, + deletePageAction, +}) { + const updatePage = (blocks) => { + const newPagesDraft = JSON.parse(JSON.stringify(pagesDraft)); + if (pageIdx < newPagesDraft.length) { + newPagesDraft[pageIdx].blocks = blocks; + setPagesDraft(newPagesDraft); + } else { + throw Error( + `updatePage error: Page at position ${pageIdx} not found in pagesDraft` + ); + } + }; + return ( -
- {Editor && } +
+
+ {pageIdx !== 0 && ( + + )} +
+
+
+ {Editor && ( + updatePage(blocks)} + value={pagesDraft[pageIdx]} + /> + )} +
+
); } diff --git a/components/builder/UsageIntro.tsx b/components/builder/UsageIntro.tsx new file mode 100644 index 0000000000..885e4c8446 --- /dev/null +++ b/components/builder/UsageIntro.tsx @@ -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 && ( +
+
+
+
+
+

+ 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. +

+

+ setDismissed(true)} + className="font-medium text-gray-700 whitespace-nowrap hover:text-gray-600" + > + Dismiss + +

+
+
+
+ ) + ); +} diff --git a/components/editorjs/Editor.tsx b/components/editorjs/Editor.tsx new file mode 100644 index 0000000000..abc8cae920 --- /dev/null +++ b/components/editorjs/Editor.tsx @@ -0,0 +1,57 @@ +/* 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"; + +const Editor = ({ id, autofocus = false, onChange, value }) => { + const [blocks, setBlocks] = useState([]); + const ejInstance = useRef(); + + 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 }, + }); + }; + + return ( + +
+ + ); +}; + +export default Editor; diff --git a/components/editorjs/tools/TextQuestion.tsx b/components/editorjs/tools/TextQuestion.tsx new file mode 100644 index 0000000000..353ffc86db --- /dev/null +++ b/components/editorjs/tools/TextQuestion.tsx @@ -0,0 +1,77 @@ +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: ``, + title: "Text Question", + }; + } + + constructor({ + data, + }: { + api: API; + config?: ToolConfig; + data?: TextQuestionData; + }) { + this.label = data.label; + this.placeholder = data.placeholder; + } + + save(block: HTMLDivElement) { + // console.log(block) + // ;(window as any).x = block + + return { + label: (block.firstElementChild.firstElementChild as HTMLInputElement) + .value, + placeholder: ( + block.firstElementChild.lastElementChild as HTMLInputElement + ).value, + }; + } + /* renderSettings(): HTMLElement { + return document.createElement("div"); + } */ + + render(): HTMLElement { + /* this.wrapperDiv.innerHTML = ""; + this.latexDiv.innerHTML = ""; + this.renderLatex(); + this.wrapperDiv.append(this.latexDiv); + this.wrapperDiv.append(this.editTextfield); + + return this.wrapperDiv; */ + const container = document.createElement("div"); + const toolView = ( +
+ + +
+ ); + ReactDOM.render(toolView, container); + return container; + } +} diff --git a/components/form/FormCode.tsx b/components/form/FormCode.tsx index 23a9a7d307..867ac01fcd 100644 --- a/components/form/FormCode.tsx +++ b/components/form/FormCode.tsx @@ -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.

+
+
+ +
+
+ +
+ +
+
+
diff --git a/components/layout/LayoutFormBuilder.tsx b/components/layout/LayoutFormBuilder.tsx index 71475544cb..ed61661fe2 100644 --- a/components/layout/LayoutFormBuilder.tsx +++ b/components/layout/LayoutFormBuilder.tsx @@ -40,18 +40,6 @@ export default function LayoutFormResults({
-
-
- -
-
{/* Main content */} diff --git a/lib/noCodeForm.ts b/lib/noCodeForm.ts index ab1cb6e919..47b85a8ce6 100644 --- a/lib/noCodeForm.ts +++ b/lib/noCodeForm.ts @@ -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); + } +}; diff --git a/lib/pipelines.ts b/lib/pipelines.ts new file mode 100644 index 0000000000..e0f0149731 --- /dev/null +++ b/lib/pipelines.ts @@ -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, + }; +}; diff --git a/package.json b/package.json index 825af2970f..9dcbc08214 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "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", @@ -17,15 +18,17 @@ "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" }, diff --git a/pages/api/forms/[id]/pipelines/index.ts b/pages/api/forms/[id]/pipelines/index.ts new file mode 100644 index 0000000000..fde6730d89 --- /dev/null +++ b/pages/api/forms/[id]/pipelines/index.ts @@ -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.` + ); + } +} diff --git a/pages/api/forms/[id]/submissionSessions/index.ts b/pages/api/forms/[id]/submissionSessions/index.ts index f52ed1b383..bf0ecc9b5a 100644 --- a/pages/api/forms/[id]/submissionSessions/index.ts +++ b/pages/api/forms/[id]/submissionSessions/index.ts @@ -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: - diff --git a/pages/forms/[id]/form.tsx b/pages/forms/[id]/form.tsx index b966ed9071..dc8dcdd632 100644 --- a/pages/forms/[id]/form.tsx +++ b/pages/forms/[id]/form.tsx @@ -38,7 +38,7 @@ export default function FormPage() { return ( <> - + ); diff --git a/pages/forms/[id]/pipelines.tsx b/pages/forms/[id]/pipelines.tsx index f22f88ff80..a6a79f5d47 100644 --- a/pages/forms/[id]/pipelines.tsx +++ b/pages/forms/[id]/pipelines.tsx @@ -76,11 +76,11 @@ export default function PipelinesPage() {

- 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.

diff --git a/prisma/migrations/20220615062843_init/migration.sql b/prisma/migrations/20220619113127_init/migration.sql similarity index 98% rename from prisma/migrations/20220615062843_init/migration.sql rename to prisma/migrations/20220619113127_init/migration.sql index ebe1fadfab..0697b9ebcf 100644 --- a/prisma/migrations/20220615062843_init/migration.sql +++ b/prisma/migrations/20220619113127_init/migration.sql @@ -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") ); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bfaedb1cb8..d94fc14fc5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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 { diff --git a/styles/globals.css b/styles/globals.css index 97832dab87..84f0f17ecb 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -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; +} diff --git a/yarn.lock b/yarn.lock index e795e6f70f..fe83885049 100644 --- a/yarn.lock +++ b/yarn.lock @@ -53,6 +53,11 @@ codex-tooltip "^1.0.5" nanoid "^3.1.22" +"@editorjs/header@^2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@editorjs/header/-/header-2.6.2.tgz#523b6dda72ff882e53f64325840ee7bfc68ee6b7" + integrity sha512-U1dnT+KGjwFmpWneEEyR2Nqp42hn9iKwQDgRHWQM+y6qx82pg+eAyuIf0QWt2Mluu9uPD2CzNfvJ+pxIuwX8Lw== + "@editorjs/paragraph@^2.8.0": version "2.8.0" resolved "https://registry.yarnpkg.com/@editorjs/paragraph/-/paragraph-2.8.0.tgz#11cc381fcafaf8b9160517ce65d59eee93fc4af9" @@ -230,25 +235,6 @@ 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" @@ -789,6 +775,18 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +editorjs-drag-drop@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/editorjs-drag-drop/-/editorjs-drag-drop-1.1.2.tgz#090eed9875a6bd0d0243aa52c0f30b7052917116" + integrity sha512-vgr0QgYScfIp1kurfh8FmceIAnS+ce3Vdtt5Z2SYTf3j0/eL3B5o5FrwOLJBv9GLumqXETZXbYcCo8Q75c7twQ== + +editorjs-undo@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/editorjs-undo/-/editorjs-undo-2.0.3.tgz#d1f464a08f8cac4602831b396ee60bdea0aad8a9" + integrity sha512-Q0XSOM2I/E7y4nkKEnYr1QuQq6/M+uuY/G0857DZ1/6QDS9OLcwwszbV/vlCxZPMVFD1GDCN+YwLYKFwwO3htg== + dependencies: + vanilla-caret-js "^1.0.1" + electron-to-chromium@^1.4.147: version "1.4.155" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.155.tgz#9c7190bf5e8608c5a5e4953b1ccf0facf3d1b49d" @@ -1975,21 +1973,14 @@ quick-lru@^5.1.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== -react-dom@18.1.0: - version "18.1.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.1.0.tgz#7f6dd84b706408adde05e1df575b3a024d7e8a2f" - integrity sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w== +react-dom@17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" + integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== dependencies: loose-envify "^1.1.0" - scheduler "^0.22.0" - -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: - "@react-editor-js/client" "2.0.6" - "@react-editor-js/server" "2.0.6" + object-assign "^4.1.1" + scheduler "^0.20.2" react-icons@^4.4.0: version "4.4.0" @@ -2001,12 +1992,18 @@ react-is@^16.13.1, react-is@^16.7.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react@18.1.0: - version "18.1.0" - resolved "https://registry.yarnpkg.com/react/-/react-18.1.0.tgz#6f8620382decb17fdc5cc223a115e2adbf104890" - integrity sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ== +react-loader-spinner@^5.1.5: + version "5.1.5" + resolved "https://registry.yarnpkg.com/react-loader-spinner/-/react-loader-spinner-5.1.5.tgz#392ecc2c964363cde0572b8aaa41c29ebbe372ad" + integrity sha512-kHSBa3pNPNzd3UGOiOsHCC33qx5bZA7IHKau+YSTVn36Jib4eBVx/TNMggudbzW5CTnRCbP5bnYU4ACAX3mA6g== + +react@17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" + integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== dependencies: loose-envify "^1.1.0" + object-assign "^4.1.1" read-cache@^1.0.0: version "1.0.0" @@ -2082,12 +2079,13 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -scheduler@^0.22.0: - version "0.22.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.22.0.tgz#83a5d63594edf074add9a7198b1bae76c3db01b8" - integrity sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ== +scheduler@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" + integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== dependencies: loose-envify "^1.1.0" + object-assign "^4.1.1" semver@^6.3.0: version "6.3.0" @@ -2350,6 +2348,11 @@ v8-compile-cache@^2.0.3: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== +vanilla-caret-js@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/vanilla-caret-js/-/vanilla-caret-js-1.0.1.tgz#96cf4e6ee154ca87c813bb4604c90a24555abc35" + integrity sha512-s5hXdyIMmeJ1dzfqvZSEGIXGi5xaHrWKsfRZfNUFVqDLwD2TA5QlPJqFhUhZ4D3wgyf6Pjw5ygUbOpj/QePsSw== + vary@^1: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"