Merge pull request #1923 from makeplane/develop

promote: develop to stage-release
This commit is contained in:
Nikhil 2023-08-21 18:18:14 +05:30 committed by GitHub
commit 3beab9de6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 877 additions and 404 deletions

View File

@ -5,9 +5,11 @@ WORKDIR /app
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
RUN yarn global add turbo RUN yarn global add turbo
RUN apk add tree
COPY . . 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 # Add lockfile and package.json's of isolated subworkspace
FROM node:18-alpine AS installer 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 COPY --from=builder /app/out/yarn.lock ./yarn.lock
RUN yarn install RUN yarn install
# Build the project # # Build the project
COPY --from=builder /app/out/full/ . COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json COPY turbo.json turbo.json
COPY replace-env-vars.sh /usr/local/bin/ COPY replace-env-vars.sh /usr/local/bin/
USER root USER root
RUN chmod +x /usr/local/bin/replace-env-vars.sh 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 \ ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_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/next.config.js .
COPY --from=installer /app/apps/app/package.json . 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/standalone ./
COPY --from=installer --chown=captain:plane /app/apps/app/.next/static ./apps/app/.next/static 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 ENV NEXT_TELEMETRY_DISABLED 1
# RUN rm /etc/nginx/conf.d/default.conf # RUN rm /etc/nginx/conf.d/default.conf

View File

@ -33,8 +33,8 @@ RUN yarn turbo run build --filter=app
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \ ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL BUILT_NEXT_PUBLIC_API_BASE_URL=$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} 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 FROM node:18-alpine AS runner
WORKDIR /app WORKDIR /app

View File

@ -140,14 +140,14 @@ export const GptAssistantModal: React.FC<Props> = ({
return ( return (
<div <div
className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow ${ className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow ${isOpen ? "block" : "hidden"
isOpen ? "block" : "hidden" }`}
}`}
> >
{((content && content !== "") || (htmlContent && htmlContent !== "<p></p>")) && ( {((content && content !== "") || (htmlContent && htmlContent !== "<p></p>")) && (
<div className="text-sm"> <div className="text-sm">
Content: Content:
<TiptapEditor <TiptapEditor
workspaceSlug={workspaceSlug as string}
value={htmlContent ?? `<p>${content}</p>`} value={htmlContent ?? `<p>${content}</p>`}
customClassName="-m-3" customClassName="-m-3"
noBorder noBorder
@ -161,6 +161,7 @@ export const GptAssistantModal: React.FC<Props> = ({
<div className="page-block-section text-sm"> <div className="page-block-section text-sm">
Response: Response:
<Tiptap <Tiptap
workspaceSlug={workspaceSlug as string}
value={`<p>${response}</p>`} value={`<p>${response}</p>`}
customClassName="-mx-3 -my-3" customClassName="-mx-3 -my-3"
noBorder noBorder
@ -179,11 +180,10 @@ export const GptAssistantModal: React.FC<Props> = ({
type="text" type="text"
name="task" name="task"
register={register} register={register}
placeholder={`${ placeholder={`${content && content !== ""
content && content !== ""
? "Tell AI what action to perform on this content..." ? "Tell AI what action to perform on this content..."
: "Ask AI anything..." : "Ask AI anything..."
}`} }`}
autoComplete="off" autoComplete="off"
/> />
<div className={`flex gap-2 ${response === "" ? "justify-end" : "justify-between"}`}> <div className={`flex gap-2 ${response === "" ? "justify-end" : "justify-between"}`}>
@ -219,8 +219,8 @@ export const GptAssistantModal: React.FC<Props> = ({
{isSubmitting {isSubmitting
? "Generating response..." ? "Generating response..."
: response === "" : response === ""
? "Generate response" ? "Generate response"
: "Generate again"} : "Generate again"}
</PrimaryButton> </PrimaryButton>
</div> </div>
</div> </div>

View File

@ -86,7 +86,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
}) => { }) => {
// context menu // context menu
const [contextMenu, setContextMenu] = useState(false); const [contextMenu, setContextMenu] = useState(false);
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }); const [contextMenuPosition, setContextMenuPosition] = useState<React.MouseEvent | null>(null);
const [isMenuActive, setIsMenuActive] = useState(false); const [isMenuActive, setIsMenuActive] = useState(false);
const [isDropdownActive, setIsDropdownActive] = useState(false); const [isDropdownActive, setIsDropdownActive] = useState(false);
@ -201,7 +202,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
return ( return (
<> <>
<ContextMenu <ContextMenu
position={contextMenuPosition} clickEvent={contextMenuPosition}
title="Quick actions" title="Quick actions"
isOpen={contextMenu} isOpen={contextMenu}
setIsOpen={setContextMenu} setIsOpen={setContextMenu}
@ -243,7 +244,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
onContextMenu={(e) => { onContextMenu={(e) => {
e.preventDefault(); e.preventDefault();
setContextMenu(true); setContextMenu(true);
setContextMenuPosition({ x: e.pageX, y: e.pageY }); setContextMenuPosition(e);
}} }}
> >
<div className="flex flex-col justify-between gap-1.5 group/card relative select-none px-3.5 py-3 h-[118px]"> <div className="flex flex-col justify-between gap-1.5 group/card relative select-none px-3.5 py-3 h-[118px]">

View File

@ -33,7 +33,7 @@ import {
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { LayerDiagonalIcon } from "components/icons"; import { LayerDiagonalIcon } from "components/icons";
// helpers // helpers
import { copyTextToClipboard, truncateText } from "helpers/string.helper"; import { copyTextToClipboard } from "helpers/string.helper";
import { handleIssuesMutation } from "constants/issue"; import { handleIssuesMutation } from "constants/issue";
// types // types
import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types"; import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types";
@ -71,7 +71,7 @@ export const SingleListIssue: React.FC<Props> = ({
}) => { }) => {
// context menu // context menu
const [contextMenu, setContextMenu] = useState(false); const [contextMenu, setContextMenu] = useState(false);
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }); const [contextMenuPosition, setContextMenuPosition] = useState<React.MouseEvent | null>(null);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
@ -167,7 +167,7 @@ export const SingleListIssue: React.FC<Props> = ({
return ( return (
<> <>
<ContextMenu <ContextMenu
position={contextMenuPosition} clickEvent={contextMenuPosition}
title="Quick actions" title="Quick actions"
isOpen={contextMenu} isOpen={contextMenu}
setIsOpen={setContextMenu} setIsOpen={setContextMenu}
@ -199,7 +199,7 @@ export const SingleListIssue: React.FC<Props> = ({
onContextMenu={(e) => { onContextMenu={(e) => {
e.preventDefault(); e.preventDefault();
setContextMenu(true); setContextMenu(true);
setContextMenuPosition({ x: e.pageX, y: e.pageY }); setContextMenuPosition(e);
}} }}
> >
<div className="flex-grow cursor-pointer min-w-[200px] whitespace-nowrap overflow-hidden overflow-ellipsis"> <div className="flex-grow cursor-pointer min-w-[200px] whitespace-nowrap overflow-hidden overflow-ellipsis">

View File

@ -293,6 +293,7 @@ export const InboxMainContent: React.FC = () => {
</div> </div>
<div> <div>
<IssueDescriptionForm <IssueDescriptionForm
workspaceSlug={workspaceSlug as string}
issue={{ issue={{
name: issueDetails.name, name: issueDetails.name,
description: issueDetails.description, description: issueDetails.description,

View File

@ -175,6 +175,7 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
return ( return (
<div key={activityItem.id} className="mt-4"> <div key={activityItem.id} className="mt-4">
<CommentCard <CommentCard
workspaceSlug={workspaceSlug as string}
comment={activityItem as IIssueComment} comment={activityItem as IIssueComment}
onSubmit={handleCommentUpdate} onSubmit={handleCommentUpdate}
handleCommentDeletion={handleCommentDelete} handleCommentDeletion={handleCommentDelete}

View File

@ -93,11 +93,12 @@ export const AddComment: React.FC<Props> = ({ issueId, user, disabled = false })
control={control} control={control}
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<TiptapEditor <TiptapEditor
workspaceSlug={workspaceSlug as string}
ref={editorRef} ref={editorRef}
value={ value={
!value || !value ||
value === "" || value === "" ||
(typeof value === "object" && Object.keys(value).length === 0) (typeof value === "object" && Object.keys(value).length === 0)
? watch("comment_html") ? watch("comment_html")
: value : value
} }

View File

@ -22,12 +22,13 @@ const TiptapEditor = React.forwardRef<ITiptapRichTextEditor, ITiptapRichTextEdit
TiptapEditor.displayName = "TiptapEditor"; TiptapEditor.displayName = "TiptapEditor";
type Props = { type Props = {
workspaceSlug: string;
comment: IIssueComment; comment: IIssueComment;
onSubmit: (comment: IIssueComment) => void; onSubmit: (comment: IIssueComment) => void;
handleCommentDeletion: (comment: string) => void; handleCommentDeletion: (comment: string) => void;
}; };
export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentDeletion }) => { export const CommentCard: React.FC<Props> = ({ comment, workspaceSlug, onSubmit, handleCommentDeletion }) => {
const { user } = useUser(); const { user } = useUser();
const editorRef = React.useRef<any>(null); const editorRef = React.useRef<any>(null);
@ -109,6 +110,7 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
> >
<div> <div>
<TiptapEditor <TiptapEditor
workspaceSlug={workspaceSlug as string}
ref={editorRef} ref={editorRef}
value={watch("comment_html")} value={watch("comment_html")}
debouncedUpdatesEnabled={false} debouncedUpdatesEnabled={false}
@ -138,6 +140,7 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
</form> </form>
<div className={`${isEditing ? "hidden" : ""}`}> <div className={`${isEditing ? "hidden" : ""}`}>
<TiptapEditor <TiptapEditor
workspaceSlug={workspaceSlug as string}
ref={showEditorRef} ref={showEditorRef}
value={comment.comment_html} value={comment.comment_html}
editable={false} editable={false}

View File

@ -24,6 +24,7 @@ export interface IssueDetailsProps {
description: string; description: string;
description_html: string; description_html: string;
}; };
workspaceSlug: string;
handleFormSubmit: (value: IssueDescriptionFormValues) => Promise<void>; handleFormSubmit: (value: IssueDescriptionFormValues) => Promise<void>;
isAllowed: boolean; isAllowed: boolean;
} }
@ -31,6 +32,7 @@ export interface IssueDetailsProps {
export const IssueDescriptionForm: FC<IssueDetailsProps> = ({ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
issue, issue,
handleFormSubmit, handleFormSubmit,
workspaceSlug,
isAllowed, isAllowed,
}) => { }) => {
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
@ -69,11 +71,15 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
useEffect(() => { useEffect(() => {
if (isSubmitting === "submitted") { if (isSubmitting === "submitted") {
setShowAlert(false);
setTimeout(async () => { setTimeout(async () => {
setIsSubmitting("saved"); setIsSubmitting("saved");
}, 2000); }, 2000);
} else if (isSubmitting === "submitting") {
setShowAlert(true);
} }
}, [isSubmitting]); }, [isSubmitting, setShowAlert]);
// reset form values // reset form values
useEffect(() => { useEffect(() => {
@ -112,9 +118,8 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
{characterLimit && ( {characterLimit && (
<div className="pointer-events-none absolute bottom-1 right-1 z-[2] rounded bg-custom-background-100 text-custom-text-200 p-0.5 text-xs"> <div className="pointer-events-none absolute bottom-1 right-1 z-[2] rounded bg-custom-background-100 text-custom-text-200 p-0.5 text-xs">
<span <span
className={`${ className={`${watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : ""
watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : "" }`}
}`}
> >
{watch("name").length} {watch("name").length}
</span> </span>
@ -134,16 +139,19 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
<Tiptap <Tiptap
value={ value={
!value || !value ||
value === "" || value === "" ||
(typeof value === "object" && Object.keys(value).length === 0) (typeof value === "object" && Object.keys(value).length === 0)
? watch("description_html") ? watch("description_html")
: value : value
} }
workspaceSlug={workspaceSlug}
debouncedUpdatesEnabled={true} debouncedUpdatesEnabled={true}
setShouldShowAlert={setShowAlert}
setIsSubmitting={setIsSubmitting} setIsSubmitting={setIsSubmitting}
customClassName="min-h-[150px] shadow-sm" customClassName="min-h-[150px] shadow-sm"
editorContentCustomClassNames="pb-9" editorContentCustomClassNames="pb-9"
onChange={(description: Object, description_html: string) => { onChange={(description: Object, description_html: string) => {
setShowAlert(true);
setIsSubmitting("submitting"); setIsSubmitting("submitting");
onChange(description_html); onChange(description_html);
setValue("description", description); setValue("description", description);
@ -156,9 +164,8 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
}} }}
/> />
<div <div
className={`absolute right-5 bottom-5 text-xs text-custom-text-200 border border-custom-border-400 rounded-xl w-[6.5rem] py-1 z-10 flex items-center justify-center ${ className={`absolute right-5 bottom-5 text-xs text-custom-text-200 border border-custom-border-400 rounded-xl w-[6.5rem] py-1 z-10 flex items-center justify-center ${isSubmitting === "saved" ? "fadeOut" : "fadeIn"
isSubmitting === "saved" ? "fadeOut" : "fadeIn" }`}
}`}
> >
{isSubmitting === "submitting" ? "Saving..." : "Saved"} {isSubmitting === "submitting" ? "Saving..." : "Saved"}
</div> </div>

View File

@ -370,6 +370,7 @@ export const IssueForm: FC<IssueFormProps> = ({
return ( return (
<TiptapEditor <TiptapEditor
workspaceSlug={workspaceSlug as string}
ref={editorRef} ref={editorRef}
debouncedUpdatesEnabled={false} debouncedUpdatesEnabled={false}
value={ value={

View File

@ -124,6 +124,7 @@ export const IssueMainContent: React.FC<Props> = ({
</div> </div>
) : null} ) : null}
<IssueDescriptionForm <IssueDescriptionForm
workspaceSlug={workspaceSlug as string}
issue={issueDetails} issue={issueDetails}
handleFormSubmit={submitChanges} handleFormSubmit={submitChanges}
isAllowed={memberRole.isMember || memberRole.isOwner || !uneditable} isAllowed={memberRole.isMember || memberRole.isOwner || !uneditable}

View File

@ -17,7 +17,7 @@ type Props = {
export const IssueDateSelect: React.FC<Props> = ({ label, maxDate, minDate, onChange, value }) => ( export const IssueDateSelect: React.FC<Props> = ({ label, maxDate, minDate, onChange, value }) => (
<Popover className="relative flex items-center justify-center rounded-lg"> <Popover className="relative flex items-center justify-center rounded-lg">
{({ open }) => ( {({ close }) => (
<> <>
<Popover.Button className="flex cursor-pointer items-center rounded-md border border-custom-border-200 text-xs shadow-sm duration-200"> <Popover.Button className="flex cursor-pointer items-center rounded-md border border-custom-border-200 text-xs shadow-sm duration-200">
<span className="flex items-center justify-center gap-2 px-2 py-1 text-xs text-custom-text-200 hover:bg-custom-background-80"> <span className="flex items-center justify-center gap-2 px-2 py-1 text-xs text-custom-text-200 hover:bg-custom-background-80">
@ -52,6 +52,8 @@ export const IssueDateSelect: React.FC<Props> = ({ label, maxDate, minDate, onCh
onChange={(val) => { onChange={(val) => {
if (!val) onChange(""); if (!val) onChange("");
else onChange(renderDateFormat(val)); else onChange(renderDateFormat(val));
close();
}} }}
dateFormat="dd-MM-yyyy" dateFormat="dd-MM-yyyy"
minDate={minDate} minDate={minDate}

View File

@ -1,15 +1,11 @@
import { useEffect, useState } from "react"; import { useEffect } from "react";
// react-hook-form // react-hook-form
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
// hooks
import useToast from "hooks/use-toast";
// components // components
import { ModuleLeadSelect, ModuleMembersSelect, ModuleStatusSelect } from "components/modules"; import { ModuleLeadSelect, ModuleMembersSelect, ModuleStatusSelect } from "components/modules";
// ui // ui
import { DateSelect, Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; import { DateSelect, Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
// helper
import { isDateRangeValid } from "helpers/date-time.helper";
// types // types
import { IModule } from "types"; import { IModule } from "types";
@ -29,8 +25,6 @@ const defaultValues: Partial<IModule> = {
}; };
export const ModuleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, status, data }) => { export const ModuleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, status, data }) => {
const [isDateValid, setIsDateValid] = useState(true);
const { setToastAlert } = useToast();
const { const {
register, register,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
@ -57,6 +51,15 @@ export const ModuleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, sta
}); });
}, [data, reset]); }, [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 ( return (
<form onSubmit={handleSubmit(handleCreateUpdateModule)}> <form onSubmit={handleSubmit(handleCreateUpdateModule)}>
<div className="space-y-5"> <div className="space-y-5">
@ -103,20 +106,8 @@ export const ModuleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, sta
value={value} value={value}
onChange={(val) => { onChange={(val) => {
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<Props> = ({ handleFormSubmit, handleClose, sta
value={value} value={value}
onChange={(val) => { onChange={(val) => {
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<Props> = ({ handleFormSubmit, handleClose, sta
</div> </div>
<div className="-mx-5 mt-5 flex justify-end gap-2 border-t border-custom-border-200 px-5 pt-5"> <div className="-mx-5 mt-5 flex justify-end gap-2 border-t border-custom-border-200 px-5 pt-5">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton> <SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting || isDateValid ? false : true}> <PrimaryButton type="submit" loading={isSubmitting}>
{status {status
? isSubmitting ? isSubmitting
? "Updating Module..." ? "Updating Module..."

View File

@ -231,9 +231,9 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
description: description:
!data.description || data.description === "" !data.description || data.description === ""
? { ? {
type: "doc", type: "doc",
content: [{ type: "paragraph" }], content: [{ type: "paragraph" }],
} }
: data.description, : data.description,
description_html: data.description_html ?? "<p></p>", description_html: data.description_html ?? "<p></p>",
}); });
@ -292,6 +292,7 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
if (!data) if (!data)
return ( return (
<TiptapEditor <TiptapEditor
workspaceSlug={workspaceSlug as string}
ref={editorRef} ref={editorRef}
value={"<p></p>"} value={"<p></p>"}
debouncedUpdatesEnabled={false} debouncedUpdatesEnabled={false}
@ -311,12 +312,11 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
return ( return (
<TiptapEditor <TiptapEditor
workspaceSlug={workspaceSlug as string}
ref={editorRef} ref={editorRef}
value={ value={
value && value !== "" && Object.keys(value).length > 0 value && value !== "" && Object.keys(value).length > 0
? value ? value
: watch("description_html") && watch("description_html") !== ""
? watch("description_html")
: { type: "doc", content: [{ type: "paragraph" }] } : { type: "doc", content: [{ type: "paragraph" }] }
} }
debouncedUpdatesEnabled={false} debouncedUpdatesEnabled={false}
@ -334,9 +334,8 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
<div className="m-2 mt-6 flex"> <div className="m-2 mt-6 flex">
<button <button
type="button" type="button"
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-80 ${ className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-80 ${iAmFeelingLucky ? "cursor-wait bg-custom-background-90" : ""
iAmFeelingLucky ? "cursor-wait bg-custom-background-90" : "" }`}
}`}
onClick={handleAutoGenerateDescription} onClick={handleAutoGenerateDescription}
disabled={iAmFeelingLucky} disabled={iAmFeelingLucky}
> >
@ -368,8 +367,8 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
? "Updating..." ? "Updating..."
: "Update block" : "Update block"
: isSubmitting : isSubmitting
? "Adding..." ? "Adding..."
: "Add block"} : "Add block"}
</PrimaryButton> </PrimaryButton>
</div> </div>
</form> </form>

View File

@ -456,6 +456,7 @@ export const SinglePageBlock: React.FC<Props> = ({
{showBlockDetails {showBlockDetails
? block.description_html.length > 7 && ( ? block.description_html.length > 7 && (
<TiptapEditor <TiptapEditor
workspaceSlug={workspaceSlug as string}
value={block.description_html} value={block.description_html}
customClassName="text-sm min-h-[150px]" customClassName="text-sm min-h-[150px]"
noBorder noBorder

View File

@ -12,10 +12,10 @@ type Props = {
}; };
export const ProfilePriorityDistribution: React.FC<Props> = ({ userProfile }) => ( export const ProfilePriorityDistribution: React.FC<Props> = ({ userProfile }) => (
<div className="space-y-2"> <div className="flex flex-col space-y-2">
<h3 className="text-lg font-medium">Issues by Priority</h3> <h3 className="text-lg font-medium">Issues by Priority</h3>
{userProfile ? ( {userProfile ? (
<div className="border border-custom-border-100 rounded"> <div className="flex-grow border border-custom-border-100 rounded">
{userProfile.priority_distribution.length > 0 ? ( {userProfile.priority_distribution.length > 0 ? (
<BarGraph <BarGraph
data={userProfile.priority_distribution.map((priority) => ({ data={userProfile.priority_distribution.map((priority) => ({
@ -63,7 +63,7 @@ export const ProfilePriorityDistribution: React.FC<Props> = ({ userProfile }) =>
}} }}
/> />
) : ( ) : (
<div className="p-7"> <div className="flex-grow p-7">
<ProfileEmptyState <ProfileEmptyState
title="No Data yet" title="No Data yet"
description="Create issues to view the them by priority in the graph for better analysis." description="Create issues to view the them by priority in the graph for better analysis."

View File

@ -16,9 +16,9 @@ export const ProfileStateDistribution: React.FC<Props> = ({ stateDistribution, u
if (!userProfile) return null; if (!userProfile) return null;
return ( return (
<div className="space-y-2"> <div className="flex flex-col space-y-2">
<h3 className="text-lg font-medium">Issues by State</h3> <h3 className="text-lg font-medium">Issues by State</h3>
<div className="border border-custom-border-100 rounded p-7"> <div className="flex-grow border border-custom-border-100 rounded p-7">
{userProfile.state_distribution.length > 0 ? ( {userProfile.state_distribution.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-x-6">
<div> <div>

View File

@ -1,9 +1,11 @@
import React, { useEffect, useState } from "react"; import React from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { mutate } from "swr"; import { mutate } from "swr";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// headless ui // headless ui
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// services // services
@ -27,6 +29,11 @@ type TConfirmProjectDeletionProps = {
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
}; };
const defaultValues = {
projectName: "",
confirmDelete: "",
};
export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
isOpen, isOpen,
data, data,
@ -34,51 +41,41 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
onSuccess, onSuccess,
user, user,
}) => { }) => {
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [confirmProjectName, setConfirmProjectName] = useState("");
const [confirmDeleteMyProject, setConfirmDeleteMyProject] = useState(false);
const [selectedProject, setSelectedProject] = useState<IProject | null>(null);
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const canDelete = confirmProjectName === data?.name && confirmDeleteMyProject; const {
control,
formState: { isSubmitting },
handleSubmit,
reset,
watch,
} = useForm({ defaultValues });
useEffect(() => { const canDelete =
if (data) setSelectedProject(data); watch("projectName") === data?.name && watch("confirmDelete") === "delete my project";
else {
const timer = setTimeout(() => {
setSelectedProject(null);
clearTimeout(timer);
}, 300);
}
}, [data]);
const handleClose = () => { const handleClose = () => {
setIsDeleteLoading(false);
const timer = setTimeout(() => { const timer = setTimeout(() => {
setConfirmProjectName(""); reset(defaultValues);
setConfirmDeleteMyProject(false);
clearTimeout(timer); clearTimeout(timer);
}, 350); }, 350);
onClose(); onClose();
}; };
const handleDeletion = async () => { const onSubmit = async () => {
if (!data || !workspaceSlug || !canDelete) return; if (!data || !workspaceSlug || !canDelete) return;
setIsDeleteLoading(true);
await projectService await projectService
.deleteProject(workspaceSlug as string, data.id, user) .deleteProject(workspaceSlug.toString(), data.id, user)
.then(() => { .then(() => {
handleClose(); handleClose();
mutate<IProject[]>( mutate<IProject[]>(
PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }), PROJECTS_LIST(workspaceSlug.toString(), { is_favorite: "all" }),
(prevData) => prevData?.filter((project: IProject) => project.id !== data.id), (prevData) => prevData?.filter((project: IProject) => project.id !== data.id),
false false
); );
@ -91,8 +88,7 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
title: "Error!", title: "Error!",
message: "Something went wrong. Please try again later.", message: "Something went wrong. Please try again later.",
}) })
) );
.finally(() => setIsDeleteLoading(false));
}; };
return ( return (
@ -122,7 +118,7 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl"> <Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<div className="flex flex-col gap-6 p-6"> <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-6 p-6">
<div className="flex w-full items-center justify-start gap-6"> <div className="flex w-full items-center justify-start gap-6">
<span className="place-items-center rounded-full bg-red-500/20 p-4"> <span className="place-items-center rounded-full bg-red-500/20 p-4">
<ExclamationTriangleIcon <ExclamationTriangleIcon
@ -137,28 +133,29 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
<span> <span>
<p className="text-sm leading-7 text-custom-text-200"> <p className="text-sm leading-7 text-custom-text-200">
Are you sure you want to delete project{" "} Are you sure you want to delete project{" "}
<span className="break-words font-semibold">{selectedProject?.name}</span>? <span className="break-words font-semibold">{data?.name}</span>? All of the
All of the data related to the project will be permanently removed. This data related to the project will be permanently removed. This action cannot be
action cannot be undone undone
</p> </p>
</span> </span>
<div className="text-custom-text-200"> <div className="text-custom-text-200">
<p className="break-words text-sm "> <p className="break-words text-sm ">
Enter the project name{" "} Enter the project name{" "}
<span className="font-medium text-custom-text-100"> <span className="font-medium text-custom-text-100">{data?.name}</span> to
{selectedProject?.name} continue:
</span>{" "}
to continue:
</p> </p>
<Input <Controller
type="text" control={control}
placeholder="Project name"
className="mt-2"
value={confirmProjectName}
onChange={(e) => {
setConfirmProjectName(e.target.value);
}}
name="projectName" name="projectName"
render={({ field: { onChange, value } }) => (
<Input
type="text"
placeholder="Project name"
className="mt-2"
value={value}
onChange={onChange}
/>
)}
/> />
</div> </div>
<div className="text-custom-text-200"> <div className="text-custom-text-200">
@ -167,31 +164,27 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
<span className="font-medium text-custom-text-100">delete my project</span>{" "} <span className="font-medium text-custom-text-100">delete my project</span>{" "}
below: below:
</p> </p>
<Input <Controller
type="text" control={control}
placeholder="Enter 'delete my project'" name="confirmDelete"
className="mt-2" render={({ field: { onChange, value } }) => (
onChange={(e) => { <Input
if (e.target.value === "delete my project") { type="text"
setConfirmDeleteMyProject(true); placeholder="Enter 'delete my project'"
} else { className="mt-2"
setConfirmDeleteMyProject(false); onChange={onChange}
} value={value}
}} />
name="typeDelete" )}
/> />
</div> </div>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton> <SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<DangerButton <DangerButton type="submit" disabled={!canDelete} loading={isSubmitting}>
onClick={handleDeletion} {isSubmitting ? "Deleting..." : "Delete Project"}
disabled={!canDelete}
loading={isDeleteLoading}
>
{isDeleteLoading ? "Deleting..." : "Delete Project"}
</DangerButton> </DangerButton>
</div> </div>
</div> </form>
</Dialog.Panel> </Dialog.Panel>
</Transition.Child> </Transition.Child>
</div> </div>

View File

@ -15,36 +15,36 @@ export interface BubbleMenuItem {
type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children">; type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children">;
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => { export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
const items: BubbleMenuItem[] = [ const items: BubbleMenuItem[] = [
{ {
name: "bold", name: "bold",
isActive: () => props.editor.isActive("bold"), isActive: () => props.editor?.isActive("bold"),
command: () => props.editor.chain().focus().toggleBold().run(), command: () => props.editor?.chain().focus().toggleBold().run(),
icon: BoldIcon, icon: BoldIcon,
}, },
{ {
name: "italic", name: "italic",
isActive: () => props.editor.isActive("italic"), isActive: () => props.editor?.isActive("italic"),
command: () => props.editor.chain().focus().toggleItalic().run(), command: () => props.editor?.chain().focus().toggleItalic().run(),
icon: ItalicIcon, icon: ItalicIcon,
}, },
{ {
name: "underline", name: "underline",
isActive: () => props.editor.isActive("underline"), isActive: () => props.editor?.isActive("underline"),
command: () => props.editor.chain().focus().toggleUnderline().run(), command: () => props.editor?.chain().focus().toggleUnderline().run(),
icon: UnderlineIcon, icon: UnderlineIcon,
}, },
{ {
name: "strike", name: "strike",
isActive: () => props.editor.isActive("strike"), isActive: () => props.editor?.isActive("strike"),
command: () => props.editor.chain().focus().toggleStrike().run(), command: () => props.editor?.chain().focus().toggleStrike().run(),
icon: StrikethroughIcon, icon: StrikethroughIcon,
}, },
{ {
name: "code", name: "code",
isActive: () => props.editor.isActive("code"), isActive: () => props.editor?.isActive("code"),
command: () => props.editor.chain().focus().toggleCode().run(), command: () => props.editor?.chain().focus().toggleCode().run(),
icon: CodeIcon, icon: CodeIcon,
}, },
]; ];
@ -78,7 +78,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl" className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl"
> >
<NodeSelector <NodeSelector
editor={props.editor} editor={props.editor!}
isOpen={isNodeSelectorOpen} isOpen={isNodeSelectorOpen}
setIsOpen={() => { setIsOpen={() => {
setIsNodeSelectorOpen(!isNodeSelectorOpen); setIsNodeSelectorOpen(!isNodeSelectorOpen);
@ -86,7 +86,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
}} }}
/> />
<LinkSelector <LinkSelector
editor={props.editor} editor={props.editor!!}
isOpen={isLinkSelectorOpen} isOpen={isLinkSelectorOpen}
setIsOpen={() => { setIsOpen={() => {
setIsLinkSelectorOpen(!isLinkSelectorOpen); setIsLinkSelectorOpen(!isLinkSelectorOpen);

View File

@ -1,17 +1,27 @@
import { Editor } from "@tiptap/core"; import { Editor } from "@tiptap/core";
import { Check, Trash } from "lucide-react"; 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 { cn } from "../utils";
import isValidHttpUrl from "./utils/link-validator";
interface LinkSelectorProps { interface LinkSelectorProps {
editor: Editor; editor: Editor;
isOpen: boolean; isOpen: boolean;
setIsOpen: Dispatch<SetStateAction<boolean>>; setIsOpen: Dispatch<SetStateAction<boolean>>;
} }
export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen }) => { export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen }) => {
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(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(() => { useEffect(() => {
inputRef.current && inputRef.current?.focus(); inputRef.current && inputRef.current?.focus();
}); });
@ -38,15 +48,13 @@ export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen
</p> </p>
</button> </button>
{isOpen && ( {isOpen && (
<form <div
onSubmit={(e) => {
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);
}}
className="fixed top-full z-[99999] mt-1 flex w-60 overflow-hidden rounded border border-custom-border-300 bg-custom-background-100 dow-xl animate-in fade-in slide-in-from-top-1" className="fixed top-full z-[99999] mt-1 flex w-60 overflow-hidden rounded border border-custom-border-300 bg-custom-background-100 dow-xl animate-in fade-in slide-in-from-top-1"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault(); onLinkSubmit();
}
}}
> >
<input <input
ref={inputRef} ref={inputRef}
@ -57,6 +65,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen
/> />
{editor.getAttributes("link").href ? ( {editor.getAttributes("link").href ? (
<button <button
type="button"
className="flex items-center rounded-sm p-1 text-red-600 transition-all hover:bg-red-100 dark:hover:bg-red-800" className="flex items-center rounded-sm p-1 text-red-600 transition-all hover:bg-red-100 dark:hover:bg-red-800"
onClick={() => { onClick={() => {
editor.chain().focus().unsetLink().run(); editor.chain().focus().unsetLink().run();
@ -66,11 +75,15 @@ export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen
<Trash className="h-4 w-4" /> <Trash className="h-4 w-4" />
</button> </button>
) : ( ) : (
<button className="flex items-center rounded-sm p-1 text-custom-text-300 transition-all hover:bg-custom-background-90"> <button className="flex items-center rounded-sm p-1 text-custom-text-300 transition-all hover:bg-custom-background-90" type="button"
onClick={() => {
onLinkSubmit();
}}
>
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
</button> </button>
)} )}
</form> </div>
)} )}
</div> </div>
); );

View File

@ -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:";
}

View File

@ -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 (
<>
<Moveable
target={document.querySelector(".ProseMirror-selectednode") as any}
container={null}
origin={false}
edge={false}
throttleDrag={0}
keepRatio={true}
resizable={true}
throttleResize={0}
onResize={({
target,
width,
height,
delta,
}:
any) => {
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;
}}
/>
</>
);
};

View File

@ -1,7 +1,6 @@
import StarterKit from "@tiptap/starter-kit"; import StarterKit from "@tiptap/starter-kit";
import HorizontalRule from "@tiptap/extension-horizontal-rule"; import HorizontalRule from "@tiptap/extension-horizontal-rule";
import TiptapLink from "@tiptap/extension-link"; import TiptapLink from "@tiptap/extension-link";
import TiptapImage from "@tiptap/extension-image";
import Placeholder from "@tiptap/extension-placeholder"; import Placeholder from "@tiptap/extension-placeholder";
import TiptapUnderline from "@tiptap/extension-underline"; import TiptapUnderline from "@tiptap/extension-underline";
import TextStyle from "@tiptap/extension-text-style"; 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 ts from "highlight.js/lib/languages/typescript";
import "highlight.js/styles/github-dark.css"; import "highlight.js/styles/github-dark.css";
import UploadImagesPlugin from "../plugins/upload-image";
import UniqueID from "@tiptap-pro/extension-unique-id"; 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); lowlight.registerLanguage("ts", ts);
const CustomImage = TiptapImage.extend({ export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) => [
addProseMirrorPlugins() {
return [UploadImagesPlugin()];
},
});
export const TiptapExtensions = [
StarterKit.configure({ StarterKit.configure({
bulletList: { bulletList: {
HTMLAttributes: { HTMLAttributes: {
@ -93,13 +87,14 @@ export const TiptapExtensions = [
}, },
}), }),
TiptapLink.configure({ TiptapLink.configure({
protocols: ["http", "https"],
validate: (url) => isValidHttpUrl(url),
HTMLAttributes: { HTMLAttributes: {
class: class:
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
}, },
}), }),
CustomImage.configure({ UpdatedImage.configure({
allowBase64: true,
HTMLAttributes: { HTMLAttributes: {
class: "rounded-lg border border-custom-border-300", class: "rounded-lg border border-custom-border-300",
}, },
@ -117,7 +112,7 @@ export const TiptapExtensions = [
UniqueID.configure({ UniqueID.configure({
types: ["image"], types: ["image"],
}), }),
SlashCommand, SlashCommand(workspaceSlug, setIsSubmitting),
TiptapUnderline, TiptapUnderline,
TextStyle, TextStyle,
Color, Color,

View File

@ -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;

View File

@ -1,14 +1,10 @@
// @ts-nocheck
import { useEditor, EditorContent, Editor } from "@tiptap/react"; import { useEditor, EditorContent, Editor } from "@tiptap/react";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import { EditorBubbleMenu } from "./bubble-menu"; import { EditorBubbleMenu } from "./bubble-menu";
import { TiptapExtensions } from "./extensions"; import { TiptapExtensions } from "./extensions";
import { TiptapEditorProps } from "./props"; import { TiptapEditorProps } from "./props";
import { Node } from "@tiptap/pm/model"; import { useImperativeHandle, useRef } from "react";
import { Editor as CoreEditor } from "@tiptap/core"; import { ImageResizer } from "./extensions/image-resize";
import { useCallback, useImperativeHandle, useRef } from "react";
import { EditorState } from "@tiptap/pm/state";
import fileService from "services/file.service";
export interface ITiptapRichTextEditor { export interface ITiptapRichTextEditor {
value: string; value: string;
@ -18,6 +14,8 @@ export interface ITiptapRichTextEditor {
editorContentCustomClassNames?: string; editorContentCustomClassNames?: string;
onChange?: (json: any, html: string) => void; onChange?: (json: any, html: string) => void;
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void; setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
setShouldShowAlert?: (showAlert: boolean) => void;
workspaceSlug: string;
editable?: boolean; editable?: boolean;
forwardedRef?: any; forwardedRef?: any;
debouncedUpdatesEnabled?: boolean; debouncedUpdatesEnabled?: boolean;
@ -30,22 +28,24 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
forwardedRef, forwardedRef,
editable, editable,
setIsSubmitting, setIsSubmitting,
setShouldShowAlert,
editorContentCustomClassNames, editorContentCustomClassNames,
value, value,
noBorder, noBorder,
workspaceSlug,
borderOnFocus, borderOnFocus,
customClassName, customClassName,
} = props; } = props;
const editor = useEditor({ const editor = useEditor({
editable: editable ?? true, editable: editable ?? true,
editorProps: TiptapEditorProps, editorProps: TiptapEditorProps(workspaceSlug, setIsSubmitting),
extensions: TiptapExtensions, extensions: TiptapExtensions(workspaceSlug, setIsSubmitting),
content: value, content: value,
onUpdate: async ({ editor }) => { onUpdate: async ({ editor }) => {
// for instant feedback loop // for instant feedback loop
setIsSubmitting?.("submitting"); setIsSubmitting?.("submitting");
checkForNodeDeletions(editor); setShouldShowAlert?.(true);
if (debouncedUpdatesEnabled) { if (debouncedUpdatesEnabled) {
debouncedUpdates({ onChange, editor }); debouncedUpdates({ onChange, editor });
} else { } else {
@ -65,45 +65,6 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
}, },
})); }));
const previousState = useRef<EditorState>();
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<string, Node> = {};
previousState.current?.doc.forEach((node) => {
if (node.attrs.id) {
prevNodesById[node.attrs.id] = node;
}
});
const nodesById: Record<string, Node> = {};
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 }) => { const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => {
setTimeout(async () => { setTimeout(async () => {
if (onChange) { if (onChange) {
@ -112,10 +73,9 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
}, 500); }, 500);
}, 1000); }, 1000);
const editorClassNames = `relative w-full max-w-screen-lg mt-2 p-3 relative focus:outline-none rounded-lg 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"} ${ ${noBorder ? "" : "border border-custom-border-200"} ${borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0"
borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0" } ${customClassName}`;
} ${customClassName}`;
if (!editor) return null; if (!editor) return null;
editorRef.current = editor; editorRef.current = editor;
@ -131,6 +91,7 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
{editor && <EditorBubbleMenu editor={editor} />} {editor && <EditorBubbleMenu editor={editor} />}
<div className={`${editorContentCustomClassNames}`}> <div className={`${editorContentCustomClassNames}`}>
<EditorContent editor={editor} /> <EditorContent editor={editor} />
{editor?.isActive("image") && <ImageResizer editor={editor} />}
</div> </div>
</div> </div>
); );

View File

@ -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");
}
}

View File

@ -57,7 +57,7 @@ function findPlaceholder(state: EditorState, id: {}) {
return found.length ? found[0].from : null; 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/")) { if (!file.type.includes("image/")) {
return; return;
} else if (file.size / 1024 / 1024 > 20) { } else if (file.size / 1024 / 1024 > 20) {
@ -82,7 +82,11 @@ export async function startImageUpload(file: File, view: EditorView, pos: number
view.dispatch(tr); view.dispatch(tr);
}; };
const src = await UploadImageHandler(file); if (!workspaceSlug) {
return;
}
setIsSubmitting?.("submitting")
const src = await UploadImageHandler(file, workspaceSlug);
const { schema } = view.state; const { schema } = view.state;
pos = findPlaceholder(view.state, id); pos = findPlaceholder(view.state, id);
@ -96,7 +100,10 @@ export async function startImageUpload(file: File, view: EditorView, pos: number
view.dispatch(transaction); view.dispatch(transaction);
} }
const UploadImageHandler = (file: File): Promise<string> => { const UploadImageHandler = (file: File, workspaceSlug: string): Promise<string> => {
if (!workspaceSlug) {
return Promise.reject("Workspace slug is missing");
}
try { try {
const formData = new FormData(); const formData = new FormData();
formData.append("asset", file); formData.append("asset", file);
@ -104,7 +111,7 @@ const UploadImageHandler = (file: File): Promise<string> => {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const imageUrl = await fileService const imageUrl = await fileService
.uploadFile("plane", formData) .uploadFile(workspaceSlug, formData)
.then((response) => response.asset); .then((response) => response.asset);
const image = new Image(); const image = new Image();

View File

@ -1,56 +1,56 @@
import { EditorProps } from "@tiptap/pm/view"; import { EditorProps } from "@tiptap/pm/view";
import { startImageUpload } from "./plugins/upload-image"; import { startImageUpload } from "./plugins/upload-image";
export const TiptapEditorProps: EditorProps = { export function TiptapEditorProps(workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void): EditorProps {
attributes: { return {
class: `prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none`, 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;
}
}
}, },
}, handleDOMEvents: {
handlePaste: (view, event) => { keydown: (_view, event) => {
if ( // prevent default event listeners from firing when slash command is active
event.clipboardData && if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) {
event.clipboardData.files && const slashCommand = document.querySelector("#slash-command");
event.clipboardData.files[0] if (slashCommand) {
) { return true;
event.preventDefault(); }
const file = event.clipboardData.files[0]; }
const pos = view.state.selection.from; },
},
startImageUpload(file, view, pos); handlePaste: (view, event) => {
return true; if (
} event.clipboardData &&
return false; event.clipboardData.files &&
}, event.clipboardData.files[0]
handleDrop: (view, event, _slice, moved) => { ) {
if ( event.preventDefault();
!moved && const file = event.clipboardData.files[0];
event.dataTransfer && const pos = view.state.selection.from;
event.dataTransfer.files && startImageUpload(file, view, pos, workspaceSlug, setIsSubmitting);
event.dataTransfer.files[0] return true;
) {
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);
} }
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;
},
};
}

View File

@ -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", title: "Text",
@ -163,7 +163,7 @@ const getSuggestionItems = ({ query }: { query: string }) =>
if (input.files?.length) { if (input.files?.length) {
const file = input.files[0]; const file = input.files[0];
const pos = editor.view.state.selection.from; const pos = editor.view.state.selection.from;
startImageUpload(file, editor.view, pos); startImageUpload(file, editor.view, pos, workspaceSlug, setIsSubmitting);
} }
}; };
input.click(); input.click();
@ -328,11 +328,12 @@ const renderItems = () => {
}; };
}; };
const SlashCommand = Command.configure({ export const SlashCommand = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) =>
suggestion: { Command.configure({
items: getSuggestionItems, suggestion: {
render: renderItems, items: getSuggestionItems(workspaceSlug, setIsSubmitting),
}, render: renderItems,
}); },
});
export default SlashCommand; export default SlashCommand;

View File

@ -1,47 +1,76 @@
import React, { useEffect } from "react"; import React, { useEffect, useRef } from "react";
import Link from "next/link"; import Link from "next/link";
// hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector";
type Props = { type Props = {
position: { clickEvent: React.MouseEvent | null;
x: number;
y: number;
};
children: React.ReactNode; children: React.ReactNode;
title?: string | JSX.Element; title?: string | JSX.Element;
isOpen: boolean; isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>; setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
}; };
const ContextMenu = ({ position, children, title, isOpen, setIsOpen }: Props) => { const ContextMenu = ({ clickEvent, children, title, isOpen, setIsOpen }: Props) => {
const contextMenuRef = useRef<HTMLDivElement>(null);
// Close the context menu when clicked outside
useOutsideClickDetector(contextMenuRef, () => {
if (isOpen) setIsOpen(false);
});
useEffect(() => { useEffect(() => {
const hideContextMenu = () => { const hideContextMenu = () => {
if (isOpen) setIsOpen(false); if (isOpen) setIsOpen(false);
}; };
window.addEventListener("click", hideContextMenu); const escapeKeyEvent = (e: KeyboardEvent) => {
window.addEventListener("keydown", (e: KeyboardEvent) => {
if (e.key === "Escape") hideContextMenu(); if (e.key === "Escape") hideContextMenu();
}); };
window.addEventListener("click", hideContextMenu);
window.addEventListener("keydown", escapeKeyEvent);
return () => { return () => {
window.removeEventListener("click", hideContextMenu); window.removeEventListener("click", hideContextMenu);
window.removeEventListener("keydown", hideContextMenu); window.removeEventListener("keydown", escapeKeyEvent);
}; };
}, [isOpen, setIsOpen]); }, [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 ( return (
<div <div
className={`fixed z-20 h-full w-full ${ className={`fixed z-50 top-0 left-0 h-full w-full ${
isOpen ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0" isOpen ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
}`} }`}
> >
<div <div
className={`fixed z-20 flex min-w-[8rem] flex-col items-stretch gap-1 rounded-md border border-custom-border-200 bg-custom-background-90 p-2 text-xs shadow-lg`} ref={contextMenuRef}
style={{ className={`fixed z-50 flex min-w-[8rem] flex-col items-stretch gap-1 rounded-md border border-custom-border-200 bg-custom-background-90 p-2 text-xs shadow-lg`}
left: `${position.x}px`,
top: `${position.y}px`,
}}
> >
{title && ( {title && (
<h4 className="border-b border-custom-border-200 px-1 py-1 pb-2 text-[0.8rem] font-medium"> <h4 className="border-b border-custom-border-200 px-1 py-1 pb-2 text-[0.8rem] font-medium">

View File

@ -29,9 +29,9 @@ export const Input: React.FC<Props> = ({
type={type} type={type}
id={id} id={id}
value={value} value={value}
{...(register && register(name, validations))} {...(register && register(name ?? "", validations))}
onChange={(e) => { onChange={(e) => {
register && register(name).onChange(e); register && register(name ?? "").onChange(e);
onChange && onChange(e); onChange && onChange(e);
}} }}
className={`block rounded-md bg-transparent text-sm focus:outline-none placeholder-custom-text-400 ${ className={`block rounded-md bg-transparent text-sm focus:outline-none placeholder-custom-text-400 ${

View File

@ -3,7 +3,7 @@ import type { UseFormRegister, RegisterOptions } from "react-hook-form";
export interface Props extends React.ComponentPropsWithoutRef<"input"> { export interface Props extends React.ComponentPropsWithoutRef<"input"> {
label?: string; label?: string;
name: string; name?: string;
value?: string | number | readonly string[]; value?: string | number | readonly string[];
mode?: "primary" | "transparent" | "trueTransparent" | "secondary" | "disabled"; mode?: "primary" | "transparent" | "trueTransparent" | "secondary" | "disabled";
register?: UseFormRegister<any>; register?: UseFormRegister<any>;

View File

@ -45,6 +45,7 @@ const restrictedUrls = [
"profile", "profile",
"reset-password", "reset-password",
"sign-up", "sign-up",
"spaces",
"workspace-member-invitation", "workspace-member-invitation",
]; ];

View File

@ -1,9 +1,11 @@
import React, { useEffect, useState } from "react"; import React from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { mutate } from "swr"; import { mutate } from "swr";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// headless ui // headless ui
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// services // services
@ -26,57 +28,63 @@ type Props = {
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
}; };
const defaultValues = {
workspaceName: "",
confirmDelete: "",
};
export const DeleteWorkspaceModal: React.FC<Props> = ({ isOpen, data, onClose, user }) => { export const DeleteWorkspaceModal: React.FC<Props> = ({ isOpen, data, onClose, user }) => {
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [confirmWorkspaceName, setConfirmWorkspaceName] = useState("");
const [confirmDeleteMyWorkspace, setConfirmDeleteMyWorkspace] = useState(false);
const [selectedWorkspace, setSelectedWorkspace] = useState<IWorkspace | null>(null);
const router = useRouter(); const router = useRouter();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
useEffect(() => { const {
if (data) setSelectedWorkspace(data); control,
else { formState: { isSubmitting },
const timer = setTimeout(() => { handleSubmit,
setSelectedWorkspace(null); reset,
clearTimeout(timer); watch,
}, 350); } = useForm({ defaultValues });
}
}, [data]);
const canDelete = confirmWorkspaceName === data?.name && confirmDeleteMyWorkspace; const canDelete =
watch("workspaceName") === data?.name && watch("confirmDelete") === "delete my workspace";
const handleClose = () => { const handleClose = () => {
setIsDeleteLoading(false); const timer = setTimeout(() => {
setConfirmWorkspaceName(""); reset(defaultValues);
setConfirmDeleteMyWorkspace(false); clearTimeout(timer);
}, 350);
onClose(); onClose();
}; };
const handleDeletion = async () => { const onSubmit = async () => {
setIsDeleteLoading(true);
if (!data || !canDelete) return; if (!data || !canDelete) return;
await workspaceService await workspaceService
.deleteWorkspace(data.slug, user) .deleteWorkspace(data.slug, user)
.then(() => { .then(() => {
handleClose(); handleClose();
router.push("/"); router.push("/");
mutate<IWorkspace[]>(USER_WORKSPACES, (prevData) => mutate<IWorkspace[]>(USER_WORKSPACES, (prevData) =>
prevData?.filter((workspace) => workspace.id !== data.id) prevData?.filter((workspace) => workspace.id !== data.id)
); );
setToastAlert({ setToastAlert({
type: "success", type: "success",
message: "Workspace deleted successfully", title: "Success!",
title: "Success", message: "Workspace deleted successfully.",
}); });
}) })
.catch((error) => { .catch(() =>
console.log(error); setToastAlert({
setIsDeleteLoading(false); type: "error",
}); title: "Error!",
message: "Something went wrong. Please try again later.",
})
);
}; };
return ( return (
@ -106,7 +114,7 @@ export const DeleteWorkspaceModal: React.FC<Props> = ({ isOpen, data, onClose, u
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl"> <Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<div className="flex flex-col gap-6 p-6"> <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-6 p-6">
<div className="flex w-full items-center justify-start gap-6"> <div className="flex w-full items-center justify-start gap-6">
<span className="place-items-center rounded-full bg-red-500/20 p-4"> <span className="place-items-center rounded-full bg-red-500/20 p-4">
<ExclamationTriangleIcon <ExclamationTriangleIcon
@ -131,20 +139,21 @@ export const DeleteWorkspaceModal: React.FC<Props> = ({ isOpen, data, onClose, u
<div className="text-custom-text-200"> <div className="text-custom-text-200">
<p className="break-words text-sm "> <p className="break-words text-sm ">
Enter the workspace name{" "} Enter the workspace name{" "}
<span className="font-medium text-custom-text-100"> <span className="font-medium text-custom-text-100">{data?.name}</span> to
{selectedWorkspace?.name} continue:
</span>{" "}
to continue:
</p> </p>
<Input <Controller
type="text" control={control}
placeholder="Workspace name"
className="mt-2"
value={confirmWorkspaceName}
onChange={(e) => {
setConfirmWorkspaceName(e.target.value);
}}
name="workspaceName" name="workspaceName"
render={({ field: { onChange, value } }) => (
<Input
type="text"
placeholder="Workspace name"
className="mt-2"
onChange={onChange}
value={value}
/>
)}
/> />
</div> </div>
@ -154,28 +163,28 @@ export const DeleteWorkspaceModal: React.FC<Props> = ({ isOpen, data, onClose, u
<span className="font-medium text-custom-text-100">delete my workspace</span>{" "} <span className="font-medium text-custom-text-100">delete my workspace</span>{" "}
below: below:
</p> </p>
<Input <Controller
type="text" control={control}
placeholder="Enter 'delete my workspace'" name="confirmDelete"
className="mt-2" render={({ field: { onChange, value } }) => (
onChange={(e) => { <Input
if (e.target.value === "delete my workspace") { type="text"
setConfirmDeleteMyWorkspace(true); placeholder="Enter 'delete my workspace'"
} else { className="mt-2"
setConfirmDeleteMyWorkspace(false); onChange={onChange}
} value={value}
}} />
name="typeDelete" )}
/> />
</div> </div>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton> <SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<DangerButton onClick={handleDeletion} loading={isDeleteLoading || !canDelete}> <DangerButton type="submit" disabled={!canDelete} loading={isSubmitting}>
{isDeleteLoading ? "Deleting..." : "Delete Workspace"} {isSubmitting ? "Deleting..." : "Delete Workspace"}
</DangerButton> </DangerButton>
</div> </div>
</div> </form>
</Dialog.Panel> </Dialog.Panel>
</Transition.Child> </Transition.Child>
</div> </div>

View File

@ -8,10 +8,10 @@ const useOutsideClickDetector = (ref: React.RefObject<HTMLElement>, callback: ()
}; };
useEffect(() => { useEffect(() => {
document.addEventListener("click", handleClick); document.addEventListener("mousedown", handleClick);
return () => { return () => {
document.removeEventListener("click", handleClick); document.removeEventListener("mousedown", handleClick);
}; };
}); });
}; };

View File

@ -52,10 +52,10 @@
"highlight.js": "^11.8.0", "highlight.js": "^11.8.0",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"mobx": "^6.10.0",
"mobx-react-lite": "^4.0.3",
"lowlight": "^2.9.0", "lowlight": "^2.9.0",
"lucide-react": "^0.263.1", "lucide-react": "^0.263.1",
"mobx": "^6.10.0",
"mobx-react-lite": "^4.0.3",
"next": "12.3.2", "next": "12.3.2",
"next-pwa": "^5.6.0", "next-pwa": "^5.6.0",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
@ -68,6 +68,7 @@
"react-dropzone": "^14.2.3", "react-dropzone": "^14.2.3",
"react-hook-form": "^7.38.0", "react-hook-form": "^7.38.0",
"react-markdown": "^8.0.7", "react-markdown": "^8.0.7",
"react-moveable": "^0.54.1",
"sharp": "^0.32.1", "sharp": "^0.32.1",
"sonner": "^0.6.2", "sonner": "^0.6.2",
"swr": "^2.1.3", "swr": "^2.1.3",

View File

@ -106,6 +106,7 @@ const ProfileActivity = () => {
</div> </div>
<div className="issue-comments-section p-0"> <div className="issue-comments-section p-0">
<Tiptap <Tiptap
workspaceSlug={workspaceSlug as string}
value={ value={
activityItem?.new_value !== "" activityItem?.new_value !== ""
? activityItem.new_value ? activityItem.new_value

View File

@ -126,3 +126,27 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
transition: opacity 0.2s ease-out; 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);
}
}

View File

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

View File

@ -25,7 +25,8 @@ const WorkspaceProjectPage = observer(() => {
const routerSearchparams = useSearchParams(); const routerSearchparams = useSearchParams();
const { workspace_slug, project_slug } = routerParams as { workspace_slug: string; project_slug: string }; 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 // updating default board view when we are in the issues page
useEffect(() => { useEffect(() => {

View File

@ -1,10 +1,6 @@
"use client"; "use client";
import { useEffect } from "react"; import { useEffect } from "react";
// next imports
import { useSearchParams } from "next/navigation";
// interface
import { TIssueBoardKeys } from "store/types";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root"; import { RootStore } from "store/root";
@ -12,11 +8,6 @@ import { RootStore } from "store/root";
const MobxStoreInit = () => { const MobxStoreInit = () => {
const store: RootStore = useMobxStore(); const store: RootStore = useMobxStore();
// search params
const routerSearchparams = useSearchParams();
const board = routerSearchparams.get("board") as TIssueBoardKeys;
useEffect(() => { useEffect(() => {
// theme // theme
const _theme = localStorage && localStorage.getItem("app_theme") ? localStorage.getItem("app_theme") : "light"; const _theme = localStorage && localStorage.getItem("app_theme") ? localStorage.getItem("app_theme") : "light";

View File

@ -1,9 +1,14 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const path = require('path')
const nextConfig = { const nextConfig = {
reactStrictMode: false,
swcMinify: true,
experimental: { experimental: {
outputFileTracingRoot: path.join(__dirname, "../../"),
appDir: true, appDir: true,
}, },
output: 'standalone'
}; };
module.exports = nextConfig; module.exports = nextConfig;

View File

@ -1,11 +1,11 @@
{ {
"name": "plane-deploy", "name": "space",
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev -p 4000",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start -p 4000",
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {

10
apps/space/pages/_app.tsx Normal file
View File

@ -0,0 +1,10 @@
// styles
import "styles/globals.css";
// types
import type { AppProps } from "next/app";
function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}
export default MyApp;

View File

@ -0,0 +1,17 @@
import Document, { Html, Head, Main, NextScript } from "next/document";
class MyDocument extends Document {
render() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;

View File

@ -3,7 +3,7 @@ import axios from "axios";
// js cookie // js cookie
import Cookies from "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 { abstract class APIService {
protected baseURL: string; protected baseURL: string;

View File

@ -38,11 +38,12 @@ services:
container_name: planefrontend container_name: planefrontend
image: makeplane/plane-frontend:latest image: makeplane/plane-frontend:latest
restart: always restart: always
command: /usr/local/bin/start.sh command: /usr/local/bin/start.sh apps/app/server.js app
env_file: env_file:
- .env - .env
environment: environment:
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} 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_GOOGLE_CLIENTID: "0"
NEXT_PUBLIC_GITHUB_APP_NAME: "0" NEXT_PUBLIC_GITHUB_APP_NAME: "0"
NEXT_PUBLIC_GITHUB_ID: "0" NEXT_PUBLIC_GITHUB_ID: "0"
@ -54,6 +55,20 @@ services:
depends_on: depends_on:
- plane-api - plane-api
- plane-worker - 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: plane-api:
container_name: planebackend container_name: planebackend

View File

@ -41,12 +41,14 @@ services:
dockerfile: ./apps/app/Dockerfile.web dockerfile: ./apps/app/Dockerfile.web
args: args:
NEXT_PUBLIC_API_BASE_URL: http://localhost:8000 NEXT_PUBLIC_API_BASE_URL: http://localhost:8000
NEXT_PUBLIC_DEPLOY_URL: http://localhost/spaces
restart: always restart: always
command: /usr/local/bin/start.sh command: /usr/local/bin/start.sh apps/app/server.js app
env_file: env_file:
- .env - .env
environment: environment:
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} 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_GOOGLE_CLIENTID: "0"
NEXT_PUBLIC_GITHUB_APP_NAME: "0" NEXT_PUBLIC_GITHUB_APP_NAME: "0"
NEXT_PUBLIC_GITHUB_ID: "0" NEXT_PUBLIC_GITHUB_ID: "0"
@ -58,6 +60,23 @@ services:
depends_on: depends_on:
- plane-api - plane-api
- plane-worker - 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: plane-api:
container_name: planebackend container_name: planebackend

View File

@ -18,6 +18,11 @@ server {
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; 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; error_page 500 502 503 504 /50x.html;
location = /50x.html { location = /50x.html {
root /usr/share/nginx/html; root /usr/share/nginx/html;

View File

@ -15,6 +15,10 @@ server {
proxy_pass http://planefrontend:3000/; proxy_pass http://planefrontend:3000/;
} }
location /spaces/ {
proxy_pass http://planedeploy:3000/;
}
location /api/ { location /api/ {
proxy_pass http://planebackend:8000/api/; proxy_pass http://planebackend:8000/api/;
} }

View File

@ -1,6 +1,7 @@
#!/bin/sh #!/bin/sh
FROM=$1 FROM=$1
TO=$2 TO=$2
DIRECTORY=$3
if [ "${FROM}" = "${TO}" ]; then if [ "${FROM}" = "${TO}" ]; then
echo "Nothing to replace, the value is already set to ${TO}." 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. # Only perform action if $FROM and $TO are different.
echo "Replacing all statically built instances of $FROM with this string $TO ." 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" "{}"

View File

@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
# cp ./.env.example ./.env cp ./.env.example ./.env
# Export for tr error in mac # Export for tr error in mac
export LC_ALL=C export LC_ALL=C

View File

@ -3,7 +3,7 @@ set -x
# Replace the statically built BUILT_NEXT_PUBLIC_API_BASE_URL with run-time NEXT_PUBLIC_API_BASE_URL # 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. # 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.." echo "Starting Plane Frontend.."
node apps/app/server.js node $1

165
yarn.lock
View File

@ -1006,6 +1006,40 @@
react-popper "^2.3.0" react-popper "^2.3.0"
tslib "~2.5.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": "@emotion/babel-plugin@^11.11.0":
version "11.11.0" version "11.11.0"
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c" 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" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.3.3.tgz#16ab6c727d8c2020a5b6e4a176a243ecd88d8d69"
integrity sha512-0xd7qez0AQ+MbHatZTlI1gu5vkG8r7MYRUJAHPAHJBmGLs16zpkrpAVLvjQKQOqaXPDUBwOiJzNc00znHSCVBw== 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": "@sentry-internal/tracing@7.63.0":
version "7.63.0" version "7.63.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.63.0.tgz#58903b2205456034611cc5bc1b5b2479275f89c7" 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: dependencies:
tiny-invariant "^1.0.6" 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: cssesc@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" 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" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== 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: fs-constants@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" 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" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== 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: 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" version "1.2.1"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" 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.assign "^4.1.4"
object.values "^1.1.6" 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: kleur@^4.0.3:
version "4.1.5" version "4.1.5"
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" 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" resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.1.1.tgz#61481269c44031c449915497bf5a4ad273c512d2"
integrity sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g== 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: p-limit@^2.2.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" 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-history "^1.0.0"
prosemirror-state "^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" version "1.18.1"
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.18.1.tgz#1d5d6b6de7b983ee67a479dc607165fdef3935bd" resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.18.1.tgz#1d5d6b6de7b983ee67a479dc607165fdef3935bd"
integrity sha512-IxSVBKAEMjD7s3n8cgtwMlxAXZrC7Mlag7zYsAKDndAqnDScvSmp/UdnRTV/B33lTCVU3CCm7dyAn/rVVD0mcw== integrity sha512-IxSVBKAEMjD7s3n8cgtwMlxAXZrC7Mlag7zYsAKDndAqnDScvSmp/UdnRTV/B33lTCVU3CCm7dyAn/rVVD0mcw==
dependencies: dependencies:
orderedmap "^2.0.0" 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: prosemirror-schema-basic@^1.2.0:
version "1.2.2" version "1.2.2"
resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.2.tgz#6695f5175e4628aab179bf62e5568628b9cfe6c7" 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" reactcss "^1.2.0"
tinycolor2 "^1.4.1" 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: react-datepicker@^4.8.0:
version "4.16.0" version "4.16.0"
resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-4.16.0.tgz#b9dd389bb5611a1acc514bba1dd944be21dd877f" 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" unist-util-visit "^4.0.0"
vfile "^5.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: react-onclickoutside@^6.12.2:
version "6.13.0" version "6.13.0"
resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz#e165ea4e5157f3da94f4376a3ab3e22a565f4ffc" 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-callback-ref "^1.3.0"
use-sidecar "^1.1.2" 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: react-style-singleton@^2.2.1:
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" 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 "^6.12.5"
ajv-keywords "^3.5.2" 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: semver@^6.0.0, semver@^6.3.0, semver@^6.3.1:
version "6.3.1" version "6.3.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"