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
|
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
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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]">
|
||||||
|
@ -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">
|
||||||
|
@ -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,
|
||||||
|
@ -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}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
@ -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={
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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..."
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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."
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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 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,
|
||||||
|
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 { 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>
|
||||||
);
|
);
|
||||||
|
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;
|
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();
|
||||||
|
@ -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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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">
|
||||||
|
@ -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 ${
|
||||||
|
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"> {
|
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>;
|
||||||
|
@ -45,6 +45,7 @@ const restrictedUrls = [
|
|||||||
"profile",
|
"profile",
|
||||||
"reset-password",
|
"reset-password",
|
||||||
"sign-up",
|
"sign-up",
|
||||||
|
"spaces",
|
||||||
"workspace-member-invitation",
|
"workspace-member-invitation",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 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(() => {
|
||||||
|
@ -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";
|
||||||
|
@ -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;
|
||||||
|
@ -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
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
|
// 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;
|
||||||
|
@ -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"
|
||||||
@ -55,6 +56,20 @@ services:
|
|||||||
- 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
|
||||||
image: makeplane/plane-backend:latest
|
image: makeplane/plane-backend:latest
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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/;
|
||||||
}
|
}
|
||||||
|
@ -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" "{}"
|
||||||
|
2
setup.sh
2
setup.sh
@ -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
|
||||||
|
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
|
# 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
165
yarn.lock
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user