[WEB-3088] fix: home edits (#6357)

* fix: added delete sticky confirmation modal

* fix: prevented quick links reordering

* fix: quick links css

* fix: minor css

* fix: empty states

* Filter quick_tutorial and new_at_plane

* fix: stickies search backend change

* fix: stickies editor enhanced

* fix: sticky delete function

---------

Co-authored-by: gakshita <akshitagoyal1516@gmail.com>
This commit is contained in:
Sangeetha 2025-01-09 14:51:04 +05:30 committed by GitHub
parent 5d8f66ae22
commit d96ab2e7af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 303 additions and 173 deletions

View file

@ -31,7 +31,11 @@ class WorkspacePreferenceViewSet(BaseAPIView):
create_preference_keys = [] create_preference_keys = []
keys = [key for key, _ in WorkspaceHomePreference.HomeWidgetKeys.choices] keys = [
key
for key, _ in WorkspaceHomePreference.HomeWidgetKeys.choices
if key not in ["quick_tutorial", "new_at_plane"]
]
sort_order_counter = 1 sort_order_counter = 1

View file

@ -39,9 +39,14 @@ class WorkspaceStickyViewSet(BaseViewSet):
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE" allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
) )
def list(self, request, slug): def list(self, request, slug):
query = request.query_params.get("query", False)
stickies = self.get_queryset()
if query:
stickies = stickies.filter(name__icontains=query)
return self.paginate( return self.paginate(
request=request, request=request,
queryset=(self.get_queryset()), queryset=(stickies),
on_results=lambda stickies: StickySerializer(stickies, many=True).data, on_results=lambda stickies: StickySerializer(stickies, many=True).data,
default_per_page=20, default_per_page=20,
) )

View file

@ -0,0 +1,111 @@
import { ReactNode, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react";
import { cn } from "@plane/utils";
interface IContentOverflowWrapper {
children: ReactNode;
maxHeight?: number;
gradientColor?: string;
buttonClassName?: string;
containerClassName?: string;
fallback?: ReactNode;
}
export const ContentOverflowWrapper = observer((props: IContentOverflowWrapper) => {
const {
children,
maxHeight = 625,
buttonClassName = "text-sm font-medium text-custom-primary-100",
containerClassName,
fallback = null,
} = props;
// states
const [containerHeight, setContainerHeight] = useState(0);
const [showAll, setShowAll] = useState(false);
// refs
const contentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!contentRef?.current) return;
const updateHeight = () => {
if (contentRef.current) {
const height = contentRef.current.getBoundingClientRect().height;
setContainerHeight(height);
}
};
// Initial height measurement
updateHeight();
// Create ResizeObserver for size changes
const resizeObserver = new ResizeObserver(updateHeight);
resizeObserver.observe(contentRef.current);
// Create MutationObserver for content changes
const mutationObserver = new MutationObserver((mutations) => {
const shouldUpdate = mutations.some(
(mutation) =>
mutation.type === "childList" ||
(mutation.type === "attributes" && (mutation.attributeName === "style" || mutation.attributeName === "class"))
);
if (shouldUpdate) {
updateHeight();
}
});
mutationObserver.observe(contentRef.current, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["style", "class"],
});
return () => {
resizeObserver.disconnect();
mutationObserver.disconnect();
};
}, [contentRef?.current]);
if (!children) return fallback;
return (
<div
className={cn(
"relative",
{
[`overflow-hidden`]: !showAll,
"overflow-visible": showAll,
},
containerClassName
)}
style={{ maxHeight: showAll ? "100%" : `${maxHeight}px` }}
>
<div ref={contentRef}>{children}</div>
{containerHeight > maxHeight && (
<div
className={cn(
"bottom-0 left-0 w-full",
`bg-gradient-to-t from-custom-background-100 to-transparent flex flex-col items-center justify-end`,
"text-center",
{
"absolute h-[100px]": !showAll,
"h-[30px]": showAll,
}
)}
>
<button
className={cn("gap-1 w-full text-custom-primary-100 text-sm font-medium", buttonClassName)}
onClick={() => setShowAll((prev) => !prev)}
>
{showAll ? "Show less" : "Show all"}
</button>
</div>
)}
</div>
);
});

View file

@ -29,7 +29,7 @@ interface StickyEditorWrapperProps
uploadFile: (file: File) => Promise<string>; uploadFile: (file: File) => Promise<string>;
parentClassName?: string; parentClassName?: string;
handleColorChange: (data: Partial<TSticky>) => Promise<void>; handleColorChange: (data: Partial<TSticky>) => Promise<void>;
handleDelete: () => Promise<void>; handleDelete: () => void;
} }
export const StickyEditor = React.forwardRef<EditorRefApi, StickyEditorWrapperProps>((props, ref) => { export const StickyEditor = React.forwardRef<EditorRefApi, StickyEditorWrapperProps>((props, ref) => {

View file

@ -1,21 +0,0 @@
import Image from "next/image";
import { useTheme } from "next-themes";
import UpcomingIssuesDark from "@/public/empty-state/dashboard/dark/upcoming-issues.svg";
import UpcomingIssuesLight from "@/public/empty-state/dashboard/light/upcoming-issues.svg";
export const IssuesEmptyState = () => {
// next-themes
const { resolvedTheme } = useTheme();
const image = resolvedTheme === "dark" ? UpcomingIssuesDark : UpcomingIssuesLight;
// TODO: update empty state logic to use a general component
return (
<div className="text-center space-y-6 flex flex-col items-center justify-center">
<div className="h-24 w-24">
<Image src={image} className="w-full h-full" alt="Assigned issues" />
</div>
<p className="text-sm font-medium text-custom-text-300 whitespace-pre-line">No activity to display</p>
</div>
);
};

View file

@ -0,0 +1,27 @@
import { Link2, Plus } from "lucide-react";
import { Button } from "@plane/ui";
type TProps = {
handleCreate: () => void;
};
export const LinksEmptyState = (props: TProps) => {
const { handleCreate } = props;
return (
<div className="min-h-[200px] flex w-full justify-center py-6 border-[1.5px] border-custom-border-100 rounded">
<div className="m-auto">
<div
className={`mb-2 rounded-full mx-auto last:rounded-full w-[50px] h-[50px] flex items-center justify-center bg-custom-background-80/40 transition-transform duration-300`}
>
<Link2 size={30} className="text-custom-text-400 -rotate-45" />
</div>
<div className="text-custom-text-100 font-medium text-base text-center mb-1">No quick links yet</div>
<div className="text-custom-text-300 text-sm text-center mb-2">
Add any links you need for quick access to your work.{" "}
</div>
<Button variant="accent-primary" size="sm" onClick={handleCreate} className="mx-auto">
<Plus className="size-4 my-auto" /> <span>Add quick link</span>
</Button>
</div>
</div>
);
};

View file

@ -0,0 +1,15 @@
import { History } from "lucide-react";
export const RecentsEmptyState = () => (
<div className="h-[200px] flex w-full justify-center py-6 border-[1.5px] border-custom-border-100 rounded">
<div className="m-auto">
<div
className={`mb-2 rounded-full mx-auto last:rounded-full w-[50px] h-[50px] flex items-center justify-center bg-custom-background-80/40 transition-transform duration-300`}
>
<History size={30} className="text-custom-text-400 -rotate-45" />
</div>
<div className="text-custom-text-100 font-medium text-base text-center mb-1">No recent items yet</div>
<div className="text-custom-text-300 text-sm text-center mb-2">You dont have any recent items yet. </div>
</div>
</div>
);

View file

@ -1,9 +1,10 @@
import { FC, useEffect, useState } from "react"; import { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
// computed // computed
import { ContentOverflowWrapper } from "@/components/core/content-overflow-HOC";
import { useHome } from "@/hooks/store/use-home"; import { useHome } from "@/hooks/store/use-home";
import { LinksEmptyState } from "../empty-states/links";
import { EWidgetKeys, WidgetLoader } from "../loaders"; import { EWidgetKeys, WidgetLoader } from "../loaders";
import { AddLink } from "./action";
import { ProjectLinkDetail } from "./link-detail"; import { ProjectLinkDetail } from "./link-detail";
import { TLinkOperations } from "./use-links"; import { TLinkOperations } from "./use-links";
@ -17,9 +18,6 @@ export type TProjectLinkList = {
export const ProjectLinkList: FC<TProjectLinkList> = observer((props) => { export const ProjectLinkList: FC<TProjectLinkList> = observer((props) => {
// props // props
const { linkOperations, workspaceSlug } = props; const { linkOperations, workspaceSlug } = props;
// states
const [columnCount, setColumnCount] = useState(4);
const [showAll, setShowAll] = useState(false);
// hooks // hooks
const { const {
quickLinks: { getLinksByWorkspaceId, toggleLinkModal }, quickLinks: { getLinksByWorkspaceId, toggleLinkModal },
@ -27,51 +25,23 @@ export const ProjectLinkList: FC<TProjectLinkList> = observer((props) => {
const links = getLinksByWorkspaceId(workspaceSlug); const links = getLinksByWorkspaceId(workspaceSlug);
useEffect(() => {
const updateColumnCount = () => {
if (window.matchMedia("(min-width: 1024px)").matches) {
setColumnCount(4); // lg screens
} else if (window.matchMedia("(min-width: 768px)").matches) {
setColumnCount(3); // md screens
} else if (window.matchMedia("(min-width: 640px)").matches) {
setColumnCount(2); // sm screens
} else {
setColumnCount(1); // mobile
}
};
// Initial check
updateColumnCount();
// Add event listener for window resize
window.addEventListener("resize", updateColumnCount);
// Cleanup
return () => window.removeEventListener("resize", updateColumnCount);
}, []);
if (links === undefined) return <WidgetLoader widgetKey={EWidgetKeys.QUICK_LINKS} />; if (links === undefined) return <WidgetLoader widgetKey={EWidgetKeys.QUICK_LINKS} />;
if (links.length === 0) return <LinksEmptyState handleCreate={() => toggleLinkModal(true)} />;
return ( return (
<ContentOverflowWrapper
maxHeight={150}
containerClassName="pb-2 box-border"
fallback={<></>}
buttonClassName="bg-custom-background-90/20"
>
<div> <div>
<div className="flex gap-2 mb-2 flex-wrap justify-center "> <div className="flex gap-2 mb-2 flex-wrap">
{links && {links &&
links.length > 0 && links.length > 0 &&
(showAll ? links : links.slice(0, 2 * columnCount - 1)).map((linkId) => ( links.map((linkId) => <ProjectLinkDetail key={linkId} linkId={linkId} linkOperations={linkOperations} />)}
<ProjectLinkDetail key={linkId} linkId={linkId} linkOperations={linkOperations} />
))}
{/* Add new link */}
<AddLink onClick={() => toggleLinkModal(true)} />
</div> </div>
{links.length > 2 * columnCount - 1 && (
<button
className="flex items-center justify-center gap-1 rounded-md px-2 py-1 text-sm font-medium text-custom-primary-100 mx-auto"
onClick={() => setShowAll((state) => !state)}
>
{showAll ? "Show less" : "Show more"}
</button>
)}
</div> </div>
</ContentOverflowWrapper>
); );
}); });

View file

@ -1,5 +1,6 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import useSWR from "swr"; import useSWR from "swr";
import { Plus } from "lucide-react";
import { THomeWidgetProps } from "@plane/types"; import { THomeWidgetProps } from "@plane/types";
import { useHome } from "@/hooks/store/use-home"; import { useHome } from "@/hooks/store/use-home";
import { LinkCreateUpdateModal } from "./create-update-link-modal"; import { LinkCreateUpdateModal } from "./create-update-link-modal";
@ -31,10 +32,23 @@ export const DashboardQuickLinks = observer((props: THomeWidgetProps) => {
preloadedData={linkData} preloadedData={linkData}
setLinkData={setLinkData} setLinkData={setLinkData}
/> />
<div className="flex mx-auto flex-wrap pb-4 w-full justify-center"> <div className="mb-2">
<div className="flex items-center justify-between mb-4">
<div className="text-base font-semibold text-custom-text-350">Quick links</div>
<button
onClick={() => {
toggleLinkModal(true);
}}
className="flex gap-1 text-sm font-medium text-custom-primary-100 my-auto"
>
<Plus className="size-4 my-auto" /> <span>Add quick link</span>
</button>
</div>
<div className="flex flex-wrap w-full">
{/* rendering links */} {/* rendering links */}
<ProjectLinkList workspaceSlug={workspaceSlug} linkOperations={linkOperations} /> <ProjectLinkList workspaceSlug={workspaceSlug} linkOperations={linkOperations} />
</div> </div>
</div>
</> </>
); );
}); });

View file

@ -5,7 +5,7 @@ import range from "lodash/range";
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
export const QuickLinksWidgetLoader = () => ( export const QuickLinksWidgetLoader = () => (
<Loader className="bg-custom-background-100 rounded-xl gap-2 flex flex-wrap justify-center"> <Loader className="bg-custom-background-100 rounded-xl gap-2 flex flex-wrap">
{range(4).map((index) => ( {range(4).map((index) => (
<Loader.Item key={index} height="56px" width="230px" /> <Loader.Item key={index} height="56px" width="230px" />
))} ))}

View file

@ -6,7 +6,7 @@ import { Loader } from "@plane/ui";
export const RecentActivityWidgetLoader = () => ( export const RecentActivityWidgetLoader = () => (
<Loader className="bg-custom-background-100 rounded-xl px-2 space-y-6"> <Loader className="bg-custom-background-100 rounded-xl px-2 space-y-6">
{range(7).map((index) => ( {range(5).map((index) => (
<div key={index} className="flex items-start gap-3.5"> <div key={index} className="flex items-start gap-3.5">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<Loader.Item height="32px" width="32px" /> <Loader.Item height="32px" width="32px" />

View file

@ -11,7 +11,7 @@ import { LayersIcon } from "@plane/ui";
import { useProject } from "@/hooks/store"; import { useProject } from "@/hooks/store";
import { WorkspaceService } from "@/plane-web/services"; import { WorkspaceService } from "@/plane-web/services";
import { EmptyWorkspace } from "../empty-states"; import { EmptyWorkspace } from "../empty-states";
import { IssuesEmptyState } from "../empty-states/issues"; import { RecentsEmptyState } from "../empty-states/recents";
import { EWidgetKeys, WidgetLoader } from "../loaders"; import { EWidgetKeys, WidgetLoader } from "../loaders";
import { FiltersDropdown } from "./filters"; import { FiltersDropdown } from "./filters";
import { RecentIssue } from "./issue"; import { RecentIssue } from "./issue";
@ -68,24 +68,24 @@ export const RecentActivityWidget: React.FC<THomeWidgetProps> = observer((props)
if (!isLoading && recents?.length === 0) if (!isLoading && recents?.length === 0)
return ( return (
<div ref={ref} className=" max-h-[500px] overflow-y-scroll"> <div ref={ref} className=" max-h-[500px] overflow-y-scroll">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-4">
<div className="text-base font-semibold text-custom-text-350">Recents</div> <div className="text-base font-semibold text-custom-text-350">Recents</div>
<FiltersDropdown filters={filters} activeFilter={filter} setActiveFilter={setFilter} /> <FiltersDropdown filters={filters} activeFilter={filter} setActiveFilter={setFilter} />
</div> </div>
<div className="min-h-[400px] flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<IssuesEmptyState /> <RecentsEmptyState />
</div> </div>
</div> </div>
); );
return ( return (
<div ref={ref} className=" max-h-[500px] min-h-[400px] overflow-y-scroll"> <div ref={ref} className=" max-h-[500px] min-h-[250px] overflow-y-scroll">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<div className="text-base font-semibold text-custom-text-350">Recents</div> <div className="text-base font-semibold text-custom-text-350">Recents</div>
<FiltersDropdown filters={filters} activeFilter={filter} setActiveFilter={setFilter} /> <FiltersDropdown filters={filters} activeFilter={filter} setActiveFilter={setFilter} />
</div> </div>
<div className="min-h-[400px] flex flex-col"> <div className="min-h-[250px] flex flex-col">
{isLoading && <WidgetLoader widgetKey={WIDGET_KEY} />} {isLoading && <WidgetLoader widgetKey={WIDGET_KEY} />}
{!isLoading && {!isLoading &&
recents?.length > 0 && recents?.length > 0 &&

View file

@ -0,0 +1,44 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react";
// ui
import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui";
interface IStickyDelete {
isOpen: boolean;
handleSubmit: () => void;
handleClose: () => void;
}
export const StickyDeleteModal: React.FC<IStickyDelete> = observer((props) => {
const { isOpen, handleClose, handleSubmit } = props;
// states
const [loader, setLoader] = useState(false);
const formSubmit = async () => {
try {
setLoader(true);
await handleSubmit();
} catch (error) {
setToast({
type: TOAST_TYPE.ERROR,
title: "Warning!",
message: "Something went wrong please try again later.",
});
} finally {
setLoader(false);
}
};
return (
<AlertModalCore
handleClose={handleClose}
handleSubmit={formSubmit}
isSubmitting={loader}
isOpen={isOpen}
title="Delete sticky"
content={<>Are you sure you want to delete the sticky? </>}
/>
);
});

View file

@ -1,4 +1,5 @@
import { Plus, StickyNote as StickyIcon, X } from "lucide-react"; import { Plus, StickyNote as StickyIcon } from "lucide-react";
import { Button } from "@plane/ui";
type TProps = { type TProps = {
handleCreate: () => void; handleCreate: () => void;
@ -7,22 +8,18 @@ type TProps = {
export const EmptyState = (props: TProps) => { export const EmptyState = (props: TProps) => {
const { handleCreate, creatingSticky } = props; const { handleCreate, creatingSticky } = props;
return ( return (
<div className="flex justify-center h-[500px]"> <div className="flex justify-center h-[500px] rounded border-[1.5px] border-custom-border-100 mx-2">
<div className="m-auto"> <div className="m-auto">
<div <div
className={`mb-4 rounded-full mx-auto last:rounded-full w-[98px] h-[98px] flex items-center justify-center bg-custom-background-80/40 transition-transform duration-300`} className={`mb-2 rounded-full mx-auto last:rounded-full w-[50px] h-[50px] flex items-center justify-center bg-custom-background-80/40 transition-transform duration-300`}
> >
<StickyIcon className="size-[60px] rotate-90 text-custom-text-350/20" /> <StickyIcon className="size-[30px] rotate-90 text-custom-text-350/20" />
</div> </div>
<div className="text-custom-text-100 font-medium text-lg text-center">No stickies yet</div> <div className="text-custom-text-100 font-medium text-lg text-center mb-1">No stickies yet</div>
<div className="text-custom-text-300 text-sm text-center my-2"> <div className="text-custom-text-300 text-sm text-center mb-2">
All your stickies in this workspace will appear here. All your stickies in this workspace will appear here.
</div> </div>
<button <Button size="sm" variant="accent-primary" className="mx-auto" onClick={handleCreate} disabled={creatingSticky}>
onClick={handleCreate}
className="mx-auto flex gap-1 text-sm font-medium text-custom-primary-100 my-auto"
disabled={creatingSticky}
>
<Plus className="size-4 my-auto" /> <span>Add sticky</span> <Plus className="size-4 my-auto" /> <span>Add sticky</span>
{creatingSticky && ( {creatingSticky && (
<div className="flex items-center justify-center ml-2"> <div className="flex items-center justify-center ml-2">
@ -33,7 +30,7 @@ export const EmptyState = (props: TProps) => {
/> />
</div> </div>
)} )}
</button> </Button>
</div> </div>
</div> </div>
); );

View file

@ -7,6 +7,7 @@ import { Loader } from "@plane/ui";
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; import { useIntersectionObserver } from "@/hooks/use-intersection-observer";
import { useSticky } from "@/hooks/use-stickies"; import { useSticky } from "@/hooks/use-stickies";
import { ContentOverflowWrapper } from "../core/content-overflow-HOC";
import { STICKY_COLORS } from "../editor/sticky-editor/color-pallete"; import { STICKY_COLORS } from "../editor/sticky-editor/color-pallete";
import { EmptyState } from "./empty"; import { EmptyState } from "./empty";
import { StickyNote } from "./sticky"; import { StickyNote } from "./sticky";
@ -24,8 +25,6 @@ export const StickyAll = observer((props: TProps) => {
const masonryRef = useRef<HTMLDivElement>(null); const masonryRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
// states // states
const [containerHeight, setContainerHeight] = useState(0);
const [showAllStickies, setShowAllStickies] = useState(false);
const [intersectionElement, setIntersectionElement] = useState<HTMLDivElement | null>(null); const [intersectionElement, setIntersectionElement] = useState<HTMLDivElement | null>(null);
// router // router
const { workspaceSlug } = useParams(); const { workspaceSlug } = useParams();
@ -59,44 +58,6 @@ export const StickyAll = observer((props: TProps) => {
} }
}, [fetchingWorkspaceStickies, workspaceStickies, toggleShowNewSticky]); }, [fetchingWorkspaceStickies, workspaceStickies, toggleShowNewSticky]);
// Update this useEffect to correctly track height
useEffect(() => {
if (!masonryRef?.current) return;
const updateHeight = () => {
if (masonryRef.current) {
const height = masonryRef.current.getBoundingClientRect().height;
setContainerHeight(parseInt(height.toString()));
}
};
// Initial height measurement
updateHeight();
// Create ResizeObserver
const resizeObserver = new ResizeObserver(() => {
updateHeight();
});
resizeObserver.observe(masonryRef.current);
// Also update height when Masonry content changes
const mutationObserver = new MutationObserver(() => {
updateHeight();
});
mutationObserver.observe(masonryRef.current, {
childList: true,
subtree: true,
attributes: true,
});
return () => {
resizeObserver.disconnect();
mutationObserver.disconnect();
};
}, [masonryRef?.current]);
useIntersectionObserver(containerRef, fetchingWorkspaceStickies ? null : intersectionElement, incrementPage, "20%"); useIntersectionObserver(containerRef, fetchingWorkspaceStickies ? null : intersectionElement, incrementPage, "20%");
if (fetchingWorkspaceStickies && workspaceStickies.length === 0) { if (fetchingWorkspaceStickies && workspaceStickies.length === 0) {
@ -145,26 +106,16 @@ export const StickyAll = observer((props: TProps) => {
); );
return ( return (
<div <div ref={containerRef}>
ref={containerRef} <ContentOverflowWrapper
className={cn("relative max-h-[625px] overflow-hidden pb-2 box-border", { maxHeight={650}
"max-h-full overflow-scroll": showAllStickies, containerClassName="pb-2 box-border"
})} fallback={<></>}
buttonClassName="bg-custom-background-90/20"
> >
<div className="h-full w-full" ref={masonryRef}>
{/* @ts-expect-error type mismatch here */} {/* @ts-expect-error type mismatch here */}
<Masonry elementType="div">{childElements}</Masonry> <Masonry elementType="div">{childElements}</Masonry>
</div> </ContentOverflowWrapper>
{containerHeight > 632.9 && (
<div className="absolute bottom-0 left-0 bg-gradient-to-t from-custom-background-100 to-transparent w-full h-[100px] text-center text-sm font-medium text-custom-primary-100">
<button
className="flex flex-col items-center justify-end gap-1 h-full m-auto w-full"
onClick={() => setShowAllStickies((state) => !state)}
>
{showAllStickies ? "Show less" : "Show all"}
</button>
</div>
)}
</div> </div>
); );
}); });

View file

@ -13,7 +13,7 @@ type TProps = {
handleUpdate: DebouncedFunc<(payload: Partial<TSticky>) => Promise<void>>; handleUpdate: DebouncedFunc<(payload: Partial<TSticky>) => Promise<void>>;
stickyId: string | undefined; stickyId: string | undefined;
handleChange: (data: Partial<TSticky>) => Promise<void>; handleChange: (data: Partial<TSticky>) => Promise<void>;
handleDelete: () => Promise<void>; handleDelete: () => void;
}; };
export const StickyInput = (props: TProps) => { export const StickyInput = (props: TProps) => {
const { stickyData, workspaceSlug, handleUpdate, stickyId, handleDelete, handleChange } = props; const { stickyData, workspaceSlug, handleUpdate, stickyId, handleDelete, handleChange } = props;

View file

@ -1,4 +1,4 @@
import { useCallback } from "react"; import { useCallback, useState } from "react";
import { debounce } from "lodash"; import { debounce } from "lodash";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Minimize2 } from "lucide-react"; import { Minimize2 } from "lucide-react";
@ -6,6 +6,7 @@ import { TSticky } from "@plane/types";
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
import { useSticky } from "@/hooks/use-stickies"; import { useSticky } from "@/hooks/use-stickies";
import { STICKY_COLORS } from "../../editor/sticky-editor/color-pallete"; import { STICKY_COLORS } from "../../editor/sticky-editor/color-pallete";
import { StickyDeleteModal } from "../delete-modal";
import { StickyInput } from "./inputs"; import { StickyInput } from "./inputs";
import { useStickyOperations } from "./use-operations"; import { useStickyOperations } from "./use-operations";
@ -17,6 +18,8 @@ type TProps = {
}; };
export const StickyNote = observer((props: TProps) => { export const StickyNote = observer((props: TProps) => {
const { onClose, workspaceSlug, className = "", stickyId } = props; const { onClose, workspaceSlug, className = "", stickyId } = props;
//state
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
// hooks // hooks
const { stickyOperations } = useStickyOperations({ workspaceSlug }); const { stickyOperations } = useStickyOperations({ workspaceSlug });
const { stickies } = useSticky(); const { stickies } = useSticky();
@ -49,6 +52,12 @@ export const StickyNote = observer((props: TProps) => {
}; };
return ( return (
<>
<StickyDeleteModal
isOpen={isDeleteModalOpen}
handleSubmit={handleDelete}
handleClose={() => setIsDeleteModalOpen(false)}
/>
<div <div
className={cn("w-full flex flex-col h-fit rounded p-4 group/sticky", className)} className={cn("w-full flex flex-col h-fit rounded p-4 group/sticky", className)}
style={{ backgroundColor: stickyData?.color || STICKY_COLORS[0] }} style={{ backgroundColor: stickyData?.color || STICKY_COLORS[0] }}
@ -64,9 +73,13 @@ export const StickyNote = observer((props: TProps) => {
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
handleUpdate={debouncedFormSave} handleUpdate={debouncedFormSave}
stickyId={stickyId} stickyId={stickyId}
handleDelete={handleDelete} handleDelete={() => {
if (!stickyId) return;
setIsDeleteModalOpen(true);
}}
handleChange={handleChange} handleChange={handleChange}
/> />
</div> </div>
</>
); );
}); });