mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge pull request #1923 from makeplane/develop
promote: develop to stage-release
This commit is contained in:
commit
3beab9de6f
13
Dockerfile
13
Dockerfile
@ -5,9 +5,11 @@ WORKDIR /app
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
|
||||
|
||||
RUN yarn global add turbo
|
||||
RUN apk add tree
|
||||
COPY . .
|
||||
|
||||
RUN turbo prune --scope=app --docker
|
||||
RUN turbo prune --scope=app --scope=plane-deploy --docker
|
||||
CMD tree -I node_modules/
|
||||
|
||||
# Add lockfile and package.json's of isolated subworkspace
|
||||
FROM node:18-alpine AS installer
|
||||
@ -21,14 +23,14 @@ COPY --from=builder /app/out/json/ .
|
||||
COPY --from=builder /app/out/yarn.lock ./yarn.lock
|
||||
RUN yarn install
|
||||
|
||||
# Build the project
|
||||
# # Build the project
|
||||
COPY --from=builder /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
COPY replace-env-vars.sh /usr/local/bin/
|
||||
USER root
|
||||
RUN chmod +x /usr/local/bin/replace-env-vars.sh
|
||||
|
||||
RUN yarn turbo run build --filter=app
|
||||
RUN yarn turbo run build
|
||||
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
|
||||
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
@ -96,11 +98,16 @@ RUN adduser --system --uid 1001 captain
|
||||
|
||||
COPY --from=installer /app/apps/app/next.config.js .
|
||||
COPY --from=installer /app/apps/app/package.json .
|
||||
COPY --from=installer /app/apps/space/next.config.js .
|
||||
COPY --from=installer /app/apps/space/package.json .
|
||||
|
||||
COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone ./
|
||||
|
||||
COPY --from=installer --chown=captain:plane /app/apps/app/.next/static ./apps/app/.next/static
|
||||
|
||||
COPY --from=installer --chown=captain:plane /app/apps/space/.next/standalone ./
|
||||
COPY --from=installer --chown=captain:plane /app/apps/space/.next ./apps/space/.next
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
# RUN rm /etc/nginx/conf.d/default.conf
|
||||
|
@ -33,8 +33,8 @@ RUN yarn turbo run build --filter=app
|
||||
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
|
||||
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL}
|
||||
ENV NEXT_PUBLIC_DEPLOY_URL=${NEXT_PUBLIC_DEPLOY_URL}
|
||||
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL} app
|
||||
|
||||
FROM node:18-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
@ -140,14 +140,14 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<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 ${
|
||||
isOpen ? "block" : "hidden"
|
||||
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"
|
||||
}`}
|
||||
>
|
||||
{((content && content !== "") || (htmlContent && htmlContent !== "<p></p>")) && (
|
||||
<div className="text-sm">
|
||||
Content:
|
||||
<TiptapEditor
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
value={htmlContent ?? `<p>${content}</p>`}
|
||||
customClassName="-m-3"
|
||||
noBorder
|
||||
@ -161,6 +161,7 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
<div className="page-block-section text-sm">
|
||||
Response:
|
||||
<Tiptap
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
value={`<p>${response}</p>`}
|
||||
customClassName="-mx-3 -my-3"
|
||||
noBorder
|
||||
@ -179,8 +180,7 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
type="text"
|
||||
name="task"
|
||||
register={register}
|
||||
placeholder={`${
|
||||
content && content !== ""
|
||||
placeholder={`${content && content !== ""
|
||||
? "Tell AI what action to perform on this content..."
|
||||
: "Ask AI anything..."
|
||||
}`}
|
||||
|
@ -86,7 +86,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
}) => {
|
||||
// context menu
|
||||
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 [isDropdownActive, setIsDropdownActive] = useState(false);
|
||||
|
||||
@ -201,7 +202,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
return (
|
||||
<>
|
||||
<ContextMenu
|
||||
position={contextMenuPosition}
|
||||
clickEvent={contextMenuPosition}
|
||||
title="Quick actions"
|
||||
isOpen={contextMenu}
|
||||
setIsOpen={setContextMenu}
|
||||
@ -243,7 +244,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
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]">
|
||||
|
@ -33,7 +33,7 @@ import {
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { LayerDiagonalIcon } from "components/icons";
|
||||
// helpers
|
||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
import { handleIssuesMutation } from "constants/issue";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types";
|
||||
@ -71,7 +71,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
}) => {
|
||||
// context menu
|
||||
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 { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
@ -167,7 +167,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
return (
|
||||
<>
|
||||
<ContextMenu
|
||||
position={contextMenuPosition}
|
||||
clickEvent={contextMenuPosition}
|
||||
title="Quick actions"
|
||||
isOpen={contextMenu}
|
||||
setIsOpen={setContextMenu}
|
||||
@ -199,7 +199,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
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">
|
||||
|
@ -293,6 +293,7 @@ export const InboxMainContent: React.FC = () => {
|
||||
</div>
|
||||
<div>
|
||||
<IssueDescriptionForm
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
issue={{
|
||||
name: issueDetails.name,
|
||||
description: issueDetails.description,
|
||||
|
@ -175,6 +175,7 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
|
||||
return (
|
||||
<div key={activityItem.id} className="mt-4">
|
||||
<CommentCard
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
comment={activityItem as IIssueComment}
|
||||
onSubmit={handleCommentUpdate}
|
||||
handleCommentDeletion={handleCommentDelete}
|
||||
|
@ -93,6 +93,7 @@ export const AddComment: React.FC<Props> = ({ issueId, user, disabled = false })
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<TiptapEditor
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
ref={editorRef}
|
||||
value={
|
||||
!value ||
|
||||
|
@ -22,12 +22,13 @@ const TiptapEditor = React.forwardRef<ITiptapRichTextEditor, ITiptapRichTextEdit
|
||||
TiptapEditor.displayName = "TiptapEditor";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
comment: IIssueComment;
|
||||
onSubmit: (comment: IIssueComment) => 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 editorRef = React.useRef<any>(null);
|
||||
@ -109,6 +110,7 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
|
||||
>
|
||||
<div>
|
||||
<TiptapEditor
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
ref={editorRef}
|
||||
value={watch("comment_html")}
|
||||
debouncedUpdatesEnabled={false}
|
||||
@ -138,6 +140,7 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
|
||||
</form>
|
||||
<div className={`${isEditing ? "hidden" : ""}`}>
|
||||
<TiptapEditor
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
ref={showEditorRef}
|
||||
value={comment.comment_html}
|
||||
editable={false}
|
||||
|
@ -24,6 +24,7 @@ export interface IssueDetailsProps {
|
||||
description: string;
|
||||
description_html: string;
|
||||
};
|
||||
workspaceSlug: string;
|
||||
handleFormSubmit: (value: IssueDescriptionFormValues) => Promise<void>;
|
||||
isAllowed: boolean;
|
||||
}
|
||||
@ -31,6 +32,7 @@ export interface IssueDetailsProps {
|
||||
export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
||||
issue,
|
||||
handleFormSubmit,
|
||||
workspaceSlug,
|
||||
isAllowed,
|
||||
}) => {
|
||||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
||||
@ -69,11 +71,15 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (isSubmitting === "submitted") {
|
||||
setShowAlert(false);
|
||||
setTimeout(async () => {
|
||||
setIsSubmitting("saved");
|
||||
}, 2000);
|
||||
} else if (isSubmitting === "submitting") {
|
||||
setShowAlert(true);
|
||||
}
|
||||
}, [isSubmitting]);
|
||||
}, [isSubmitting, setShowAlert]);
|
||||
|
||||
|
||||
// reset form values
|
||||
useEffect(() => {
|
||||
@ -112,8 +118,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
||||
{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">
|
||||
<span
|
||||
className={`${
|
||||
watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : ""
|
||||
className={`${watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : ""
|
||||
}`}
|
||||
>
|
||||
{watch("name").length}
|
||||
@ -139,11 +144,14 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
||||
? watch("description_html")
|
||||
: value
|
||||
}
|
||||
workspaceSlug={workspaceSlug}
|
||||
debouncedUpdatesEnabled={true}
|
||||
setShouldShowAlert={setShowAlert}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
customClassName="min-h-[150px] shadow-sm"
|
||||
editorContentCustomClassNames="pb-9"
|
||||
onChange={(description: Object, description_html: string) => {
|
||||
setShowAlert(true);
|
||||
setIsSubmitting("submitting");
|
||||
onChange(description_html);
|
||||
setValue("description", description);
|
||||
@ -156,8 +164,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
||||
}}
|
||||
/>
|
||||
<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 ${
|
||||
isSubmitting === "saved" ? "fadeOut" : "fadeIn"
|
||||
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 === "submitting" ? "Saving..." : "Saved"}
|
||||
|
@ -370,6 +370,7 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
|
||||
return (
|
||||
<TiptapEditor
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
ref={editorRef}
|
||||
debouncedUpdatesEnabled={false}
|
||||
value={
|
||||
|
@ -124,6 +124,7 @@ export const IssueMainContent: React.FC<Props> = ({
|
||||
</div>
|
||||
) : null}
|
||||
<IssueDescriptionForm
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
issue={issueDetails}
|
||||
handleFormSubmit={submitChanges}
|
||||
isAllowed={memberRole.isMember || memberRole.isOwner || !uneditable}
|
||||
|
@ -17,7 +17,7 @@ type Props = {
|
||||
|
||||
export const IssueDateSelect: React.FC<Props> = ({ label, maxDate, minDate, onChange, value }) => (
|
||||
<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">
|
||||
<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) => {
|
||||
if (!val) onChange("");
|
||||
else onChange(renderDateFormat(val));
|
||||
|
||||
close();
|
||||
}}
|
||||
dateFormat="dd-MM-yyyy"
|
||||
minDate={minDate}
|
||||
|
@ -1,15 +1,11 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { ModuleLeadSelect, ModuleMembersSelect, ModuleStatusSelect } from "components/modules";
|
||||
// ui
|
||||
import { DateSelect, Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
|
||||
// helper
|
||||
import { isDateRangeValid } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IModule } from "types";
|
||||
|
||||
@ -29,8 +25,6 @@ const defaultValues: Partial<IModule> = {
|
||||
};
|
||||
|
||||
export const ModuleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, status, data }) => {
|
||||
const [isDateValid, setIsDateValid] = useState(true);
|
||||
const { setToastAlert } = useToast();
|
||||
const {
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
@ -57,6 +51,15 @@ export const ModuleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, sta
|
||||
});
|
||||
}, [data, reset]);
|
||||
|
||||
const startDate = watch("start_date");
|
||||
const targetDate = watch("target_date");
|
||||
|
||||
const minDate = startDate ? new Date(startDate) : null;
|
||||
minDate?.setDate(minDate.getDate());
|
||||
|
||||
const maxDate = targetDate ? new Date(targetDate) : null;
|
||||
maxDate?.setDate(maxDate.getDate());
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleCreateUpdateModule)}>
|
||||
<div className="space-y-5">
|
||||
@ -103,20 +106,8 @@ export const ModuleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, sta
|
||||
value={value}
|
||||
onChange={(val) => {
|
||||
onChange(val);
|
||||
if (val && watch("target_date")) {
|
||||
if (isDateRangeValid(val, `${watch("target_date")}`)) {
|
||||
setIsDateValid(true);
|
||||
} else {
|
||||
setIsDateValid(false);
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message:
|
||||
"The date you have entered is invalid. Please check and enter a valid date.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
maxDate={maxDate ?? undefined}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -129,20 +120,8 @@ export const ModuleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, sta
|
||||
value={value}
|
||||
onChange={(val) => {
|
||||
onChange(val);
|
||||
if (watch("start_date") && val) {
|
||||
if (isDateRangeValid(`${watch("start_date")}`, val)) {
|
||||
setIsDateValid(true);
|
||||
} else {
|
||||
setIsDateValid(false);
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message:
|
||||
"The date you have entered is invalid. Please check and enter a valid date.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
minDate={minDate ?? undefined}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -166,7 +145,7 @@ export const ModuleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, sta
|
||||
</div>
|
||||
<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>
|
||||
<PrimaryButton type="submit" loading={isSubmitting || isDateValid ? false : true}>
|
||||
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||
{status
|
||||
? isSubmitting
|
||||
? "Updating Module..."
|
||||
|
@ -292,6 +292,7 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
||||
if (!data)
|
||||
return (
|
||||
<TiptapEditor
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
ref={editorRef}
|
||||
value={"<p></p>"}
|
||||
debouncedUpdatesEnabled={false}
|
||||
@ -311,12 +312,11 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<TiptapEditor
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
ref={editorRef}
|
||||
value={
|
||||
value && value !== "" && Object.keys(value).length > 0
|
||||
? value
|
||||
: watch("description_html") && watch("description_html") !== ""
|
||||
? watch("description_html")
|
||||
: { type: "doc", content: [{ type: "paragraph" }] }
|
||||
}
|
||||
debouncedUpdatesEnabled={false}
|
||||
@ -334,8 +334,7 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
||||
<div className="m-2 mt-6 flex">
|
||||
<button
|
||||
type="button"
|
||||
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" : ""
|
||||
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" : ""
|
||||
}`}
|
||||
onClick={handleAutoGenerateDescription}
|
||||
disabled={iAmFeelingLucky}
|
||||
|
@ -456,6 +456,7 @@ export const SinglePageBlock: React.FC<Props> = ({
|
||||
{showBlockDetails
|
||||
? block.description_html.length > 7 && (
|
||||
<TiptapEditor
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
value={block.description_html}
|
||||
customClassName="text-sm min-h-[150px]"
|
||||
noBorder
|
||||
|
@ -12,10 +12,10 @@ type Props = {
|
||||
};
|
||||
|
||||
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>
|
||||
{userProfile ? (
|
||||
<div className="border border-custom-border-100 rounded">
|
||||
<div className="flex-grow border border-custom-border-100 rounded">
|
||||
{userProfile.priority_distribution.length > 0 ? (
|
||||
<BarGraph
|
||||
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
|
||||
title="No Data yet"
|
||||
description="Create issues to view the them by priority in the graph for better analysis."
|
||||
|
@ -16,9 +16,9 @@ export const ProfileStateDistribution: React.FC<Props> = ({ stateDistribution, u
|
||||
if (!userProfile) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<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 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6">
|
||||
<div>
|
||||
|
@ -1,9 +1,11 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
@ -27,6 +29,11 @@ type TConfirmProjectDeletionProps = {
|
||||
user: ICurrentUserResponse | undefined;
|
||||
};
|
||||
|
||||
const defaultValues = {
|
||||
projectName: "",
|
||||
confirmDelete: "",
|
||||
};
|
||||
|
||||
export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
|
||||
isOpen,
|
||||
data,
|
||||
@ -34,51 +41,41 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
|
||||
onSuccess,
|
||||
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 { workspaceSlug } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const canDelete = confirmProjectName === data?.name && confirmDeleteMyProject;
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
} = useForm({ defaultValues });
|
||||
|
||||
useEffect(() => {
|
||||
if (data) setSelectedProject(data);
|
||||
else {
|
||||
const timer = setTimeout(() => {
|
||||
setSelectedProject(null);
|
||||
clearTimeout(timer);
|
||||
}, 300);
|
||||
}
|
||||
}, [data]);
|
||||
const canDelete =
|
||||
watch("projectName") === data?.name && watch("confirmDelete") === "delete my project";
|
||||
|
||||
const handleClose = () => {
|
||||
setIsDeleteLoading(false);
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setConfirmProjectName("");
|
||||
setConfirmDeleteMyProject(false);
|
||||
reset(defaultValues);
|
||||
clearTimeout(timer);
|
||||
}, 350);
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDeletion = async () => {
|
||||
const onSubmit = async () => {
|
||||
if (!data || !workspaceSlug || !canDelete) return;
|
||||
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
await projectService
|
||||
.deleteProject(workspaceSlug as string, data.id, user)
|
||||
.deleteProject(workspaceSlug.toString(), data.id, user)
|
||||
.then(() => {
|
||||
handleClose();
|
||||
|
||||
mutate<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),
|
||||
false
|
||||
);
|
||||
@ -91,8 +88,7 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
|
||||
title: "Error!",
|
||||
message: "Something went wrong. Please try again later.",
|
||||
})
|
||||
)
|
||||
.finally(() => setIsDeleteLoading(false));
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -122,7 +118,7 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
|
||||
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">
|
||||
<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">
|
||||
<span className="place-items-center rounded-full bg-red-500/20 p-4">
|
||||
<ExclamationTriangleIcon
|
||||
@ -137,28 +133,29 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
|
||||
<span>
|
||||
<p className="text-sm leading-7 text-custom-text-200">
|
||||
Are you sure you want to delete project{" "}
|
||||
<span className="break-words font-semibold">{selectedProject?.name}</span>?
|
||||
All of the data related to the project will be permanently removed. This
|
||||
action cannot be undone
|
||||
<span className="break-words font-semibold">{data?.name}</span>? All of the
|
||||
data related to the project will be permanently removed. This action cannot be
|
||||
undone
|
||||
</p>
|
||||
</span>
|
||||
<div className="text-custom-text-200">
|
||||
<p className="break-words text-sm ">
|
||||
Enter the project name{" "}
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{selectedProject?.name}
|
||||
</span>{" "}
|
||||
to continue:
|
||||
<span className="font-medium text-custom-text-100">{data?.name}</span> to
|
||||
continue:
|
||||
</p>
|
||||
<Controller
|
||||
control={control}
|
||||
name="projectName"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Project name"
|
||||
className="mt-2"
|
||||
value={confirmProjectName}
|
||||
onChange={(e) => {
|
||||
setConfirmProjectName(e.target.value);
|
||||
}}
|
||||
name="projectName"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<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>{" "}
|
||||
below:
|
||||
</p>
|
||||
<Controller
|
||||
control={control}
|
||||
name="confirmDelete"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter 'delete my project'"
|
||||
className="mt-2"
|
||||
onChange={(e) => {
|
||||
if (e.target.value === "delete my project") {
|
||||
setConfirmDeleteMyProject(true);
|
||||
} else {
|
||||
setConfirmDeleteMyProject(false);
|
||||
}
|
||||
}}
|
||||
name="typeDelete"
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||
<DangerButton
|
||||
onClick={handleDeletion}
|
||||
disabled={!canDelete}
|
||||
loading={isDeleteLoading}
|
||||
>
|
||||
{isDeleteLoading ? "Deleting..." : "Delete Project"}
|
||||
<DangerButton type="submit" disabled={!canDelete} loading={isSubmitting}>
|
||||
{isSubmitting ? "Deleting..." : "Delete Project"}
|
||||
</DangerButton>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
|
@ -15,36 +15,36 @@ export interface BubbleMenuItem {
|
||||
|
||||
type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children">;
|
||||
|
||||
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
||||
const items: BubbleMenuItem[] = [
|
||||
{
|
||||
name: "bold",
|
||||
isActive: () => props.editor.isActive("bold"),
|
||||
command: () => props.editor.chain().focus().toggleBold().run(),
|
||||
isActive: () => props.editor?.isActive("bold"),
|
||||
command: () => props.editor?.chain().focus().toggleBold().run(),
|
||||
icon: BoldIcon,
|
||||
},
|
||||
{
|
||||
name: "italic",
|
||||
isActive: () => props.editor.isActive("italic"),
|
||||
command: () => props.editor.chain().focus().toggleItalic().run(),
|
||||
isActive: () => props.editor?.isActive("italic"),
|
||||
command: () => props.editor?.chain().focus().toggleItalic().run(),
|
||||
icon: ItalicIcon,
|
||||
},
|
||||
{
|
||||
name: "underline",
|
||||
isActive: () => props.editor.isActive("underline"),
|
||||
command: () => props.editor.chain().focus().toggleUnderline().run(),
|
||||
isActive: () => props.editor?.isActive("underline"),
|
||||
command: () => props.editor?.chain().focus().toggleUnderline().run(),
|
||||
icon: UnderlineIcon,
|
||||
},
|
||||
{
|
||||
name: "strike",
|
||||
isActive: () => props.editor.isActive("strike"),
|
||||
command: () => props.editor.chain().focus().toggleStrike().run(),
|
||||
isActive: () => props.editor?.isActive("strike"),
|
||||
command: () => props.editor?.chain().focus().toggleStrike().run(),
|
||||
icon: StrikethroughIcon,
|
||||
},
|
||||
{
|
||||
name: "code",
|
||||
isActive: () => props.editor.isActive("code"),
|
||||
command: () => props.editor.chain().focus().toggleCode().run(),
|
||||
isActive: () => props.editor?.isActive("code"),
|
||||
command: () => props.editor?.chain().focus().toggleCode().run(),
|
||||
icon: CodeIcon,
|
||||
},
|
||||
];
|
||||
@ -78,7 +78,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl"
|
||||
>
|
||||
<NodeSelector
|
||||
editor={props.editor}
|
||||
editor={props.editor!}
|
||||
isOpen={isNodeSelectorOpen}
|
||||
setIsOpen={() => {
|
||||
setIsNodeSelectorOpen(!isNodeSelectorOpen);
|
||||
@ -86,7 +86,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
}}
|
||||
/>
|
||||
<LinkSelector
|
||||
editor={props.editor}
|
||||
editor={props.editor!!}
|
||||
isOpen={isLinkSelectorOpen}
|
||||
setIsOpen={() => {
|
||||
setIsLinkSelectorOpen(!isLinkSelectorOpen);
|
||||
|
@ -1,17 +1,27 @@
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Check, Trash } from "lucide-react";
|
||||
import { Dispatch, FC, SetStateAction, useEffect, useRef } from "react";
|
||||
import { Dispatch, FC, SetStateAction, useCallback, useEffect, useRef } from "react";
|
||||
import { cn } from "../utils";
|
||||
|
||||
import isValidHttpUrl from "./utils/link-validator";
|
||||
interface LinkSelectorProps {
|
||||
editor: Editor;
|
||||
isOpen: boolean;
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
|
||||
export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen }) => {
|
||||
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(() => {
|
||||
inputRef.current && inputRef.current?.focus();
|
||||
});
|
||||
@ -38,15 +48,13 @@ export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen
|
||||
</p>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<form
|
||||
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);
|
||||
}}
|
||||
<div
|
||||
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
|
||||
ref={inputRef}
|
||||
@ -57,6 +65,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen
|
||||
/>
|
||||
{editor.getAttributes("link").href ? (
|
||||
<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"
|
||||
onClick={() => {
|
||||
editor.chain().focus().unsetLink().run();
|
||||
@ -66,11 +75,15 @@ export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen
|
||||
<Trash className="h-4 w-4" />
|
||||
</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" />
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -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:";
|
||||
}
|
||||
|
57
apps/app/components/tiptap/extensions/image-resize.tsx
Normal file
57
apps/app/components/tiptap/extensions/image-resize.tsx
Normal 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;
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,6 @@
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import HorizontalRule from "@tiptap/extension-horizontal-rule";
|
||||
import TiptapLink from "@tiptap/extension-link";
|
||||
import TiptapImage from "@tiptap/extension-image";
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
import TiptapUnderline from "@tiptap/extension-underline";
|
||||
import TextStyle from "@tiptap/extension-text-style";
|
||||
@ -18,18 +17,13 @@ import { InputRule } from "@tiptap/core";
|
||||
import ts from "highlight.js/lib/languages/typescript";
|
||||
|
||||
import "highlight.js/styles/github-dark.css";
|
||||
import UploadImagesPlugin from "../plugins/upload-image";
|
||||
import UniqueID from "@tiptap-pro/extension-unique-id";
|
||||
import UpdatedImage from "./updated-image";
|
||||
import isValidHttpUrl from "../bubble-menu/utils/link-validator";
|
||||
|
||||
lowlight.registerLanguage("ts", ts);
|
||||
|
||||
const CustomImage = TiptapImage.extend({
|
||||
addProseMirrorPlugins() {
|
||||
return [UploadImagesPlugin()];
|
||||
},
|
||||
});
|
||||
|
||||
export const TiptapExtensions = [
|
||||
export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) => [
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
HTMLAttributes: {
|
||||
@ -93,13 +87,14 @@ export const TiptapExtensions = [
|
||||
},
|
||||
}),
|
||||
TiptapLink.configure({
|
||||
protocols: ["http", "https"],
|
||||
validate: (url) => isValidHttpUrl(url),
|
||||
HTMLAttributes: {
|
||||
class:
|
||||
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
|
||||
},
|
||||
}),
|
||||
CustomImage.configure({
|
||||
allowBase64: true,
|
||||
UpdatedImage.configure({
|
||||
HTMLAttributes: {
|
||||
class: "rounded-lg border border-custom-border-300",
|
||||
},
|
||||
@ -117,7 +112,7 @@ export const TiptapExtensions = [
|
||||
UniqueID.configure({
|
||||
types: ["image"],
|
||||
}),
|
||||
SlashCommand,
|
||||
SlashCommand(workspaceSlug, setIsSubmitting),
|
||||
TiptapUnderline,
|
||||
TextStyle,
|
||||
Color,
|
||||
|
22
apps/app/components/tiptap/extensions/updated-image.tsx
Normal file
22
apps/app/components/tiptap/extensions/updated-image.tsx
Normal 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;
|
@ -1,14 +1,10 @@
|
||||
// @ts-nocheck
|
||||
import { useEditor, EditorContent, Editor } from "@tiptap/react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { EditorBubbleMenu } from "./bubble-menu";
|
||||
import { TiptapExtensions } from "./extensions";
|
||||
import { TiptapEditorProps } from "./props";
|
||||
import { Node } from "@tiptap/pm/model";
|
||||
import { Editor as CoreEditor } from "@tiptap/core";
|
||||
import { useCallback, useImperativeHandle, useRef } from "react";
|
||||
import { EditorState } from "@tiptap/pm/state";
|
||||
import fileService from "services/file.service";
|
||||
import { useImperativeHandle, useRef } from "react";
|
||||
import { ImageResizer } from "./extensions/image-resize";
|
||||
|
||||
export interface ITiptapRichTextEditor {
|
||||
value: string;
|
||||
@ -18,6 +14,8 @@ export interface ITiptapRichTextEditor {
|
||||
editorContentCustomClassNames?: string;
|
||||
onChange?: (json: any, html: string) => void;
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
|
||||
setShouldShowAlert?: (showAlert: boolean) => void;
|
||||
workspaceSlug: string;
|
||||
editable?: boolean;
|
||||
forwardedRef?: any;
|
||||
debouncedUpdatesEnabled?: boolean;
|
||||
@ -30,22 +28,24 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
|
||||
forwardedRef,
|
||||
editable,
|
||||
setIsSubmitting,
|
||||
setShouldShowAlert,
|
||||
editorContentCustomClassNames,
|
||||
value,
|
||||
noBorder,
|
||||
workspaceSlug,
|
||||
borderOnFocus,
|
||||
customClassName,
|
||||
} = props;
|
||||
|
||||
const editor = useEditor({
|
||||
editable: editable ?? true,
|
||||
editorProps: TiptapEditorProps,
|
||||
extensions: TiptapExtensions,
|
||||
editorProps: TiptapEditorProps(workspaceSlug, setIsSubmitting),
|
||||
extensions: TiptapExtensions(workspaceSlug, setIsSubmitting),
|
||||
content: value,
|
||||
onUpdate: async ({ editor }) => {
|
||||
// for instant feedback loop
|
||||
setIsSubmitting?.("submitting");
|
||||
checkForNodeDeletions(editor);
|
||||
setShouldShowAlert?.(true);
|
||||
if (debouncedUpdatesEnabled) {
|
||||
debouncedUpdates({ onChange, editor });
|
||||
} else {
|
||||
@ -65,45 +65,6 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
|
||||
},
|
||||
}));
|
||||
|
||||
const previousState = useRef<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 }) => {
|
||||
setTimeout(async () => {
|
||||
if (onChange) {
|
||||
@ -112,9 +73,8 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
|
||||
}, 500);
|
||||
}, 1000);
|
||||
|
||||
const editorClassNames = `relative w-full max-w-screen-lg mt-2 p-3 relative focus:outline-none rounded-lg
|
||||
${noBorder ? "" : "border border-custom-border-200"} ${
|
||||
borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0"
|
||||
const editorClassNames = `relative w-full max-w-screen-lg sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md
|
||||
${noBorder ? "" : "border border-custom-border-200"} ${borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0"
|
||||
} ${customClassName}`;
|
||||
|
||||
if (!editor) return null;
|
||||
@ -131,6 +91,7 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
|
||||
{editor && <EditorBubbleMenu editor={editor} />}
|
||||
<div className={`${editorContentCustomClassNames}`}>
|
||||
<EditorContent editor={editor} />
|
||||
{editor?.isActive("image") && <ImageResizer editor={editor} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
56
apps/app/components/tiptap/plugins/delete-image.tsx
Normal file
56
apps/app/components/tiptap/plugins/delete-image.tsx
Normal 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");
|
||||
}
|
||||
}
|
@ -57,7 +57,7 @@ function findPlaceholder(state: EditorState, id: {}) {
|
||||
return found.length ? found[0].from : null;
|
||||
}
|
||||
|
||||
export async function startImageUpload(file: File, view: EditorView, pos: number) {
|
||||
export async function startImageUpload(file: File, view: EditorView, pos: number, workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) {
|
||||
if (!file.type.includes("image/")) {
|
||||
return;
|
||||
} else if (file.size / 1024 / 1024 > 20) {
|
||||
@ -82,7 +82,11 @@ export async function startImageUpload(file: File, view: EditorView, pos: number
|
||||
view.dispatch(tr);
|
||||
};
|
||||
|
||||
const src = await UploadImageHandler(file);
|
||||
if (!workspaceSlug) {
|
||||
return;
|
||||
}
|
||||
setIsSubmitting?.("submitting")
|
||||
const src = await UploadImageHandler(file, workspaceSlug);
|
||||
const { schema } = view.state;
|
||||
pos = findPlaceholder(view.state, id);
|
||||
|
||||
@ -96,7 +100,10 @@ export async function startImageUpload(file: File, view: EditorView, pos: number
|
||||
view.dispatch(transaction);
|
||||
}
|
||||
|
||||
const UploadImageHandler = (file: File): Promise<string> => {
|
||||
const UploadImageHandler = (file: File, workspaceSlug: string): Promise<string> => {
|
||||
if (!workspaceSlug) {
|
||||
return Promise.reject("Workspace slug is missing");
|
||||
}
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("asset", file);
|
||||
@ -104,7 +111,7 @@ const UploadImageHandler = (file: File): Promise<string> => {
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const imageUrl = await fileService
|
||||
.uploadFile("plane", formData)
|
||||
.uploadFile(workspaceSlug, formData)
|
||||
.then((response) => response.asset);
|
||||
|
||||
const image = new Image();
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
import { startImageUpload } from "./plugins/upload-image";
|
||||
|
||||
export const TiptapEditorProps: EditorProps = {
|
||||
export function TiptapEditorProps(workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void): EditorProps {
|
||||
return {
|
||||
attributes: {
|
||||
class: `prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none`,
|
||||
},
|
||||
@ -25,8 +26,7 @@ export const TiptapEditorProps: EditorProps = {
|
||||
event.preventDefault();
|
||||
const file = event.clipboardData.files[0];
|
||||
const pos = view.state.selection.from;
|
||||
|
||||
startImageUpload(file, view, pos);
|
||||
startImageUpload(file, view, pos, workspaceSlug, setIsSubmitting);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -46,11 +46,11 @@ export const TiptapEditorProps: EditorProps = {
|
||||
});
|
||||
// here we deduct 1 from the pos or else the image will create an extra node
|
||||
if (coordinates) {
|
||||
startImageUpload(file, view, coordinates.pos - 1);
|
||||
startImageUpload(file, view, coordinates.pos - 1, workspaceSlug, setIsSubmitting);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ const Command = Extension.create({
|
||||
},
|
||||
});
|
||||
|
||||
const getSuggestionItems = ({ query }: { query: string }) =>
|
||||
const getSuggestionItems = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) => ({ query }: { query: string }) =>
|
||||
[
|
||||
{
|
||||
title: "Text",
|
||||
@ -163,7 +163,7 @@ const getSuggestionItems = ({ query }: { query: string }) =>
|
||||
if (input.files?.length) {
|
||||
const file = input.files[0];
|
||||
const pos = editor.view.state.selection.from;
|
||||
startImageUpload(file, editor.view, pos);
|
||||
startImageUpload(file, editor.view, pos, workspaceSlug, setIsSubmitting);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
@ -328,11 +328,12 @@ const renderItems = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const SlashCommand = Command.configure({
|
||||
export const SlashCommand = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) =>
|
||||
Command.configure({
|
||||
suggestion: {
|
||||
items: getSuggestionItems,
|
||||
items: getSuggestionItems(workspaceSlug, setIsSubmitting),
|
||||
render: renderItems,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export default SlashCommand;
|
||||
|
@ -1,47 +1,76 @@
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
// hooks
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
|
||||
type Props = {
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
clickEvent: React.MouseEvent | null;
|
||||
children: React.ReactNode;
|
||||
title?: string | JSX.Element;
|
||||
isOpen: boolean;
|
||||
setIsOpen: React.Dispatch<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(() => {
|
||||
const hideContextMenu = () => {
|
||||
if (isOpen) setIsOpen(false);
|
||||
};
|
||||
|
||||
window.addEventListener("click", hideContextMenu);
|
||||
window.addEventListener("keydown", (e: KeyboardEvent) => {
|
||||
const escapeKeyEvent = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") hideContextMenu();
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener("click", hideContextMenu);
|
||||
window.addEventListener("keydown", escapeKeyEvent);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("click", hideContextMenu);
|
||||
window.removeEventListener("keydown", hideContextMenu);
|
||||
window.removeEventListener("keydown", escapeKeyEvent);
|
||||
};
|
||||
}, [isOpen, setIsOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
const contextMenu = contextMenuRef.current;
|
||||
|
||||
if (contextMenu && isOpen) {
|
||||
const contextMenuWidth = contextMenu.clientWidth;
|
||||
const contextMenuHeight = contextMenu.clientHeight;
|
||||
|
||||
const clickX = clickEvent?.pageX || 0;
|
||||
const clickY = clickEvent?.pageY || 0;
|
||||
|
||||
let top = clickY;
|
||||
// check if there's enough space at the bottom, otherwise show at the top
|
||||
if (clickY + contextMenuHeight > window.innerHeight) top = clickY - contextMenuHeight;
|
||||
|
||||
// check if there's enough space on the right, otherwise show on the left
|
||||
let left = clickX;
|
||||
if (clickX + contextMenuWidth > window.innerWidth) left = clickX - contextMenuWidth;
|
||||
|
||||
contextMenu.style.top = `${top}px`;
|
||||
contextMenu.style.left = `${left}px`;
|
||||
}
|
||||
}, [clickEvent, isOpen]);
|
||||
|
||||
return (
|
||||
<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"
|
||||
}`}
|
||||
>
|
||||
<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`}
|
||||
style={{
|
||||
left: `${position.x}px`,
|
||||
top: `${position.y}px`,
|
||||
}}
|
||||
ref={contextMenuRef}
|
||||
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`}
|
||||
>
|
||||
{title && (
|
||||
<h4 className="border-b border-custom-border-200 px-1 py-1 pb-2 text-[0.8rem] font-medium">
|
||||
|
@ -29,9 +29,9 @@ export const Input: React.FC<Props> = ({
|
||||
type={type}
|
||||
id={id}
|
||||
value={value}
|
||||
{...(register && register(name, validations))}
|
||||
{...(register && register(name ?? "", validations))}
|
||||
onChange={(e) => {
|
||||
register && register(name).onChange(e);
|
||||
register && register(name ?? "").onChange(e);
|
||||
onChange && onChange(e);
|
||||
}}
|
||||
className={`block rounded-md bg-transparent text-sm focus:outline-none placeholder-custom-text-400 ${
|
||||
|
2
apps/app/components/ui/input/types.d.ts
vendored
2
apps/app/components/ui/input/types.d.ts
vendored
@ -3,7 +3,7 @@ import type { UseFormRegister, RegisterOptions } from "react-hook-form";
|
||||
|
||||
export interface Props extends React.ComponentPropsWithoutRef<"input"> {
|
||||
label?: string;
|
||||
name: string;
|
||||
name?: string;
|
||||
value?: string | number | readonly string[];
|
||||
mode?: "primary" | "transparent" | "trueTransparent" | "secondary" | "disabled";
|
||||
register?: UseFormRegister<any>;
|
||||
|
@ -45,6 +45,7 @@ const restrictedUrls = [
|
||||
"profile",
|
||||
"reset-password",
|
||||
"sign-up",
|
||||
"spaces",
|
||||
"workspace-member-invitation",
|
||||
];
|
||||
|
||||
|
@ -1,9 +1,11 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
@ -26,57 +28,63 @@ type Props = {
|
||||
user: ICurrentUserResponse | undefined;
|
||||
};
|
||||
|
||||
const defaultValues = {
|
||||
workspaceName: "",
|
||||
confirmDelete: "",
|
||||
};
|
||||
|
||||
export const DeleteWorkspaceModal: React.FC<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 { setToastAlert } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
if (data) setSelectedWorkspace(data);
|
||||
else {
|
||||
const timer = setTimeout(() => {
|
||||
setSelectedWorkspace(null);
|
||||
clearTimeout(timer);
|
||||
}, 350);
|
||||
}
|
||||
}, [data]);
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
} = useForm({ defaultValues });
|
||||
|
||||
const canDelete = confirmWorkspaceName === data?.name && confirmDeleteMyWorkspace;
|
||||
const canDelete =
|
||||
watch("workspaceName") === data?.name && watch("confirmDelete") === "delete my workspace";
|
||||
|
||||
const handleClose = () => {
|
||||
setIsDeleteLoading(false);
|
||||
setConfirmWorkspaceName("");
|
||||
setConfirmDeleteMyWorkspace(false);
|
||||
const timer = setTimeout(() => {
|
||||
reset(defaultValues);
|
||||
clearTimeout(timer);
|
||||
}, 350);
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDeletion = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
const onSubmit = async () => {
|
||||
if (!data || !canDelete) return;
|
||||
|
||||
await workspaceService
|
||||
.deleteWorkspace(data.slug, user)
|
||||
.then(() => {
|
||||
handleClose();
|
||||
|
||||
router.push("/");
|
||||
|
||||
mutate<IWorkspace[]>(USER_WORKSPACES, (prevData) =>
|
||||
prevData?.filter((workspace) => workspace.id !== data.id)
|
||||
);
|
||||
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
message: "Workspace deleted successfully",
|
||||
title: "Success",
|
||||
title: "Success!",
|
||||
message: "Workspace deleted successfully.",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
setIsDeleteLoading(false);
|
||||
});
|
||||
.catch(() =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Something went wrong. Please try again later.",
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -106,7 +114,7 @@ export const DeleteWorkspaceModal: React.FC<Props> = ({ isOpen, data, onClose, u
|
||||
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">
|
||||
<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">
|
||||
<span className="place-items-center rounded-full bg-red-500/20 p-4">
|
||||
<ExclamationTriangleIcon
|
||||
@ -131,20 +139,21 @@ export const DeleteWorkspaceModal: React.FC<Props> = ({ isOpen, data, onClose, u
|
||||
<div className="text-custom-text-200">
|
||||
<p className="break-words text-sm ">
|
||||
Enter the workspace name{" "}
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{selectedWorkspace?.name}
|
||||
</span>{" "}
|
||||
to continue:
|
||||
<span className="font-medium text-custom-text-100">{data?.name}</span> to
|
||||
continue:
|
||||
</p>
|
||||
<Controller
|
||||
control={control}
|
||||
name="workspaceName"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Workspace name"
|
||||
className="mt-2"
|
||||
value={confirmWorkspaceName}
|
||||
onChange={(e) => {
|
||||
setConfirmWorkspaceName(e.target.value);
|
||||
}}
|
||||
name="workspaceName"
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</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>{" "}
|
||||
below:
|
||||
</p>
|
||||
<Controller
|
||||
control={control}
|
||||
name="confirmDelete"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter 'delete my workspace'"
|
||||
className="mt-2"
|
||||
onChange={(e) => {
|
||||
if (e.target.value === "delete my workspace") {
|
||||
setConfirmDeleteMyWorkspace(true);
|
||||
} else {
|
||||
setConfirmDeleteMyWorkspace(false);
|
||||
}
|
||||
}}
|
||||
name="typeDelete"
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||
<DangerButton onClick={handleDeletion} loading={isDeleteLoading || !canDelete}>
|
||||
{isDeleteLoading ? "Deleting..." : "Delete Workspace"}
|
||||
<DangerButton type="submit" disabled={!canDelete} loading={isSubmitting}>
|
||||
{isSubmitting ? "Deleting..." : "Delete Workspace"}
|
||||
</DangerButton>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
|
@ -8,10 +8,10 @@ const useOutsideClickDetector = (ref: React.RefObject<HTMLElement>, callback: ()
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("click", handleClick);
|
||||
document.addEventListener("mousedown", handleClick);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("click", handleClick);
|
||||
document.removeEventListener("mousedown", handleClick);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
@ -52,10 +52,10 @@
|
||||
"highlight.js": "^11.8.0",
|
||||
"js-cookie": "^3.0.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"mobx": "^6.10.0",
|
||||
"mobx-react-lite": "^4.0.3",
|
||||
"lowlight": "^2.9.0",
|
||||
"lucide-react": "^0.263.1",
|
||||
"mobx": "^6.10.0",
|
||||
"mobx-react-lite": "^4.0.3",
|
||||
"next": "12.3.2",
|
||||
"next-pwa": "^5.6.0",
|
||||
"next-themes": "^0.2.1",
|
||||
@ -68,6 +68,7 @@
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "^7.38.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-moveable": "^0.54.1",
|
||||
"sharp": "^0.32.1",
|
||||
"sonner": "^0.6.2",
|
||||
"swr": "^2.1.3",
|
||||
|
@ -106,6 +106,7 @@ const ProfileActivity = () => {
|
||||
</div>
|
||||
<div className="issue-comments-section p-0">
|
||||
<Tiptap
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
value={
|
||||
activityItem?.new_value !== ""
|
||||
? activityItem.new_value
|
||||
|
@ -126,3 +126,27 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
|
||||
transition: opacity 0.2s ease-out;
|
||||
}
|
||||
|
||||
.img-placeholder {
|
||||
position: relative;
|
||||
width: 35%;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 45%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid rgba(var(--color-text-200));
|
||||
border-top-color: rgba(var(--color-text-800));
|
||||
animation: spinning 0.6s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spinning {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
70
apps/space/Dockerfile.space
Normal file
70
apps/space/Dockerfile.space
Normal 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
|
@ -25,7 +25,8 @@ const WorkspaceProjectPage = observer(() => {
|
||||
const routerSearchparams = useSearchParams();
|
||||
|
||||
const { workspace_slug, project_slug } = routerParams as { workspace_slug: string; project_slug: string };
|
||||
const board = routerSearchparams.get("board") as TIssueBoardKeys | "";
|
||||
const board =
|
||||
routerSearchparams && routerSearchparams.get("board") != null && (routerSearchparams.get("board") as TIssueBoardKeys | "");
|
||||
|
||||
// updating default board view when we are in the issues page
|
||||
useEffect(() => {
|
||||
|
@ -1,10 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
// next imports
|
||||
import { useSearchParams } from "next/navigation";
|
||||
// interface
|
||||
import { TIssueBoardKeys } from "store/types";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
@ -12,11 +8,6 @@ import { RootStore } from "store/root";
|
||||
const MobxStoreInit = () => {
|
||||
const store: RootStore = useMobxStore();
|
||||
|
||||
// search params
|
||||
const routerSearchparams = useSearchParams();
|
||||
|
||||
const board = routerSearchparams.get("board") as TIssueBoardKeys;
|
||||
|
||||
useEffect(() => {
|
||||
// theme
|
||||
const _theme = localStorage && localStorage.getItem("app_theme") ? localStorage.getItem("app_theme") : "light";
|
||||
|
@ -1,9 +1,14 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const path = require('path')
|
||||
|
||||
const nextConfig = {
|
||||
reactStrictMode: false,
|
||||
swcMinify: true,
|
||||
experimental: {
|
||||
outputFileTracingRoot: path.join(__dirname, "../../"),
|
||||
appDir: true,
|
||||
},
|
||||
output: 'standalone'
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "plane-deploy",
|
||||
"name": "space",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev": "next dev -p 4000",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"start": "next start -p 4000",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
|
10
apps/space/pages/_app.tsx
Normal file
10
apps/space/pages/_app.tsx
Normal 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;
|
17
apps/space/pages/_document.tsx
Normal file
17
apps/space/pages/_document.tsx
Normal 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;
|
@ -3,7 +3,7 @@ import axios from "axios";
|
||||
// js cookie
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
const base_url: string | null = "https://boarding.plane.so";
|
||||
const base_url: string | null = "http://localhost:8000";
|
||||
|
||||
abstract class APIService {
|
||||
protected baseURL: string;
|
||||
|
@ -38,11 +38,12 @@ services:
|
||||
container_name: planefrontend
|
||||
image: makeplane/plane-frontend:latest
|
||||
restart: always
|
||||
command: /usr/local/bin/start.sh
|
||||
command: /usr/local/bin/start.sh apps/app/server.js app
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL}
|
||||
NEXT_PUBLIC_DEPLOY_URL: ${NEXT_PUBLIC_DEPLOY_URL}
|
||||
NEXT_PUBLIC_GOOGLE_CLIENTID: "0"
|
||||
NEXT_PUBLIC_GITHUB_APP_NAME: "0"
|
||||
NEXT_PUBLIC_GITHUB_ID: "0"
|
||||
@ -55,6 +56,20 @@ services:
|
||||
- plane-api
|
||||
- plane-worker
|
||||
|
||||
plane-deploy:
|
||||
container_name: planedeploy
|
||||
image: makeplane/plane-space:latest
|
||||
restart: always
|
||||
command: node apps/space/server.js space
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL}
|
||||
depends_on:
|
||||
- plane-api
|
||||
- plane-worker
|
||||
- plane-web
|
||||
|
||||
plane-api:
|
||||
container_name: planebackend
|
||||
image: makeplane/plane-backend:latest
|
||||
|
@ -41,12 +41,14 @@ services:
|
||||
dockerfile: ./apps/app/Dockerfile.web
|
||||
args:
|
||||
NEXT_PUBLIC_API_BASE_URL: http://localhost:8000
|
||||
NEXT_PUBLIC_DEPLOY_URL: http://localhost/spaces
|
||||
restart: always
|
||||
command: /usr/local/bin/start.sh
|
||||
command: /usr/local/bin/start.sh apps/app/server.js app
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL}
|
||||
NEXT_PUBLIC_DEPLOY_URL: ${NEXT_PUBLIC_DEPLOY_URL}
|
||||
NEXT_PUBLIC_GOOGLE_CLIENTID: "0"
|
||||
NEXT_PUBLIC_GITHUB_APP_NAME: "0"
|
||||
NEXT_PUBLIC_GITHUB_ID: "0"
|
||||
@ -58,6 +60,23 @@ services:
|
||||
depends_on:
|
||||
- plane-api
|
||||
- plane-worker
|
||||
plane-deploy:
|
||||
container_name: planedeploy
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./apps/space/Dockerfile.space space
|
||||
args:
|
||||
NEXT_PUBLIC_API_BASE_URL: http://localhost:8000
|
||||
restart: always
|
||||
command: /usr/local/bin/start.sh apps/space/server.js space
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL}
|
||||
depends_on:
|
||||
- plane-api
|
||||
- plane-worker
|
||||
- plane-web
|
||||
|
||||
plane-api:
|
||||
container_name: planebackend
|
||||
|
@ -18,6 +18,11 @@ server {
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
location /space/ {
|
||||
proxy_pass http://localhost:4000/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
|
@ -15,6 +15,10 @@ server {
|
||||
proxy_pass http://planefrontend:3000/;
|
||||
}
|
||||
|
||||
location /spaces/ {
|
||||
proxy_pass http://planedeploy:3000/;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://planebackend:8000/api/;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
#!/bin/sh
|
||||
FROM=$1
|
||||
TO=$2
|
||||
DIRECTORY=$3
|
||||
|
||||
if [ "${FROM}" = "${TO}" ]; then
|
||||
echo "Nothing to replace, the value is already set to ${TO}."
|
||||
@ -11,4 +12,4 @@ fi
|
||||
# Only perform action if $FROM and $TO are different.
|
||||
echo "Replacing all statically built instances of $FROM with this string $TO ."
|
||||
|
||||
grep -R -la "${FROM}" apps/app/.next | xargs -I{} sed -i "s|$FROM|$TO|g" "{}"
|
||||
grep -R -la "${FROM}" apps/$DIRECTORY/.next | xargs -I{} sed -i "s|$FROM|$TO|g" "{}"
|
||||
|
2
setup.sh
2
setup.sh
@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# cp ./.env.example ./.env
|
||||
cp ./.env.example ./.env
|
||||
|
||||
# Export for tr error in mac
|
||||
export LC_ALL=C
|
||||
|
4
start.sh
4
start.sh
@ -3,7 +3,7 @@ set -x
|
||||
|
||||
# Replace the statically built BUILT_NEXT_PUBLIC_API_BASE_URL with run-time NEXT_PUBLIC_API_BASE_URL
|
||||
# NOTE: if these values are the same, this will be skipped.
|
||||
/usr/local/bin/replace-env-vars.sh "$BUILT_NEXT_PUBLIC_API_BASE_URL" "$NEXT_PUBLIC_API_BASE_URL"
|
||||
/usr/local/bin/replace-env-vars.sh "$BUILT_NEXT_PUBLIC_API_BASE_URL" "$NEXT_PUBLIC_API_BASE_URL" $2
|
||||
|
||||
echo "Starting Plane Frontend.."
|
||||
node apps/app/server.js
|
||||
node $1
|
||||
|
165
yarn.lock
165
yarn.lock
@ -1006,6 +1006,40 @@
|
||||
react-popper "^2.3.0"
|
||||
tslib "~2.5.0"
|
||||
|
||||
"@cfcs/core@^0.0.6":
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@cfcs/core/-/core-0.0.6.tgz#9f8499dcd2ad29fd96d8fa72055411cd4a249121"
|
||||
integrity sha512-FxfJMwoLB8MEMConeXUCqtMGqxdtePQxRBOiGip9ULcYYam3WfCgoY6xdnMaSkYvRvmosp5iuG+TiPofm65+Pw==
|
||||
dependencies:
|
||||
"@egjs/component" "^3.0.2"
|
||||
|
||||
"@daybrush/utils@^1.1.1", "@daybrush/utils@^1.13.0", "@daybrush/utils@^1.4.0", "@daybrush/utils@^1.6.0", "@daybrush/utils@^1.7.1":
|
||||
version "1.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@daybrush/utils/-/utils-1.13.0.tgz#ea70a60864130da476406fdd1d465e3068aea0ff"
|
||||
integrity sha512-ALK12C6SQNNHw1enXK+UO8bdyQ+jaWNQ1Af7Z3FNxeAwjYhQT7do+TRE4RASAJ3ObaS2+TJ7TXR3oz2Gzbw0PQ==
|
||||
|
||||
"@egjs/agent@^2.2.1":
|
||||
version "2.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@egjs/agent/-/agent-2.4.3.tgz#6d44e2fb1ff7bab242c07f82732fe60305ac6f06"
|
||||
integrity sha512-XvksSENe8wPeFlEVouvrOhKdx8HMniJ3by7sro2uPF3M6QqWwjzVcmvwoPtdjiX8O1lfRoLhQMp1a7NGlVTdIA==
|
||||
|
||||
"@egjs/children-differ@^1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@egjs/children-differ/-/children-differ-1.0.1.tgz#5465fa80671d5ca3564ebe912f48b05b3e8a14fd"
|
||||
integrity sha512-DRvyqMf+CPCOzAopQKHtW+X8iN6Hy6SFol+/7zCUiE5y4P/OB8JP8FtU4NxtZwtafvSL4faD5KoQYPj3JHzPFQ==
|
||||
dependencies:
|
||||
"@egjs/list-differ" "^1.0.0"
|
||||
|
||||
"@egjs/component@^3.0.2":
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@egjs/component/-/component-3.0.4.tgz#ad7b53794b2a612806179a188ad828acb9525f61"
|
||||
integrity sha512-sXA7bGbIeLF2OAw/vpka66c6QBBUPcA4UUhR4WGJfnp2XWdiI8QrnJGJMr/UxpE/xnevX9tN3jvNPlW8WkHl3g==
|
||||
|
||||
"@egjs/list-differ@^1.0.0":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@egjs/list-differ/-/list-differ-1.0.1.tgz#5772b0f8b87973bb67827f6c7d7df8d7f64a22eb"
|
||||
integrity sha512-OTFTDQcWS+1ZREOdCWuk5hCBgYO4OsD30lXcOCyVOAjXMhgL5rBRDnt/otb6Nz8CzU0L/igdcaQBDLWc4t9gvg==
|
||||
|
||||
"@emotion/babel-plugin@^11.11.0":
|
||||
version "11.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c"
|
||||
@ -1990,6 +2024,28 @@
|
||||
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.3.3.tgz#16ab6c727d8c2020a5b6e4a176a243ecd88d8d69"
|
||||
integrity sha512-0xd7qez0AQ+MbHatZTlI1gu5vkG8r7MYRUJAHPAHJBmGLs16zpkrpAVLvjQKQOqaXPDUBwOiJzNc00znHSCVBw==
|
||||
|
||||
"@scena/dragscroll@^1.4.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@scena/dragscroll/-/dragscroll-1.4.0.tgz#220b2430c16119cd3e70044ee533a5b9a43cffd7"
|
||||
integrity sha512-3O8daaZD9VXA9CP3dra6xcgt/qrm0mg0xJCwiX6druCteQ9FFsXffkF8PrqxY4Z4VJ58fFKEa0RlKqbsi/XnRA==
|
||||
dependencies:
|
||||
"@daybrush/utils" "^1.6.0"
|
||||
"@scena/event-emitter" "^1.0.2"
|
||||
|
||||
"@scena/event-emitter@^1.0.2", "@scena/event-emitter@^1.0.5":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@scena/event-emitter/-/event-emitter-1.0.5.tgz#047e3acef93cf238d7ce3a8cc5a12ec6bd9c3bb1"
|
||||
integrity sha512-AzY4OTb0+7ynefmWFQ6hxDdk0CySAq/D4efljfhtRHCOP7MBF9zUfhKG3TJiroVjASqVgkRJFdenS8ArZo6Olg==
|
||||
dependencies:
|
||||
"@daybrush/utils" "^1.1.1"
|
||||
|
||||
"@scena/matrix@^1.0.0", "@scena/matrix@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@scena/matrix/-/matrix-1.1.1.tgz#5297f71825c72e2c2c8f802f924f482ed200c43c"
|
||||
integrity sha512-JVKBhN0tm2Srl+Yt+Ywqu0oLgLcdemDQlD1OxmN9jaCTwaFPZ7tY8n6dhVgMEaR9qcR7r+kAlMXnSfNyYdE+Vg==
|
||||
dependencies:
|
||||
"@daybrush/utils" "^1.4.0"
|
||||
|
||||
"@sentry-internal/tracing@7.63.0":
|
||||
version "7.63.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.63.0.tgz#58903b2205456034611cc5bc1b5b2479275f89c7"
|
||||
@ -3462,6 +3518,21 @@ css-box-model@^1.2.0:
|
||||
dependencies:
|
||||
tiny-invariant "^1.0.6"
|
||||
|
||||
css-styled@^1.0.8, css-styled@~1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/css-styled/-/css-styled-1.0.8.tgz#c9c05dc4abdef5571033090bfb8cfc5e19429974"
|
||||
integrity sha512-tCpP7kLRI8dI95rCh3Syl7I+v7PP+2JYOzWkl0bUEoSbJM+u8ITbutjlQVf0NC2/g4ULROJPi16sfwDIO8/84g==
|
||||
dependencies:
|
||||
"@daybrush/utils" "^1.13.0"
|
||||
|
||||
css-to-mat@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/css-to-mat/-/css-to-mat-1.1.1.tgz#0dd10dcf9ec17df15708c8ff07a74fbd0b9a3fe5"
|
||||
integrity sha512-kvpxFYZb27jRd2vium35G7q5XZ2WJ9rWjDUMNT36M3Hc41qCrLXFM5iEKMGXcrPsKfXEN+8l/riB4QzwwwiEyQ==
|
||||
dependencies:
|
||||
"@daybrush/utils" "^1.13.0"
|
||||
"@scena/matrix" "^1.0.0"
|
||||
|
||||
cssesc@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
|
||||
@ -4484,6 +4555,11 @@ fraction.js@^4.2.0:
|
||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
|
||||
integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==
|
||||
|
||||
framework-utils@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/framework-utils/-/framework-utils-1.1.0.tgz#a3b528bce838dfd623148847dc92371b09d0da2d"
|
||||
integrity sha512-KAfqli5PwpFJ8o3psRNs8svpMGyCSAe8nmGcjQ0zZBWN2H6dZDnq+ABp3N3hdUmFeMrLtjOCTXD4yplUJIWceg==
|
||||
|
||||
fs-constants@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
|
||||
@ -4539,6 +4615,14 @@ gensync@^1.0.0-beta.2:
|
||||
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
||||
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
|
||||
|
||||
gesto@^1.19.0, gesto@^1.19.1:
|
||||
version "1.19.1"
|
||||
resolved "https://registry.yarnpkg.com/gesto/-/gesto-1.19.1.tgz#b2a29730663eecf77b248982bbff929e79d4a461"
|
||||
integrity sha512-ofWVEdqmnpFm3AFf7aoclhoayseb3OkwSiXbXusKYu/99iN5HgeWP+SWqdghQ5TFlOgP5Zlz+6SY8mP2V0kFaQ==
|
||||
dependencies:
|
||||
"@daybrush/utils" "^1.13.0"
|
||||
"@scena/event-emitter" "^1.0.2"
|
||||
|
||||
get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82"
|
||||
@ -5244,6 +5328,21 @@ jsonpointer@^5.0.0:
|
||||
object.assign "^4.1.4"
|
||||
object.values "^1.1.6"
|
||||
|
||||
keycode@^2.2.0:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.1.tgz#09c23b2be0611d26117ea2501c2c391a01f39eff"
|
||||
integrity sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==
|
||||
|
||||
keycon@^1.2.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/keycon/-/keycon-1.4.0.tgz#bf2a633f3c3b659ea564045938cff33e584cebd5"
|
||||
integrity sha512-p1NAIxiRMH3jYfTeXRs2uWbVJ1WpEjpi8ktzUyBJsX7/wn2qu2VRXktneBLNtKNxJmlUYxRi9gOJt1DuthXR7A==
|
||||
dependencies:
|
||||
"@cfcs/core" "^0.0.6"
|
||||
"@daybrush/utils" "^1.7.1"
|
||||
"@scena/event-emitter" "^1.0.2"
|
||||
keycode "^2.2.0"
|
||||
|
||||
kleur@^4.0.3:
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"
|
||||
@ -6081,6 +6180,13 @@ orderedmap@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.1.1.tgz#61481269c44031c449915497bf5a4ad273c512d2"
|
||||
integrity sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==
|
||||
|
||||
overlap-area@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/overlap-area/-/overlap-area-1.1.0.tgz#1fcaa21bdb9cb1ace973d9aa299ae6b56557a4c2"
|
||||
integrity sha512-3dlJgJCaVeXH0/eZjYVJvQiLVVrPO4U1ZGqlATtx6QGO3b5eNM6+JgUKa7oStBTdYuGTk7gVoABCW6Tp+dhRdw==
|
||||
dependencies:
|
||||
"@daybrush/utils" "^1.7.1"
|
||||
|
||||
p-limit@^2.2.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
|
||||
@ -6452,20 +6558,13 @@ prosemirror-menu@^1.2.1:
|
||||
prosemirror-history "^1.0.0"
|
||||
prosemirror-state "^1.0.0"
|
||||
|
||||
prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1, prosemirror-model@^1.8.1:
|
||||
prosemirror-model@1.18.1, prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1, prosemirror-model@^1.19.0, prosemirror-model@^1.8.1:
|
||||
version "1.18.1"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.18.1.tgz#1d5d6b6de7b983ee67a479dc607165fdef3935bd"
|
||||
integrity sha512-IxSVBKAEMjD7s3n8cgtwMlxAXZrC7Mlag7zYsAKDndAqnDScvSmp/UdnRTV/B33lTCVU3CCm7dyAn/rVVD0mcw==
|
||||
dependencies:
|
||||
orderedmap "^2.0.0"
|
||||
|
||||
prosemirror-model@^1.19.0:
|
||||
version "1.19.3"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.19.3.tgz#f0d55285487fefd962d0ac695f716f4ec6705006"
|
||||
integrity sha512-tgSnwN7BS7/UM0sSARcW+IQryx2vODKX4MI7xpqY2X+iaepJdKBPc7I4aACIsDV/LTaTjt12Z56MhDr9LsyuZQ==
|
||||
dependencies:
|
||||
orderedmap "^2.0.0"
|
||||
|
||||
prosemirror-schema-basic@^1.2.0:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.2.tgz#6695f5175e4628aab179bf62e5568628b9cfe6c7"
|
||||
@ -6603,6 +6702,14 @@ react-color@^2.19.3:
|
||||
reactcss "^1.2.0"
|
||||
tinycolor2 "^1.4.1"
|
||||
|
||||
react-css-styled@^1.1.9:
|
||||
version "1.1.9"
|
||||
resolved "https://registry.yarnpkg.com/react-css-styled/-/react-css-styled-1.1.9.tgz#a7cc948e49f72b2f7fb1393bd85416a8293afab3"
|
||||
integrity sha512-M7fJZ3IWFaIHcZEkoFOnkjdiUFmwd8d+gTh2bpqMOcnxy/0Gsykw4dsL4QBiKsxcGow6tETUa4NAUcmJF+/nfw==
|
||||
dependencies:
|
||||
css-styled "~1.0.8"
|
||||
framework-utils "^1.1.0"
|
||||
|
||||
react-datepicker@^4.8.0:
|
||||
version "4.16.0"
|
||||
resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-4.16.0.tgz#b9dd389bb5611a1acc514bba1dd944be21dd877f"
|
||||
@ -6683,6 +6790,25 @@ react-markdown@^8.0.7:
|
||||
unist-util-visit "^4.0.0"
|
||||
vfile "^5.0.0"
|
||||
|
||||
react-moveable@^0.54.1:
|
||||
version "0.54.1"
|
||||
resolved "https://registry.yarnpkg.com/react-moveable/-/react-moveable-0.54.1.tgz#3c69748c444184700e6999501b0da953c934205e"
|
||||
integrity sha512-Kj2ifw9nk3LZvu7ezhst8Z5WBPRr+yVv9oROwrBirFlHmwGHHZXUGk5Gaezu+JGqqNRsQJncVMW5Uf68KSSOvg==
|
||||
dependencies:
|
||||
"@daybrush/utils" "^1.13.0"
|
||||
"@egjs/agent" "^2.2.1"
|
||||
"@egjs/children-differ" "^1.0.1"
|
||||
"@egjs/list-differ" "^1.0.0"
|
||||
"@scena/dragscroll" "^1.4.0"
|
||||
"@scena/event-emitter" "^1.0.5"
|
||||
"@scena/matrix" "^1.1.1"
|
||||
css-to-mat "^1.1.1"
|
||||
framework-utils "^1.1.0"
|
||||
gesto "^1.19.0"
|
||||
overlap-area "^1.1.0"
|
||||
react-css-styled "^1.1.9"
|
||||
react-selecto "^1.25.0"
|
||||
|
||||
react-onclickoutside@^6.12.2:
|
||||
version "6.13.0"
|
||||
resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz#e165ea4e5157f3da94f4376a3ab3e22a565f4ffc"
|
||||
@ -6740,6 +6866,13 @@ react-remove-scroll@2.5.4:
|
||||
use-callback-ref "^1.3.0"
|
||||
use-sidecar "^1.1.2"
|
||||
|
||||
react-selecto@^1.25.0:
|
||||
version "1.26.0"
|
||||
resolved "https://registry.yarnpkg.com/react-selecto/-/react-selecto-1.26.0.tgz#9157ff0a732fc426602b30c08ec21b6ca0a9c472"
|
||||
integrity sha512-aBTZEYA68uE+o8TytNjTb2GpIn4oKEv0U4LIow3cspJQlF/PdAnBwkq9UuiKVuFluu5kfLQ7Keu3S2Tihlmw0g==
|
||||
dependencies:
|
||||
selecto "~1.26.0"
|
||||
|
||||
react-style-singleton@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4"
|
||||
@ -7023,6 +7156,22 @@ schema-utils@^3.1.1:
|
||||
ajv "^6.12.5"
|
||||
ajv-keywords "^3.5.2"
|
||||
|
||||
selecto@~1.26.0:
|
||||
version "1.26.0"
|
||||
resolved "https://registry.yarnpkg.com/selecto/-/selecto-1.26.0.tgz#f3f04fb6409112b198243458f6c9963946d5ba2f"
|
||||
integrity sha512-cEFKdv5rmkF6pf2OScQJllaNp4UJy/FvviB40ZaMSHrQCxC72X/Q6uhzW1tlb2RE+0danvUNJTs64cI9VXtUyg==
|
||||
dependencies:
|
||||
"@daybrush/utils" "^1.13.0"
|
||||
"@egjs/children-differ" "^1.0.1"
|
||||
"@scena/dragscroll" "^1.4.0"
|
||||
"@scena/event-emitter" "^1.0.5"
|
||||
css-styled "^1.0.8"
|
||||
css-to-mat "^1.1.1"
|
||||
framework-utils "^1.1.0"
|
||||
gesto "^1.19.1"
|
||||
keycon "^1.2.0"
|
||||
overlap-area "^1.1.0"
|
||||
|
||||
semver@^6.0.0, semver@^6.3.0, semver@^6.3.1:
|
||||
version "6.3.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||
|
Loading…
Reference in New Issue
Block a user