diff --git a/Dockerfile b/Dockerfile index 5cff15dc5..388c5a4ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,9 +5,11 @@ WORKDIR /app ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER RUN yarn global add turbo +RUN apk add tree COPY . . -RUN turbo prune --scope=app --docker +RUN turbo prune --scope=app --scope=plane-deploy --docker +CMD tree -I node_modules/ # Add lockfile and package.json's of isolated subworkspace FROM node:18-alpine AS installer @@ -21,14 +23,14 @@ COPY --from=builder /app/out/json/ . COPY --from=builder /app/out/yarn.lock ./yarn.lock RUN yarn install -# Build the project +# # Build the project COPY --from=builder /app/out/full/ . COPY turbo.json turbo.json COPY replace-env-vars.sh /usr/local/bin/ USER root RUN chmod +x /usr/local/bin/replace-env-vars.sh -RUN yarn turbo run build --filter=app +RUN yarn turbo run build ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \ BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL @@ -96,11 +98,16 @@ RUN adduser --system --uid 1001 captain COPY --from=installer /app/apps/app/next.config.js . COPY --from=installer /app/apps/app/package.json . +COPY --from=installer /app/apps/space/next.config.js . +COPY --from=installer /app/apps/space/package.json . COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone ./ COPY --from=installer --chown=captain:plane /app/apps/app/.next/static ./apps/app/.next/static +COPY --from=installer --chown=captain:plane /app/apps/space/.next/standalone ./ +COPY --from=installer --chown=captain:plane /app/apps/space/.next ./apps/space/.next + ENV NEXT_TELEMETRY_DISABLED 1 # RUN rm /etc/nginx/conf.d/default.conf diff --git a/apps/app/Dockerfile.web b/apps/app/Dockerfile.web index e0b5f29c1..2b28e1fd1 100644 --- a/apps/app/Dockerfile.web +++ b/apps/app/Dockerfile.web @@ -33,8 +33,8 @@ RUN yarn turbo run build --filter=app ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \ BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL - -RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL} +ENV NEXT_PUBLIC_DEPLOY_URL=${NEXT_PUBLIC_DEPLOY_URL} +RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL} app FROM node:18-alpine AS runner WORKDIR /app diff --git a/apps/app/components/core/modals/gpt-assistant-modal.tsx b/apps/app/components/core/modals/gpt-assistant-modal.tsx index a3b748d66..35f282040 100644 --- a/apps/app/components/core/modals/gpt-assistant-modal.tsx +++ b/apps/app/components/core/modals/gpt-assistant-modal.tsx @@ -140,14 +140,14 @@ export const GptAssistantModal: React.FC = ({ return (
{((content && content !== "") || (htmlContent && htmlContent !== "

")) && (
Content: ${content}

`} customClassName="-m-3" noBorder @@ -161,6 +161,7 @@ export const GptAssistantModal: React.FC = ({
Response: ${response}

`} customClassName="-mx-3 -my-3" noBorder @@ -179,11 +180,10 @@ export const GptAssistantModal: React.FC = ({ type="text" name="task" register={register} - placeholder={`${ - content && content !== "" + placeholder={`${content && content !== "" ? "Tell AI what action to perform on this content..." : "Ask AI anything..." - }`} + }`} autoComplete="off" />
@@ -219,8 +219,8 @@ export const GptAssistantModal: React.FC = ({ {isSubmitting ? "Generating response..." : response === "" - ? "Generate response" - : "Generate again"} + ? "Generate response" + : "Generate again"}
diff --git a/apps/app/components/core/views/board-view/single-issue.tsx b/apps/app/components/core/views/board-view/single-issue.tsx index 4a1f25fe0..b676e809c 100644 --- a/apps/app/components/core/views/board-view/single-issue.tsx +++ b/apps/app/components/core/views/board-view/single-issue.tsx @@ -86,7 +86,8 @@ export const SingleBoardIssue: React.FC = ({ }) => { // context menu const [contextMenu, setContextMenu] = useState(false); - const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }); + const [contextMenuPosition, setContextMenuPosition] = useState(null); + const [isMenuActive, setIsMenuActive] = useState(false); const [isDropdownActive, setIsDropdownActive] = useState(false); @@ -201,7 +202,7 @@ export const SingleBoardIssue: React.FC = ({ return ( <> = ({ onContextMenu={(e) => { e.preventDefault(); setContextMenu(true); - setContextMenuPosition({ x: e.pageX, y: e.pageY }); + setContextMenuPosition(e); }} >
diff --git a/apps/app/components/core/views/list-view/single-issue.tsx b/apps/app/components/core/views/list-view/single-issue.tsx index 7d1cea37e..eafe74612 100644 --- a/apps/app/components/core/views/list-view/single-issue.tsx +++ b/apps/app/components/core/views/list-view/single-issue.tsx @@ -33,7 +33,7 @@ import { } from "@heroicons/react/24/outline"; import { LayerDiagonalIcon } from "components/icons"; // helpers -import { copyTextToClipboard, truncateText } from "helpers/string.helper"; +import { copyTextToClipboard } from "helpers/string.helper"; import { handleIssuesMutation } from "constants/issue"; // types import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types"; @@ -71,7 +71,7 @@ export const SingleListIssue: React.FC = ({ }) => { // context menu const [contextMenu, setContextMenu] = useState(false); - const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }); + const [contextMenuPosition, setContextMenuPosition] = useState(null); const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId } = router.query; @@ -167,7 +167,7 @@ export const SingleListIssue: React.FC = ({ return ( <> = ({ onContextMenu={(e) => { e.preventDefault(); setContextMenu(true); - setContextMenuPosition({ x: e.pageX, y: e.pageY }); + setContextMenuPosition(e); }} >
diff --git a/apps/app/components/inbox/inbox-main-content.tsx b/apps/app/components/inbox/inbox-main-content.tsx index af1234859..bd4f0ab01 100644 --- a/apps/app/components/inbox/inbox-main-content.tsx +++ b/apps/app/components/inbox/inbox-main-content.tsx @@ -293,6 +293,7 @@ export const InboxMainContent: React.FC = () => {
= ({ issueId, user }) => { return (
= ({ issueId, user, disabled = false }) control={control} render={({ field: { value, onChange } }) => ( void; handleCommentDeletion: (comment: string) => void; }; -export const CommentCard: React.FC = ({ comment, onSubmit, handleCommentDeletion }) => { +export const CommentCard: React.FC = ({ comment, workspaceSlug, onSubmit, handleCommentDeletion }) => { const { user } = useUser(); const editorRef = React.useRef(null); @@ -109,6 +110,7 @@ export const CommentCard: React.FC = ({ comment, onSubmit, handleCommentD >
= ({ comment, onSubmit, handleCommentD
Promise; isAllowed: boolean; } @@ -31,6 +32,7 @@ export interface IssueDetailsProps { export const IssueDescriptionForm: FC = ({ issue, handleFormSubmit, + workspaceSlug, isAllowed, }) => { const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); @@ -69,11 +71,15 @@ export const IssueDescriptionForm: FC = ({ useEffect(() => { if (isSubmitting === "submitted") { + setShowAlert(false); setTimeout(async () => { setIsSubmitting("saved"); }, 2000); + } else if (isSubmitting === "submitting") { + setShowAlert(true); } - }, [isSubmitting]); + }, [isSubmitting, setShowAlert]); + // reset form values useEffect(() => { @@ -112,9 +118,8 @@ export const IssueDescriptionForm: FC = ({ {characterLimit && (
255 ? "text-red-500" : "" - }`} + className={`${watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : "" + }`} > {watch("name").length} @@ -134,16 +139,19 @@ export const IssueDescriptionForm: FC = ({ { + setShowAlert(true); setIsSubmitting("submitting"); onChange(description_html); setValue("description", description); @@ -156,9 +164,8 @@ export const IssueDescriptionForm: FC = ({ }} />
{isSubmitting === "submitting" ? "Saving..." : "Saved"}
diff --git a/apps/app/components/issues/form.tsx b/apps/app/components/issues/form.tsx index 0e73ca349..c15c61d57 100644 --- a/apps/app/components/issues/form.tsx +++ b/apps/app/components/issues/form.tsx @@ -370,6 +370,7 @@ export const IssueForm: FC = ({ return ( = ({
) : null} = ({ label, maxDate, minDate, onChange, value }) => ( - {({ open }) => ( + {({ close }) => ( <> @@ -52,6 +52,8 @@ export const IssueDateSelect: React.FC = ({ label, maxDate, minDate, onCh onChange={(val) => { if (!val) onChange(""); else onChange(renderDateFormat(val)); + + close(); }} dateFormat="dd-MM-yyyy" minDate={minDate} diff --git a/apps/app/components/modules/form.tsx b/apps/app/components/modules/form.tsx index 0b36176c5..2715ba266 100644 --- a/apps/app/components/modules/form.tsx +++ b/apps/app/components/modules/form.tsx @@ -1,15 +1,11 @@ -import { useEffect, useState } from "react"; +import { useEffect } from "react"; // react-hook-form import { Controller, useForm } from "react-hook-form"; -// hooks -import useToast from "hooks/use-toast"; // components import { ModuleLeadSelect, ModuleMembersSelect, ModuleStatusSelect } from "components/modules"; // ui import { DateSelect, Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; -// helper -import { isDateRangeValid } from "helpers/date-time.helper"; // types import { IModule } from "types"; @@ -29,8 +25,6 @@ const defaultValues: Partial = { }; export const ModuleForm: React.FC = ({ handleFormSubmit, handleClose, status, data }) => { - const [isDateValid, setIsDateValid] = useState(true); - const { setToastAlert } = useToast(); const { register, formState: { errors, isSubmitting }, @@ -57,6 +51,15 @@ export const ModuleForm: React.FC = ({ handleFormSubmit, handleClose, sta }); }, [data, reset]); + const startDate = watch("start_date"); + const targetDate = watch("target_date"); + + const minDate = startDate ? new Date(startDate) : null; + minDate?.setDate(minDate.getDate()); + + const maxDate = targetDate ? new Date(targetDate) : null; + maxDate?.setDate(maxDate.getDate()); + return (
@@ -103,20 +106,8 @@ export const ModuleForm: React.FC = ({ handleFormSubmit, handleClose, sta value={value} onChange={(val) => { onChange(val); - if (val && watch("target_date")) { - if (isDateRangeValid(val, `${watch("target_date")}`)) { - setIsDateValid(true); - } else { - setIsDateValid(false); - setToastAlert({ - type: "error", - title: "Error!", - message: - "The date you have entered is invalid. Please check and enter a valid date.", - }); - } - } }} + maxDate={maxDate ?? undefined} /> )} /> @@ -129,20 +120,8 @@ export const ModuleForm: React.FC = ({ handleFormSubmit, handleClose, sta value={value} onChange={(val) => { onChange(val); - if (watch("start_date") && val) { - if (isDateRangeValid(`${watch("start_date")}`, val)) { - setIsDateValid(true); - } else { - setIsDateValid(false); - setToastAlert({ - type: "error", - title: "Error!", - message: - "The date you have entered is invalid. Please check and enter a valid date.", - }); - } - } }} + minDate={minDate ?? undefined} /> )} /> @@ -166,7 +145,7 @@ export const ModuleForm: React.FC = ({ handleFormSubmit, handleClose, sta
Cancel - + {status ? isSubmitting ? "Updating Module..." diff --git a/apps/app/components/pages/create-update-block-inline.tsx b/apps/app/components/pages/create-update-block-inline.tsx index 496434369..3e0f604a3 100644 --- a/apps/app/components/pages/create-update-block-inline.tsx +++ b/apps/app/components/pages/create-update-block-inline.tsx @@ -231,9 +231,9 @@ export const CreateUpdateBlockInline: React.FC = ({ description: !data.description || data.description === "" ? { - type: "doc", - content: [{ type: "paragraph" }], - } + type: "doc", + content: [{ type: "paragraph" }], + } : data.description, description_html: data.description_html ?? "

", }); @@ -292,6 +292,7 @@ export const CreateUpdateBlockInline: React.FC = ({ if (!data) return (

"} debouncedUpdatesEnabled={false} @@ -311,12 +312,11 @@ export const CreateUpdateBlockInline: React.FC = ({ return ( 0 ? value - : watch("description_html") && watch("description_html") !== "" - ? watch("description_html") : { type: "doc", content: [{ type: "paragraph" }] } } debouncedUpdatesEnabled={false} @@ -334,9 +334,8 @@ export const CreateUpdateBlockInline: React.FC = ({
diff --git a/apps/app/components/pages/single-page-block.tsx b/apps/app/components/pages/single-page-block.tsx index 8a13e3ea1..0c192990a 100644 --- a/apps/app/components/pages/single-page-block.tsx +++ b/apps/app/components/pages/single-page-block.tsx @@ -456,6 +456,7 @@ export const SinglePageBlock: React.FC = ({ {showBlockDetails ? block.description_html.length > 7 && ( = ({ userProfile }) => ( -
+

Issues by Priority

{userProfile ? ( -
+
{userProfile.priority_distribution.length > 0 ? ( ({ @@ -63,7 +63,7 @@ export const ProfilePriorityDistribution: React.FC = ({ userProfile }) => }} /> ) : ( -
+
= ({ stateDistribution, u if (!userProfile) return null; return ( -
+

Issues by State

-
+
{userProfile.state_distribution.length > 0 ? (
diff --git a/apps/app/components/project/delete-project-modal.tsx b/apps/app/components/project/delete-project-modal.tsx index 8edca368e..5b271d99d 100644 --- a/apps/app/components/project/delete-project-modal.tsx +++ b/apps/app/components/project/delete-project-modal.tsx @@ -1,9 +1,11 @@ -import React, { useEffect, useState } from "react"; +import React from "react"; import { useRouter } from "next/router"; import { mutate } from "swr"; +// react-hook-form +import { Controller, useForm } from "react-hook-form"; // headless ui import { Dialog, Transition } from "@headlessui/react"; // services @@ -27,6 +29,11 @@ type TConfirmProjectDeletionProps = { user: ICurrentUserResponse | undefined; }; +const defaultValues = { + projectName: "", + confirmDelete: "", +}; + export const DeleteProjectModal: React.FC = ({ isOpen, data, @@ -34,51 +41,41 @@ export const DeleteProjectModal: React.FC = ({ onSuccess, user, }) => { - const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const [confirmProjectName, setConfirmProjectName] = useState(""); - const [confirmDeleteMyProject, setConfirmDeleteMyProject] = useState(false); - const [selectedProject, setSelectedProject] = useState(null); - const router = useRouter(); const { workspaceSlug } = router.query; const { setToastAlert } = useToast(); - const canDelete = confirmProjectName === data?.name && confirmDeleteMyProject; + const { + control, + formState: { isSubmitting }, + handleSubmit, + reset, + watch, + } = useForm({ defaultValues }); - useEffect(() => { - if (data) setSelectedProject(data); - else { - const timer = setTimeout(() => { - setSelectedProject(null); - clearTimeout(timer); - }, 300); - } - }, [data]); + const canDelete = + watch("projectName") === data?.name && watch("confirmDelete") === "delete my project"; const handleClose = () => { - setIsDeleteLoading(false); - const timer = setTimeout(() => { - setConfirmProjectName(""); - setConfirmDeleteMyProject(false); + reset(defaultValues); clearTimeout(timer); }, 350); + onClose(); }; - const handleDeletion = async () => { + const onSubmit = async () => { if (!data || !workspaceSlug || !canDelete) return; - setIsDeleteLoading(true); - await projectService - .deleteProject(workspaceSlug as string, data.id, user) + .deleteProject(workspaceSlug.toString(), data.id, user) .then(() => { handleClose(); mutate( - PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }), + PROJECTS_LIST(workspaceSlug.toString(), { is_favorite: "all" }), (prevData) => prevData?.filter((project: IProject) => project.id !== data.id), false ); @@ -91,8 +88,7 @@ export const DeleteProjectModal: React.FC = ({ title: "Error!", message: "Something went wrong. Please try again later.", }) - ) - .finally(() => setIsDeleteLoading(false)); + ); }; return ( @@ -122,7 +118,7 @@ export const DeleteProjectModal: React.FC = ({ leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > -
+
= ({

Are you sure you want to delete project{" "} - {selectedProject?.name}? - All of the data related to the project will be permanently removed. This - action cannot be undone + {data?.name}? All of the + data related to the project will be permanently removed. This action cannot be + undone

Enter the project name{" "} - - {selectedProject?.name} - {" "} - to continue: + {data?.name} to + continue:

- { - setConfirmProjectName(e.target.value); - }} + ( + + )} />
@@ -167,31 +164,27 @@ export const DeleteProjectModal: React.FC = ({ delete my project{" "} below:

- { - if (e.target.value === "delete my project") { - setConfirmDeleteMyProject(true); - } else { - setConfirmDeleteMyProject(false); - } - }} - name="typeDelete" + ( + + )} />
Cancel - - {isDeleteLoading ? "Deleting..." : "Delete Project"} + + {isSubmitting ? "Deleting..." : "Delete Project"}
-
+
diff --git a/apps/app/components/tiptap/bubble-menu/index.tsx b/apps/app/components/tiptap/bubble-menu/index.tsx index 259b5ecea..e68900782 100644 --- a/apps/app/components/tiptap/bubble-menu/index.tsx +++ b/apps/app/components/tiptap/bubble-menu/index.tsx @@ -15,36 +15,36 @@ export interface BubbleMenuItem { type EditorBubbleMenuProps = Omit; -export const EditorBubbleMenu: FC = (props) => { +export const EditorBubbleMenu: FC = (props: any) => { const items: BubbleMenuItem[] = [ { name: "bold", - isActive: () => props.editor.isActive("bold"), - command: () => props.editor.chain().focus().toggleBold().run(), + isActive: () => props.editor?.isActive("bold"), + command: () => props.editor?.chain().focus().toggleBold().run(), icon: BoldIcon, }, { name: "italic", - isActive: () => props.editor.isActive("italic"), - command: () => props.editor.chain().focus().toggleItalic().run(), + isActive: () => props.editor?.isActive("italic"), + command: () => props.editor?.chain().focus().toggleItalic().run(), icon: ItalicIcon, }, { name: "underline", - isActive: () => props.editor.isActive("underline"), - command: () => props.editor.chain().focus().toggleUnderline().run(), + isActive: () => props.editor?.isActive("underline"), + command: () => props.editor?.chain().focus().toggleUnderline().run(), icon: UnderlineIcon, }, { name: "strike", - isActive: () => props.editor.isActive("strike"), - command: () => props.editor.chain().focus().toggleStrike().run(), + isActive: () => props.editor?.isActive("strike"), + command: () => props.editor?.chain().focus().toggleStrike().run(), icon: StrikethroughIcon, }, { name: "code", - isActive: () => props.editor.isActive("code"), - command: () => props.editor.chain().focus().toggleCode().run(), + isActive: () => props.editor?.isActive("code"), + command: () => props.editor?.chain().focus().toggleCode().run(), icon: CodeIcon, }, ]; @@ -78,7 +78,7 @@ export const EditorBubbleMenu: FC = (props) => { className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl" > { setIsNodeSelectorOpen(!isNodeSelectorOpen); @@ -86,7 +86,7 @@ export const EditorBubbleMenu: FC = (props) => { }} /> { setIsLinkSelectorOpen(!isLinkSelectorOpen); diff --git a/apps/app/components/tiptap/bubble-menu/link-selector.tsx b/apps/app/components/tiptap/bubble-menu/link-selector.tsx index 4a2859155..1596870f7 100644 --- a/apps/app/components/tiptap/bubble-menu/link-selector.tsx +++ b/apps/app/components/tiptap/bubble-menu/link-selector.tsx @@ -1,17 +1,27 @@ import { Editor } from "@tiptap/core"; import { Check, Trash } from "lucide-react"; -import { Dispatch, FC, SetStateAction, useEffect, useRef } from "react"; +import { Dispatch, FC, SetStateAction, useCallback, useEffect, useRef } from "react"; import { cn } from "../utils"; - +import isValidHttpUrl from "./utils/link-validator"; interface LinkSelectorProps { editor: Editor; isOpen: boolean; setIsOpen: Dispatch>; } + export const LinkSelector: FC = ({ editor, isOpen, setIsOpen }) => { const inputRef = useRef(null); + const onLinkSubmit = useCallback(() => { + const input = inputRef.current; + const url = input?.value; + if (url && isValidHttpUrl(url)) { + editor.chain().focus().setLink({ href: url }).run(); + setIsOpen(false); + } + }, [editor, inputRef, setIsOpen]); + useEffect(() => { inputRef.current && inputRef.current?.focus(); }); @@ -38,15 +48,13 @@ export const LinkSelector: FC = ({ editor, isOpen, setIsOpen

{isOpen && ( -
{ - e.preventDefault(); - const form = e.target as HTMLFormElement; - const input = form.elements[0] as HTMLInputElement; - editor.chain().focus().setLink({ href: input.value }).run(); - setIsOpen(false); - }} +
{ + if (e.key === "Enter") { + e.preventDefault(); onLinkSubmit(); + } + }} > = ({ editor, isOpen, setIsOpen /> {editor.getAttributes("link").href ? ( ) : ( - )} - +
)}
); diff --git a/apps/app/components/tiptap/bubble-menu/utils/link-validator.tsx b/apps/app/components/tiptap/bubble-menu/utils/link-validator.tsx new file mode 100644 index 000000000..5b05811d6 --- /dev/null +++ b/apps/app/components/tiptap/bubble-menu/utils/link-validator.tsx @@ -0,0 +1,12 @@ +export default function isValidHttpUrl(string: string): boolean { + let url; + + try { + url = new URL(string); + } catch (_) { + return false; + } + + return url.protocol === "http:" || url.protocol === "https:"; +} + diff --git a/apps/app/components/tiptap/extensions/image-resize.tsx b/apps/app/components/tiptap/extensions/image-resize.tsx new file mode 100644 index 000000000..7b2d1a2d3 --- /dev/null +++ b/apps/app/components/tiptap/extensions/image-resize.tsx @@ -0,0 +1,57 @@ +import { Editor } from "@tiptap/react"; +import Moveable from "react-moveable"; + +export const ImageResizer = ({ editor }: { editor: Editor }) => { + const updateMediaSize = () => { + const imageInfo = document.querySelector( + ".ProseMirror-selectednode", + ) as HTMLImageElement; + if (imageInfo) { + const selection = editor.state.selection; + editor.commands.setImage({ + src: imageInfo.src, + width: Number(imageInfo.style.width.replace("px", "")), + height: Number(imageInfo.style.height.replace("px", "")), + } as any); + editor.commands.setNodeSelection(selection.from); + } + }; + + return ( + <> + { + delta[0] && (target!.style.width = `${width}px`); + delta[1] && (target!.style.height = `${height}px`); + }} + onResizeEnd={() => { + updateMediaSize(); + }} + scalable={true} + renderDirections={["w", "e"]} + onScale={({ + target, + transform, + }: + any) => { + target!.style.transform = transform; + }} + /> + + ); +}; + diff --git a/apps/app/components/tiptap/extensions/index.tsx b/apps/app/components/tiptap/extensions/index.tsx index 45dee0929..2c5ffd10a 100644 --- a/apps/app/components/tiptap/extensions/index.tsx +++ b/apps/app/components/tiptap/extensions/index.tsx @@ -1,7 +1,6 @@ import StarterKit from "@tiptap/starter-kit"; import HorizontalRule from "@tiptap/extension-horizontal-rule"; import TiptapLink from "@tiptap/extension-link"; -import TiptapImage from "@tiptap/extension-image"; import Placeholder from "@tiptap/extension-placeholder"; import TiptapUnderline from "@tiptap/extension-underline"; import TextStyle from "@tiptap/extension-text-style"; @@ -18,18 +17,13 @@ import { InputRule } from "@tiptap/core"; import ts from "highlight.js/lib/languages/typescript"; import "highlight.js/styles/github-dark.css"; -import UploadImagesPlugin from "../plugins/upload-image"; import UniqueID from "@tiptap-pro/extension-unique-id"; +import UpdatedImage from "./updated-image"; +import isValidHttpUrl from "../bubble-menu/utils/link-validator"; lowlight.registerLanguage("ts", ts); -const CustomImage = TiptapImage.extend({ - addProseMirrorPlugins() { - return [UploadImagesPlugin()]; - }, -}); - -export const TiptapExtensions = [ +export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) => [ StarterKit.configure({ bulletList: { HTMLAttributes: { @@ -93,13 +87,14 @@ export const TiptapExtensions = [ }, }), TiptapLink.configure({ + protocols: ["http", "https"], + validate: (url) => isValidHttpUrl(url), HTMLAttributes: { class: "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", }, }), - CustomImage.configure({ - allowBase64: true, + UpdatedImage.configure({ HTMLAttributes: { class: "rounded-lg border border-custom-border-300", }, @@ -117,7 +112,7 @@ export const TiptapExtensions = [ UniqueID.configure({ types: ["image"], }), - SlashCommand, + SlashCommand(workspaceSlug, setIsSubmitting), TiptapUnderline, TextStyle, Color, diff --git a/apps/app/components/tiptap/extensions/updated-image.tsx b/apps/app/components/tiptap/extensions/updated-image.tsx new file mode 100644 index 000000000..01648dcd7 --- /dev/null +++ b/apps/app/components/tiptap/extensions/updated-image.tsx @@ -0,0 +1,22 @@ +import Image from "@tiptap/extension-image"; +import TrackImageDeletionPlugin from "../plugins/delete-image"; +import UploadImagesPlugin from "../plugins/upload-image"; + +const UpdatedImage = Image.extend({ + addProseMirrorPlugins() { + return [UploadImagesPlugin(), TrackImageDeletionPlugin()]; + }, + addAttributes() { + return { + ...this.parent?.(), + width: { + default: '35%', + }, + height: { + default: null, + }, + }; + }, +}); + +export default UpdatedImage; diff --git a/apps/app/components/tiptap/index.tsx b/apps/app/components/tiptap/index.tsx index 72a914bc9..418449c08 100644 --- a/apps/app/components/tiptap/index.tsx +++ b/apps/app/components/tiptap/index.tsx @@ -1,14 +1,10 @@ -// @ts-nocheck import { useEditor, EditorContent, Editor } from "@tiptap/react"; import { useDebouncedCallback } from "use-debounce"; import { EditorBubbleMenu } from "./bubble-menu"; import { TiptapExtensions } from "./extensions"; import { TiptapEditorProps } from "./props"; -import { Node } from "@tiptap/pm/model"; -import { Editor as CoreEditor } from "@tiptap/core"; -import { useCallback, useImperativeHandle, useRef } from "react"; -import { EditorState } from "@tiptap/pm/state"; -import fileService from "services/file.service"; +import { useImperativeHandle, useRef } from "react"; +import { ImageResizer } from "./extensions/image-resize"; export interface ITiptapRichTextEditor { value: string; @@ -18,6 +14,8 @@ export interface ITiptapRichTextEditor { editorContentCustomClassNames?: string; onChange?: (json: any, html: string) => void; setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void; + setShouldShowAlert?: (showAlert: boolean) => void; + workspaceSlug: string; editable?: boolean; forwardedRef?: any; debouncedUpdatesEnabled?: boolean; @@ -30,22 +28,24 @@ const Tiptap = (props: ITiptapRichTextEditor) => { forwardedRef, editable, setIsSubmitting, + setShouldShowAlert, editorContentCustomClassNames, value, noBorder, + workspaceSlug, borderOnFocus, customClassName, } = props; const editor = useEditor({ editable: editable ?? true, - editorProps: TiptapEditorProps, - extensions: TiptapExtensions, + editorProps: TiptapEditorProps(workspaceSlug, setIsSubmitting), + extensions: TiptapExtensions(workspaceSlug, setIsSubmitting), content: value, onUpdate: async ({ editor }) => { // for instant feedback loop setIsSubmitting?.("submitting"); - checkForNodeDeletions(editor); + setShouldShowAlert?.(true); if (debouncedUpdatesEnabled) { debouncedUpdates({ onChange, editor }); } else { @@ -65,45 +65,6 @@ const Tiptap = (props: ITiptapRichTextEditor) => { }, })); - const previousState = useRef(); - - const onNodeDeleted = useCallback(async (node: Node) => { - if (node.type.name === "image") { - const assetUrlWithWorkspaceId = new URL(node.attrs.src).pathname.substring(1); - const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId); - if (resStatus === 204) { - console.log("file deleted successfully"); - } - } - }, []); - - const checkForNodeDeletions = useCallback( - (editor: CoreEditor) => { - const prevNodesById: Record = {}; - previousState.current?.doc.forEach((node) => { - if (node.attrs.id) { - prevNodesById[node.attrs.id] = node; - } - }); - - const nodesById: Record = {}; - editor.state?.doc.forEach((node) => { - if (node.attrs.id) { - nodesById[node.attrs.id] = node; - } - }); - - previousState.current = editor.state; - - for (const [id, node] of Object.entries(prevNodesById)) { - if (nodesById[id] === undefined) { - onNodeDeleted(node); - } - } - }, - [onNodeDeleted] - ); - const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => { setTimeout(async () => { if (onChange) { @@ -112,10 +73,9 @@ const Tiptap = (props: ITiptapRichTextEditor) => { }, 500); }, 1000); - const editorClassNames = `relative w-full max-w-screen-lg mt-2 p-3 relative focus:outline-none rounded-lg - ${noBorder ? "" : "border border-custom-border-200"} ${ - borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0" - } ${customClassName}`; + const editorClassNames = `relative w-full max-w-screen-lg sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md + ${noBorder ? "" : "border border-custom-border-200"} ${borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0" + } ${customClassName}`; if (!editor) return null; editorRef.current = editor; @@ -131,6 +91,7 @@ const Tiptap = (props: ITiptapRichTextEditor) => { {editor && }
+ {editor?.isActive("image") && }
); diff --git a/apps/app/components/tiptap/plugins/delete-image.tsx b/apps/app/components/tiptap/plugins/delete-image.tsx new file mode 100644 index 000000000..57ab65c63 --- /dev/null +++ b/apps/app/components/tiptap/plugins/delete-image.tsx @@ -0,0 +1,56 @@ +import { Plugin, PluginKey } from "@tiptap/pm/state"; +import { Node as ProseMirrorNode } from '@tiptap/pm/model'; +import fileService from "services/file.service"; + +const deleteKey = new PluginKey("delete-image"); + +const TrackImageDeletionPlugin = () => + new Plugin({ + key: deleteKey, + appendTransaction: (transactions, oldState, newState) => { + transactions.forEach((transaction) => { + if (!transaction.docChanged) return; + + const removedImages: ProseMirrorNode[] = []; + + oldState.doc.descendants((oldNode, oldPos) => { + if (oldNode.type.name !== 'image') return; + + if (!newState.doc.resolve(oldPos).parent) return; + const newNode = newState.doc.nodeAt(oldPos); + + // Check if the node has been deleted or replaced + if (!newNode || newNode.type.name !== 'image') { + // Check if the node still exists elsewhere in the document + let nodeExists = false; + newState.doc.descendants((node) => { + if (node.attrs.id === oldNode.attrs.id) { + nodeExists = true; + } + }); + + if (!nodeExists) { + removedImages.push(oldNode as ProseMirrorNode); + } + } + }); + + removedImages.forEach((node) => { + const src = node.attrs.src; + onNodeDeleted(src); + }); + }); + + return null; + }, + }); + +export default TrackImageDeletionPlugin; + +async function onNodeDeleted(src: string) { + const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1); + const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId); + if (resStatus === 204) { + console.log("Image deleted successfully"); + } +} diff --git a/apps/app/components/tiptap/plugins/upload-image.tsx b/apps/app/components/tiptap/plugins/upload-image.tsx index ed44aa379..0657bc82b 100644 --- a/apps/app/components/tiptap/plugins/upload-image.tsx +++ b/apps/app/components/tiptap/plugins/upload-image.tsx @@ -57,7 +57,7 @@ function findPlaceholder(state: EditorState, id: {}) { return found.length ? found[0].from : null; } -export async function startImageUpload(file: File, view: EditorView, pos: number) { +export async function startImageUpload(file: File, view: EditorView, pos: number, workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) { if (!file.type.includes("image/")) { return; } else if (file.size / 1024 / 1024 > 20) { @@ -82,7 +82,11 @@ export async function startImageUpload(file: File, view: EditorView, pos: number view.dispatch(tr); }; - const src = await UploadImageHandler(file); + if (!workspaceSlug) { + return; + } + setIsSubmitting?.("submitting") + const src = await UploadImageHandler(file, workspaceSlug); const { schema } = view.state; pos = findPlaceholder(view.state, id); @@ -96,7 +100,10 @@ export async function startImageUpload(file: File, view: EditorView, pos: number view.dispatch(transaction); } -const UploadImageHandler = (file: File): Promise => { +const UploadImageHandler = (file: File, workspaceSlug: string): Promise => { + if (!workspaceSlug) { + return Promise.reject("Workspace slug is missing"); + } try { const formData = new FormData(); formData.append("asset", file); @@ -104,7 +111,7 @@ const UploadImageHandler = (file: File): Promise => { return new Promise(async (resolve, reject) => { const imageUrl = await fileService - .uploadFile("plane", formData) + .uploadFile(workspaceSlug, formData) .then((response) => response.asset); const image = new Image(); diff --git a/apps/app/components/tiptap/props.tsx b/apps/app/components/tiptap/props.tsx index 1ffbebe6d..d50fc29b0 100644 --- a/apps/app/components/tiptap/props.tsx +++ b/apps/app/components/tiptap/props.tsx @@ -1,56 +1,56 @@ import { EditorProps } from "@tiptap/pm/view"; import { startImageUpload } from "./plugins/upload-image"; -export const TiptapEditorProps: EditorProps = { - attributes: { - class: `prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none`, - }, - handleDOMEvents: { - keydown: (_view, event) => { - // prevent default event listeners from firing when slash command is active - if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) { - const slashCommand = document.querySelector("#slash-command"); - if (slashCommand) { - return true; - } - } +export function TiptapEditorProps(workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void): EditorProps { + return { + attributes: { + class: `prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none`, }, - }, - handlePaste: (view, event) => { - if ( - event.clipboardData && - event.clipboardData.files && - event.clipboardData.files[0] - ) { - event.preventDefault(); - const file = event.clipboardData.files[0]; - const pos = view.state.selection.from; - - startImageUpload(file, view, pos); - return true; - } - return false; - }, - handleDrop: (view, event, _slice, moved) => { - if ( - !moved && - event.dataTransfer && - event.dataTransfer.files && - event.dataTransfer.files[0] - ) { - event.preventDefault(); - const file = event.dataTransfer.files[0]; - const coordinates = view.posAtCoords({ - left: event.clientX, - top: event.clientY, - }); - // here we deduct 1 from the pos or else the image will create an extra node - if (coordinates) { - startImageUpload(file, view, coordinates.pos - 1); + handleDOMEvents: { + keydown: (_view, event) => { + // prevent default event listeners from firing when slash command is active + if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) { + const slashCommand = document.querySelector("#slash-command"); + if (slashCommand) { + return true; + } + } + }, + }, + handlePaste: (view, event) => { + if ( + event.clipboardData && + event.clipboardData.files && + event.clipboardData.files[0] + ) { + event.preventDefault(); + const file = event.clipboardData.files[0]; + const pos = view.state.selection.from; + startImageUpload(file, view, pos, workspaceSlug, setIsSubmitting); + return true; } - return true; - } - return false; - }, -}; - + return false; + }, + handleDrop: (view, event, _slice, moved) => { + if ( + !moved && + event.dataTransfer && + event.dataTransfer.files && + event.dataTransfer.files[0] + ) { + event.preventDefault(); + const file = event.dataTransfer.files[0]; + const coordinates = view.posAtCoords({ + left: event.clientX, + top: event.clientY, + }); + // here we deduct 1 from the pos or else the image will create an extra node + if (coordinates) { + startImageUpload(file, view, coordinates.pos - 1, workspaceSlug, setIsSubmitting); + } + return true; + } + return false; + }, + }; +} diff --git a/apps/app/components/tiptap/slash-command/index.tsx b/apps/app/components/tiptap/slash-command/index.tsx index 7c686e06b..38f5c9c0a 100644 --- a/apps/app/components/tiptap/slash-command/index.tsx +++ b/apps/app/components/tiptap/slash-command/index.tsx @@ -52,7 +52,7 @@ const Command = Extension.create({ }, }); -const getSuggestionItems = ({ query }: { query: string }) => +const getSuggestionItems = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) => ({ query }: { query: string }) => [ { title: "Text", @@ -163,7 +163,7 @@ const getSuggestionItems = ({ query }: { query: string }) => if (input.files?.length) { const file = input.files[0]; const pos = editor.view.state.selection.from; - startImageUpload(file, editor.view, pos); + startImageUpload(file, editor.view, pos, workspaceSlug, setIsSubmitting); } }; input.click(); @@ -328,11 +328,12 @@ const renderItems = () => { }; }; -const SlashCommand = Command.configure({ - suggestion: { - items: getSuggestionItems, - render: renderItems, - }, -}); +export const SlashCommand = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) => + Command.configure({ + suggestion: { + items: getSuggestionItems(workspaceSlug, setIsSubmitting), + render: renderItems, + }, + }); export default SlashCommand; diff --git a/apps/app/components/ui/dropdowns/context-menu.tsx b/apps/app/components/ui/dropdowns/context-menu.tsx index d7ecb4de7..78df25ec9 100644 --- a/apps/app/components/ui/dropdowns/context-menu.tsx +++ b/apps/app/components/ui/dropdowns/context-menu.tsx @@ -1,47 +1,76 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useRef } from "react"; import Link from "next/link"; +// hooks +import useOutsideClickDetector from "hooks/use-outside-click-detector"; + type Props = { - position: { - x: number; - y: number; - }; + clickEvent: React.MouseEvent | null; children: React.ReactNode; title?: string | JSX.Element; isOpen: boolean; setIsOpen: React.Dispatch>; }; -const ContextMenu = ({ position, children, title, isOpen, setIsOpen }: Props) => { +const ContextMenu = ({ clickEvent, children, title, isOpen, setIsOpen }: Props) => { + const contextMenuRef = useRef(null); + + // Close the context menu when clicked outside + useOutsideClickDetector(contextMenuRef, () => { + if (isOpen) setIsOpen(false); + }); + useEffect(() => { const hideContextMenu = () => { if (isOpen) setIsOpen(false); }; - window.addEventListener("click", hideContextMenu); - window.addEventListener("keydown", (e: KeyboardEvent) => { + const escapeKeyEvent = (e: KeyboardEvent) => { if (e.key === "Escape") hideContextMenu(); - }); + }; + + window.addEventListener("click", hideContextMenu); + window.addEventListener("keydown", escapeKeyEvent); return () => { window.removeEventListener("click", hideContextMenu); - window.removeEventListener("keydown", hideContextMenu); + window.removeEventListener("keydown", escapeKeyEvent); }; }, [isOpen, setIsOpen]); + useEffect(() => { + const contextMenu = contextMenuRef.current; + + if (contextMenu && isOpen) { + const contextMenuWidth = contextMenu.clientWidth; + const contextMenuHeight = contextMenu.clientHeight; + + const clickX = clickEvent?.pageX || 0; + const clickY = clickEvent?.pageY || 0; + + let top = clickY; + // check if there's enough space at the bottom, otherwise show at the top + if (clickY + contextMenuHeight > window.innerHeight) top = clickY - contextMenuHeight; + + // check if there's enough space on the right, otherwise show on the left + let left = clickX; + if (clickX + contextMenuWidth > window.innerWidth) left = clickX - contextMenuWidth; + + contextMenu.style.top = `${top}px`; + contextMenu.style.left = `${left}px`; + } + }, [clickEvent, isOpen]); + return (
{title && (

diff --git a/apps/app/components/ui/input/index.tsx b/apps/app/components/ui/input/index.tsx index d00d1386f..867712a29 100644 --- a/apps/app/components/ui/input/index.tsx +++ b/apps/app/components/ui/input/index.tsx @@ -29,9 +29,9 @@ export const Input: React.FC = ({ type={type} id={id} value={value} - {...(register && register(name, validations))} + {...(register && register(name ?? "", validations))} onChange={(e) => { - register && register(name).onChange(e); + register && register(name ?? "").onChange(e); onChange && onChange(e); }} className={`block rounded-md bg-transparent text-sm focus:outline-none placeholder-custom-text-400 ${ diff --git a/apps/app/components/ui/input/types.d.ts b/apps/app/components/ui/input/types.d.ts index 8207006df..77a48b4b9 100644 --- a/apps/app/components/ui/input/types.d.ts +++ b/apps/app/components/ui/input/types.d.ts @@ -3,7 +3,7 @@ import type { UseFormRegister, RegisterOptions } from "react-hook-form"; export interface Props extends React.ComponentPropsWithoutRef<"input"> { label?: string; - name: string; + name?: string; value?: string | number | readonly string[]; mode?: "primary" | "transparent" | "trueTransparent" | "secondary" | "disabled"; register?: UseFormRegister; diff --git a/apps/app/components/workspace/create-workspace-form.tsx b/apps/app/components/workspace/create-workspace-form.tsx index 2e1128bba..c7eb584b9 100644 --- a/apps/app/components/workspace/create-workspace-form.tsx +++ b/apps/app/components/workspace/create-workspace-form.tsx @@ -45,6 +45,7 @@ const restrictedUrls = [ "profile", "reset-password", "sign-up", + "spaces", "workspace-member-invitation", ]; diff --git a/apps/app/components/workspace/delete-workspace-modal.tsx b/apps/app/components/workspace/delete-workspace-modal.tsx index b896a87e7..7ba8c0eff 100644 --- a/apps/app/components/workspace/delete-workspace-modal.tsx +++ b/apps/app/components/workspace/delete-workspace-modal.tsx @@ -1,9 +1,11 @@ -import React, { useEffect, useState } from "react"; +import React from "react"; import { useRouter } from "next/router"; import { mutate } from "swr"; +// react-hook-form +import { Controller, useForm } from "react-hook-form"; // headless ui import { Dialog, Transition } from "@headlessui/react"; // services @@ -26,57 +28,63 @@ type Props = { user: ICurrentUserResponse | undefined; }; +const defaultValues = { + workspaceName: "", + confirmDelete: "", +}; + export const DeleteWorkspaceModal: React.FC = ({ isOpen, data, onClose, user }) => { - const [isDeleteLoading, setIsDeleteLoading] = useState(false); - - const [confirmWorkspaceName, setConfirmWorkspaceName] = useState(""); - const [confirmDeleteMyWorkspace, setConfirmDeleteMyWorkspace] = useState(false); - - const [selectedWorkspace, setSelectedWorkspace] = useState(null); - const router = useRouter(); + const { setToastAlert } = useToast(); - useEffect(() => { - if (data) setSelectedWorkspace(data); - else { - const timer = setTimeout(() => { - setSelectedWorkspace(null); - clearTimeout(timer); - }, 350); - } - }, [data]); + const { + control, + formState: { isSubmitting }, + handleSubmit, + reset, + watch, + } = useForm({ defaultValues }); - const canDelete = confirmWorkspaceName === data?.name && confirmDeleteMyWorkspace; + const canDelete = + watch("workspaceName") === data?.name && watch("confirmDelete") === "delete my workspace"; const handleClose = () => { - setIsDeleteLoading(false); - setConfirmWorkspaceName(""); - setConfirmDeleteMyWorkspace(false); + const timer = setTimeout(() => { + reset(defaultValues); + clearTimeout(timer); + }, 350); + onClose(); }; - const handleDeletion = async () => { - setIsDeleteLoading(true); + const onSubmit = async () => { if (!data || !canDelete) return; + await workspaceService .deleteWorkspace(data.slug, user) .then(() => { handleClose(); + router.push("/"); + mutate(USER_WORKSPACES, (prevData) => prevData?.filter((workspace) => workspace.id !== data.id) ); + setToastAlert({ type: "success", - message: "Workspace deleted successfully", - title: "Success", + title: "Success!", + message: "Workspace deleted successfully.", }); }) - .catch((error) => { - console.log(error); - setIsDeleteLoading(false); - }); + .catch(() => + setToastAlert({ + type: "error", + title: "Error!", + message: "Something went wrong. Please try again later.", + }) + ); }; return ( @@ -106,7 +114,7 @@ export const DeleteWorkspaceModal: React.FC = ({ isOpen, data, onClose, u leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > -
+
= ({ isOpen, data, onClose, u

Enter the workspace name{" "} - - {selectedWorkspace?.name} - {" "} - to continue: + {data?.name} to + continue:

- { - setConfirmWorkspaceName(e.target.value); - }} + ( + + )} />
@@ -154,28 +163,28 @@ export const DeleteWorkspaceModal: React.FC = ({ isOpen, data, onClose, u delete my workspace{" "} below:

- { - if (e.target.value === "delete my workspace") { - setConfirmDeleteMyWorkspace(true); - } else { - setConfirmDeleteMyWorkspace(false); - } - }} - name="typeDelete" + ( + + )} />
Cancel - - {isDeleteLoading ? "Deleting..." : "Delete Workspace"} + + {isSubmitting ? "Deleting..." : "Delete Workspace"}
-
+

diff --git a/apps/app/hooks/use-outside-click-detector.tsx b/apps/app/hooks/use-outside-click-detector.tsx index f20666f8c..5331d11c8 100644 --- a/apps/app/hooks/use-outside-click-detector.tsx +++ b/apps/app/hooks/use-outside-click-detector.tsx @@ -8,10 +8,10 @@ const useOutsideClickDetector = (ref: React.RefObject, callback: () }; useEffect(() => { - document.addEventListener("click", handleClick); + document.addEventListener("mousedown", handleClick); return () => { - document.removeEventListener("click", handleClick); + document.removeEventListener("mousedown", handleClick); }; }); }; diff --git a/apps/app/package.json b/apps/app/package.json index 89b5de611..578a95716 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -52,10 +52,10 @@ "highlight.js": "^11.8.0", "js-cookie": "^3.0.1", "lodash.debounce": "^4.0.8", - "mobx": "^6.10.0", - "mobx-react-lite": "^4.0.3", "lowlight": "^2.9.0", "lucide-react": "^0.263.1", + "mobx": "^6.10.0", + "mobx-react-lite": "^4.0.3", "next": "12.3.2", "next-pwa": "^5.6.0", "next-themes": "^0.2.1", @@ -68,6 +68,7 @@ "react-dropzone": "^14.2.3", "react-hook-form": "^7.38.0", "react-markdown": "^8.0.7", + "react-moveable": "^0.54.1", "sharp": "^0.32.1", "sonner": "^0.6.2", "swr": "^2.1.3", diff --git a/apps/app/pages/[workspaceSlug]/me/profile/activity.tsx b/apps/app/pages/[workspaceSlug]/me/profile/activity.tsx index 67f3a77ee..148d738c0 100644 --- a/apps/app/pages/[workspaceSlug]/me/profile/activity.tsx +++ b/apps/app/pages/[workspaceSlug]/me/profile/activity.tsx @@ -106,6 +106,7 @@ const ProfileActivity = () => {
div > p { transition: opacity 0.2s ease-out; } +.img-placeholder { + position: relative; + width: 35%; + + &:before { + content: ""; + box-sizing: border-box; + position: absolute; + top: 50%; + left: 45%; + width: 20px; + height: 20px; + border-radius: 50%; + border: 3px solid rgba(var(--color-text-200)); + border-top-color: rgba(var(--color-text-800)); + animation: spinning 0.6s linear infinite; + } +} + +@keyframes spinning { + to { + transform: rotate(360deg); + } +} diff --git a/apps/space/Dockerfile.space b/apps/space/Dockerfile.space new file mode 100644 index 000000000..0240ddbf7 --- /dev/null +++ b/apps/space/Dockerfile.space @@ -0,0 +1,70 @@ +FROM node:18-alpine AS builder +RUN apk add --no-cache libc6-compat +# Set working directory +WORKDIR /app +ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER + +RUN yarn global add turbo +COPY . . + +RUN turbo prune --scope=space --docker + +# Add lockfile and package.json's of isolated subworkspace +FROM node:18-alpine AS installer + +RUN apk add --no-cache libc6-compat +WORKDIR /app +ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 + +# First install the dependencies (as they change less often) +COPY .gitignore .gitignore +COPY --from=builder /app/out/json/ . +COPY --from=builder /app/out/yarn.lock ./yarn.lock +RUN yarn install --network-timeout 500000 + +# Build the project +COPY --from=builder /app/out/full/ . +COPY turbo.json turbo.json +COPY replace-env-vars.sh /usr/local/bin/ +USER root +RUN chmod +x /usr/local/bin/replace-env-vars.sh + +RUN yarn turbo run build --filter=space + +ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \ + BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL + +RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL} space + +FROM node:18-alpine AS runner +WORKDIR /app + +# Don't run production as root +RUN addgroup --system --gid 1001 plane +RUN adduser --system --uid 1001 captain +USER captain + +COPY --from=installer /app/apps/space/next.config.js . +COPY --from=installer /app/apps/space/package.json . + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=installer --chown=captain:plane /app/apps/space/.next/standalone ./ + +COPY --from=installer --chown=captain:plane /app/apps/space/.next ./apps/space/.next + +ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 +ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \ + BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL + +USER root +COPY replace-env-vars.sh /usr/local/bin/ +COPY start.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/replace-env-vars.sh +RUN chmod +x /usr/local/bin/start.sh + +USER captain + +ENV NEXT_TELEMETRY_DISABLED 1 + +EXPOSE 3000 diff --git a/apps/space/app/[workspace_slug]/[project_slug]/page.tsx b/apps/space/app/[workspace_slug]/[project_slug]/page.tsx index f6ccf5081..80dd5f038 100644 --- a/apps/space/app/[workspace_slug]/[project_slug]/page.tsx +++ b/apps/space/app/[workspace_slug]/[project_slug]/page.tsx @@ -25,7 +25,8 @@ const WorkspaceProjectPage = observer(() => { const routerSearchparams = useSearchParams(); const { workspace_slug, project_slug } = routerParams as { workspace_slug: string; project_slug: string }; - const board = routerSearchparams.get("board") as TIssueBoardKeys | ""; + const board = + routerSearchparams && routerSearchparams.get("board") != null && (routerSearchparams.get("board") as TIssueBoardKeys | ""); // updating default board view when we are in the issues page useEffect(() => { diff --git a/apps/space/lib/mobx/store-init.tsx b/apps/space/lib/mobx/store-init.tsx index 4f4b6662c..a31bc822f 100644 --- a/apps/space/lib/mobx/store-init.tsx +++ b/apps/space/lib/mobx/store-init.tsx @@ -1,10 +1,6 @@ "use client"; import { useEffect } from "react"; -// next imports -import { useSearchParams } from "next/navigation"; -// interface -import { TIssueBoardKeys } from "store/types"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; @@ -12,11 +8,6 @@ import { RootStore } from "store/root"; const MobxStoreInit = () => { const store: RootStore = useMobxStore(); - // search params - const routerSearchparams = useSearchParams(); - - const board = routerSearchparams.get("board") as TIssueBoardKeys; - useEffect(() => { // theme const _theme = localStorage && localStorage.getItem("app_theme") ? localStorage.getItem("app_theme") : "light"; diff --git a/apps/space/next.config.js b/apps/space/next.config.js index 398eb0400..0dd1894bb 100644 --- a/apps/space/next.config.js +++ b/apps/space/next.config.js @@ -1,9 +1,14 @@ /** @type {import('next').NextConfig} */ +const path = require('path') const nextConfig = { + reactStrictMode: false, + swcMinify: true, experimental: { + outputFileTracingRoot: path.join(__dirname, "../../"), appDir: true, }, + output: 'standalone' }; module.exports = nextConfig; diff --git a/apps/space/package.json b/apps/space/package.json index dd7a3058c..e37dfa54a 100644 --- a/apps/space/package.json +++ b/apps/space/package.json @@ -1,11 +1,11 @@ { - "name": "plane-deploy", + "name": "space", "version": "0.0.1", "private": true, "scripts": { - "dev": "next dev", + "dev": "next dev -p 4000", "build": "next build", - "start": "next start", + "start": "next start -p 4000", "lint": "next lint" }, "dependencies": { diff --git a/apps/space/app/404/page.tsx b/apps/space/pages/404.tsx similarity index 100% rename from apps/space/app/404/page.tsx rename to apps/space/pages/404.tsx diff --git a/apps/space/pages/_app.tsx b/apps/space/pages/_app.tsx new file mode 100644 index 000000000..8681006e1 --- /dev/null +++ b/apps/space/pages/_app.tsx @@ -0,0 +1,10 @@ +// styles +import "styles/globals.css"; +// types +import type { AppProps } from "next/app"; + +function MyApp({ Component, pageProps }: AppProps) { + return ; +} + +export default MyApp; diff --git a/apps/space/pages/_document.tsx b/apps/space/pages/_document.tsx new file mode 100644 index 000000000..8b41d05fc --- /dev/null +++ b/apps/space/pages/_document.tsx @@ -0,0 +1,17 @@ +import Document, { Html, Head, Main, NextScript } from "next/document"; + +class MyDocument extends Document { + render() { + return ( + + + +
+ + + + ); + } +} + +export default MyDocument; diff --git a/apps/space/services/api.service.ts b/apps/space/services/api.service.ts index 900d5d15f..cd7076a7a 100644 --- a/apps/space/services/api.service.ts +++ b/apps/space/services/api.service.ts @@ -3,7 +3,7 @@ import axios from "axios"; // js cookie import Cookies from "js-cookie"; -const base_url: string | null = "https://boarding.plane.so"; +const base_url: string | null = "http://localhost:8000"; abstract class APIService { protected baseURL: string; diff --git a/docker-compose-hub.yml b/docker-compose-hub.yml index c0466e09c..84ff8b833 100644 --- a/docker-compose-hub.yml +++ b/docker-compose-hub.yml @@ -38,11 +38,12 @@ services: container_name: planefrontend image: makeplane/plane-frontend:latest restart: always - command: /usr/local/bin/start.sh + command: /usr/local/bin/start.sh apps/app/server.js app env_file: - .env environment: NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} + NEXT_PUBLIC_DEPLOY_URL: ${NEXT_PUBLIC_DEPLOY_URL} NEXT_PUBLIC_GOOGLE_CLIENTID: "0" NEXT_PUBLIC_GITHUB_APP_NAME: "0" NEXT_PUBLIC_GITHUB_ID: "0" @@ -54,6 +55,20 @@ services: depends_on: - plane-api - plane-worker + + plane-deploy: + container_name: planedeploy + image: makeplane/plane-space:latest + restart: always + command: node apps/space/server.js space + env_file: + - .env + environment: + NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} + depends_on: + - plane-api + - plane-worker + - plane-web plane-api: container_name: planebackend diff --git a/docker-compose.yml b/docker-compose.yml index be8b96258..d1ffca809 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,12 +41,14 @@ services: dockerfile: ./apps/app/Dockerfile.web args: NEXT_PUBLIC_API_BASE_URL: http://localhost:8000 + NEXT_PUBLIC_DEPLOY_URL: http://localhost/spaces restart: always - command: /usr/local/bin/start.sh + command: /usr/local/bin/start.sh apps/app/server.js app env_file: - .env environment: NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} + NEXT_PUBLIC_DEPLOY_URL: ${NEXT_PUBLIC_DEPLOY_URL} NEXT_PUBLIC_GOOGLE_CLIENTID: "0" NEXT_PUBLIC_GITHUB_APP_NAME: "0" NEXT_PUBLIC_GITHUB_ID: "0" @@ -58,6 +60,23 @@ services: depends_on: - plane-api - plane-worker + plane-deploy: + container_name: planedeploy + build: + context: . + dockerfile: ./apps/space/Dockerfile.space space + args: + NEXT_PUBLIC_API_BASE_URL: http://localhost:8000 + restart: always + command: /usr/local/bin/start.sh apps/space/server.js space + env_file: + - .env + environment: + NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} + depends_on: + - plane-api + - plane-worker + - plane-web plane-api: container_name: planebackend diff --git a/nginx/nginx-single-docker-image.conf b/nginx/nginx-single-docker-image.conf index 8d927a1b2..b9f50d664 100644 --- a/nginx/nginx-single-docker-image.conf +++ b/nginx/nginx-single-docker-image.conf @@ -18,6 +18,11 @@ server { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } + location /space/ { + proxy_pass http://localhost:4000/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; diff --git a/nginx/nginx.conf.template b/nginx/nginx.conf.template index 206c94b51..796c9e10d 100644 --- a/nginx/nginx.conf.template +++ b/nginx/nginx.conf.template @@ -15,6 +15,10 @@ server { proxy_pass http://planefrontend:3000/; } + location /spaces/ { + proxy_pass http://planedeploy:3000/; + } + location /api/ { proxy_pass http://planebackend:8000/api/; } diff --git a/replace-env-vars.sh b/replace-env-vars.sh index 56c6d2025..afdc1492e 100644 --- a/replace-env-vars.sh +++ b/replace-env-vars.sh @@ -1,6 +1,7 @@ #!/bin/sh FROM=$1 TO=$2 +DIRECTORY=$3 if [ "${FROM}" = "${TO}" ]; then echo "Nothing to replace, the value is already set to ${TO}." @@ -11,4 +12,4 @@ fi # Only perform action if $FROM and $TO are different. echo "Replacing all statically built instances of $FROM with this string $TO ." -grep -R -la "${FROM}" apps/app/.next | xargs -I{} sed -i "s|$FROM|$TO|g" "{}" +grep -R -la "${FROM}" apps/$DIRECTORY/.next | xargs -I{} sed -i "s|$FROM|$TO|g" "{}" diff --git a/setup.sh b/setup.sh index a5a8e9b6a..235e1a977 100755 --- a/setup.sh +++ b/setup.sh @@ -1,5 +1,5 @@ #!/bin/bash -# cp ./.env.example ./.env +cp ./.env.example ./.env # Export for tr error in mac export LC_ALL=C diff --git a/start.sh b/start.sh index 173e333a4..dcb97db6d 100644 --- a/start.sh +++ b/start.sh @@ -3,7 +3,7 @@ set -x # Replace the statically built BUILT_NEXT_PUBLIC_API_BASE_URL with run-time NEXT_PUBLIC_API_BASE_URL # NOTE: if these values are the same, this will be skipped. -/usr/local/bin/replace-env-vars.sh "$BUILT_NEXT_PUBLIC_API_BASE_URL" "$NEXT_PUBLIC_API_BASE_URL" +/usr/local/bin/replace-env-vars.sh "$BUILT_NEXT_PUBLIC_API_BASE_URL" "$NEXT_PUBLIC_API_BASE_URL" $2 echo "Starting Plane Frontend.." -node apps/app/server.js \ No newline at end of file +node $1 diff --git a/yarn.lock b/yarn.lock index b4cf55f2f..8bc1fec30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1006,6 +1006,40 @@ react-popper "^2.3.0" tslib "~2.5.0" +"@cfcs/core@^0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@cfcs/core/-/core-0.0.6.tgz#9f8499dcd2ad29fd96d8fa72055411cd4a249121" + integrity sha512-FxfJMwoLB8MEMConeXUCqtMGqxdtePQxRBOiGip9ULcYYam3WfCgoY6xdnMaSkYvRvmosp5iuG+TiPofm65+Pw== + dependencies: + "@egjs/component" "^3.0.2" + +"@daybrush/utils@^1.1.1", "@daybrush/utils@^1.13.0", "@daybrush/utils@^1.4.0", "@daybrush/utils@^1.6.0", "@daybrush/utils@^1.7.1": + version "1.13.0" + resolved "https://registry.yarnpkg.com/@daybrush/utils/-/utils-1.13.0.tgz#ea70a60864130da476406fdd1d465e3068aea0ff" + integrity sha512-ALK12C6SQNNHw1enXK+UO8bdyQ+jaWNQ1Af7Z3FNxeAwjYhQT7do+TRE4RASAJ3ObaS2+TJ7TXR3oz2Gzbw0PQ== + +"@egjs/agent@^2.2.1": + version "2.4.3" + resolved "https://registry.yarnpkg.com/@egjs/agent/-/agent-2.4.3.tgz#6d44e2fb1ff7bab242c07f82732fe60305ac6f06" + integrity sha512-XvksSENe8wPeFlEVouvrOhKdx8HMniJ3by7sro2uPF3M6QqWwjzVcmvwoPtdjiX8O1lfRoLhQMp1a7NGlVTdIA== + +"@egjs/children-differ@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@egjs/children-differ/-/children-differ-1.0.1.tgz#5465fa80671d5ca3564ebe912f48b05b3e8a14fd" + integrity sha512-DRvyqMf+CPCOzAopQKHtW+X8iN6Hy6SFol+/7zCUiE5y4P/OB8JP8FtU4NxtZwtafvSL4faD5KoQYPj3JHzPFQ== + dependencies: + "@egjs/list-differ" "^1.0.0" + +"@egjs/component@^3.0.2": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@egjs/component/-/component-3.0.4.tgz#ad7b53794b2a612806179a188ad828acb9525f61" + integrity sha512-sXA7bGbIeLF2OAw/vpka66c6QBBUPcA4UUhR4WGJfnp2XWdiI8QrnJGJMr/UxpE/xnevX9tN3jvNPlW8WkHl3g== + +"@egjs/list-differ@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@egjs/list-differ/-/list-differ-1.0.1.tgz#5772b0f8b87973bb67827f6c7d7df8d7f64a22eb" + integrity sha512-OTFTDQcWS+1ZREOdCWuk5hCBgYO4OsD30lXcOCyVOAjXMhgL5rBRDnt/otb6Nz8CzU0L/igdcaQBDLWc4t9gvg== + "@emotion/babel-plugin@^11.11.0": version "11.11.0" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c" @@ -1990,6 +2024,28 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.3.3.tgz#16ab6c727d8c2020a5b6e4a176a243ecd88d8d69" integrity sha512-0xd7qez0AQ+MbHatZTlI1gu5vkG8r7MYRUJAHPAHJBmGLs16zpkrpAVLvjQKQOqaXPDUBwOiJzNc00znHSCVBw== +"@scena/dragscroll@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@scena/dragscroll/-/dragscroll-1.4.0.tgz#220b2430c16119cd3e70044ee533a5b9a43cffd7" + integrity sha512-3O8daaZD9VXA9CP3dra6xcgt/qrm0mg0xJCwiX6druCteQ9FFsXffkF8PrqxY4Z4VJ58fFKEa0RlKqbsi/XnRA== + dependencies: + "@daybrush/utils" "^1.6.0" + "@scena/event-emitter" "^1.0.2" + +"@scena/event-emitter@^1.0.2", "@scena/event-emitter@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@scena/event-emitter/-/event-emitter-1.0.5.tgz#047e3acef93cf238d7ce3a8cc5a12ec6bd9c3bb1" + integrity sha512-AzY4OTb0+7ynefmWFQ6hxDdk0CySAq/D4efljfhtRHCOP7MBF9zUfhKG3TJiroVjASqVgkRJFdenS8ArZo6Olg== + dependencies: + "@daybrush/utils" "^1.1.1" + +"@scena/matrix@^1.0.0", "@scena/matrix@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@scena/matrix/-/matrix-1.1.1.tgz#5297f71825c72e2c2c8f802f924f482ed200c43c" + integrity sha512-JVKBhN0tm2Srl+Yt+Ywqu0oLgLcdemDQlD1OxmN9jaCTwaFPZ7tY8n6dhVgMEaR9qcR7r+kAlMXnSfNyYdE+Vg== + dependencies: + "@daybrush/utils" "^1.4.0" + "@sentry-internal/tracing@7.63.0": version "7.63.0" resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.63.0.tgz#58903b2205456034611cc5bc1b5b2479275f89c7" @@ -3462,6 +3518,21 @@ css-box-model@^1.2.0: dependencies: tiny-invariant "^1.0.6" +css-styled@^1.0.8, css-styled@~1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/css-styled/-/css-styled-1.0.8.tgz#c9c05dc4abdef5571033090bfb8cfc5e19429974" + integrity sha512-tCpP7kLRI8dI95rCh3Syl7I+v7PP+2JYOzWkl0bUEoSbJM+u8ITbutjlQVf0NC2/g4ULROJPi16sfwDIO8/84g== + dependencies: + "@daybrush/utils" "^1.13.0" + +css-to-mat@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/css-to-mat/-/css-to-mat-1.1.1.tgz#0dd10dcf9ec17df15708c8ff07a74fbd0b9a3fe5" + integrity sha512-kvpxFYZb27jRd2vium35G7q5XZ2WJ9rWjDUMNT36M3Hc41qCrLXFM5iEKMGXcrPsKfXEN+8l/riB4QzwwwiEyQ== + dependencies: + "@daybrush/utils" "^1.13.0" + "@scena/matrix" "^1.0.0" + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -4484,6 +4555,11 @@ fraction.js@^4.2.0: resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== +framework-utils@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/framework-utils/-/framework-utils-1.1.0.tgz#a3b528bce838dfd623148847dc92371b09d0da2d" + integrity sha512-KAfqli5PwpFJ8o3psRNs8svpMGyCSAe8nmGcjQ0zZBWN2H6dZDnq+ABp3N3hdUmFeMrLtjOCTXD4yplUJIWceg== + fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" @@ -4539,6 +4615,14 @@ gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== +gesto@^1.19.0, gesto@^1.19.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/gesto/-/gesto-1.19.1.tgz#b2a29730663eecf77b248982bbff929e79d4a461" + integrity sha512-ofWVEdqmnpFm3AFf7aoclhoayseb3OkwSiXbXusKYu/99iN5HgeWP+SWqdghQ5TFlOgP5Zlz+6SY8mP2V0kFaQ== + dependencies: + "@daybrush/utils" "^1.13.0" + "@scena/event-emitter" "^1.0.2" + get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" @@ -5244,6 +5328,21 @@ jsonpointer@^5.0.0: object.assign "^4.1.4" object.values "^1.1.6" +keycode@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.1.tgz#09c23b2be0611d26117ea2501c2c391a01f39eff" + integrity sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg== + +keycon@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/keycon/-/keycon-1.4.0.tgz#bf2a633f3c3b659ea564045938cff33e584cebd5" + integrity sha512-p1NAIxiRMH3jYfTeXRs2uWbVJ1WpEjpi8ktzUyBJsX7/wn2qu2VRXktneBLNtKNxJmlUYxRi9gOJt1DuthXR7A== + dependencies: + "@cfcs/core" "^0.0.6" + "@daybrush/utils" "^1.7.1" + "@scena/event-emitter" "^1.0.2" + keycode "^2.2.0" + kleur@^4.0.3: version "4.1.5" resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" @@ -6081,6 +6180,13 @@ orderedmap@^2.0.0: resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.1.1.tgz#61481269c44031c449915497bf5a4ad273c512d2" integrity sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g== +overlap-area@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/overlap-area/-/overlap-area-1.1.0.tgz#1fcaa21bdb9cb1ace973d9aa299ae6b56557a4c2" + integrity sha512-3dlJgJCaVeXH0/eZjYVJvQiLVVrPO4U1ZGqlATtx6QGO3b5eNM6+JgUKa7oStBTdYuGTk7gVoABCW6Tp+dhRdw== + dependencies: + "@daybrush/utils" "^1.7.1" + p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -6452,20 +6558,13 @@ prosemirror-menu@^1.2.1: prosemirror-history "^1.0.0" prosemirror-state "^1.0.0" -prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1, prosemirror-model@^1.8.1: +prosemirror-model@1.18.1, prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1, prosemirror-model@^1.19.0, prosemirror-model@^1.8.1: version "1.18.1" resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.18.1.tgz#1d5d6b6de7b983ee67a479dc607165fdef3935bd" integrity sha512-IxSVBKAEMjD7s3n8cgtwMlxAXZrC7Mlag7zYsAKDndAqnDScvSmp/UdnRTV/B33lTCVU3CCm7dyAn/rVVD0mcw== dependencies: orderedmap "^2.0.0" -prosemirror-model@^1.19.0: - version "1.19.3" - resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.19.3.tgz#f0d55285487fefd962d0ac695f716f4ec6705006" - integrity sha512-tgSnwN7BS7/UM0sSARcW+IQryx2vODKX4MI7xpqY2X+iaepJdKBPc7I4aACIsDV/LTaTjt12Z56MhDr9LsyuZQ== - dependencies: - orderedmap "^2.0.0" - prosemirror-schema-basic@^1.2.0: version "1.2.2" resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.2.tgz#6695f5175e4628aab179bf62e5568628b9cfe6c7" @@ -6603,6 +6702,14 @@ react-color@^2.19.3: reactcss "^1.2.0" tinycolor2 "^1.4.1" +react-css-styled@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/react-css-styled/-/react-css-styled-1.1.9.tgz#a7cc948e49f72b2f7fb1393bd85416a8293afab3" + integrity sha512-M7fJZ3IWFaIHcZEkoFOnkjdiUFmwd8d+gTh2bpqMOcnxy/0Gsykw4dsL4QBiKsxcGow6tETUa4NAUcmJF+/nfw== + dependencies: + css-styled "~1.0.8" + framework-utils "^1.1.0" + react-datepicker@^4.8.0: version "4.16.0" resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-4.16.0.tgz#b9dd389bb5611a1acc514bba1dd944be21dd877f" @@ -6683,6 +6790,25 @@ react-markdown@^8.0.7: unist-util-visit "^4.0.0" vfile "^5.0.0" +react-moveable@^0.54.1: + version "0.54.1" + resolved "https://registry.yarnpkg.com/react-moveable/-/react-moveable-0.54.1.tgz#3c69748c444184700e6999501b0da953c934205e" + integrity sha512-Kj2ifw9nk3LZvu7ezhst8Z5WBPRr+yVv9oROwrBirFlHmwGHHZXUGk5Gaezu+JGqqNRsQJncVMW5Uf68KSSOvg== + dependencies: + "@daybrush/utils" "^1.13.0" + "@egjs/agent" "^2.2.1" + "@egjs/children-differ" "^1.0.1" + "@egjs/list-differ" "^1.0.0" + "@scena/dragscroll" "^1.4.0" + "@scena/event-emitter" "^1.0.5" + "@scena/matrix" "^1.1.1" + css-to-mat "^1.1.1" + framework-utils "^1.1.0" + gesto "^1.19.0" + overlap-area "^1.1.0" + react-css-styled "^1.1.9" + react-selecto "^1.25.0" + react-onclickoutside@^6.12.2: version "6.13.0" resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz#e165ea4e5157f3da94f4376a3ab3e22a565f4ffc" @@ -6740,6 +6866,13 @@ react-remove-scroll@2.5.4: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" +react-selecto@^1.25.0: + version "1.26.0" + resolved "https://registry.yarnpkg.com/react-selecto/-/react-selecto-1.26.0.tgz#9157ff0a732fc426602b30c08ec21b6ca0a9c472" + integrity sha512-aBTZEYA68uE+o8TytNjTb2GpIn4oKEv0U4LIow3cspJQlF/PdAnBwkq9UuiKVuFluu5kfLQ7Keu3S2Tihlmw0g== + dependencies: + selecto "~1.26.0" + react-style-singleton@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" @@ -7023,6 +7156,22 @@ schema-utils@^3.1.1: ajv "^6.12.5" ajv-keywords "^3.5.2" +selecto@~1.26.0: + version "1.26.0" + resolved "https://registry.yarnpkg.com/selecto/-/selecto-1.26.0.tgz#f3f04fb6409112b198243458f6c9963946d5ba2f" + integrity sha512-cEFKdv5rmkF6pf2OScQJllaNp4UJy/FvviB40ZaMSHrQCxC72X/Q6uhzW1tlb2RE+0danvUNJTs64cI9VXtUyg== + dependencies: + "@daybrush/utils" "^1.13.0" + "@egjs/children-differ" "^1.0.1" + "@scena/dragscroll" "^1.4.0" + "@scena/event-emitter" "^1.0.5" + css-styled "^1.0.8" + css-to-mat "^1.1.1" + framework-utils "^1.1.0" + gesto "^1.19.1" + keycon "^1.2.0" + overlap-area "^1.1.0" + semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"