mirror of
https://github.com/makeplane/plane
synced 2025-08-07 19:59:33 +00:00
[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:
@@ -31,7 +31,11 @@ class WorkspacePreferenceViewSet(BaseAPIView):
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -39,13 +39,18 @@ class WorkspaceStickyViewSet(BaseViewSet):
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
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(
|
||||
request=request,
|
||||
queryset=(self.get_queryset()),
|
||||
queryset=(stickies),
|
||||
on_results=lambda stickies: StickySerializer(stickies, many=True).data,
|
||||
default_per_page=20,
|
||||
)
|
||||
|
||||
|
||||
@allow_permission(allowed_roles=[], creator=True, model=Sticky, level="WORKSPACE")
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
return super().partial_update(request, *args, **kwargs)
|
||||
|
||||
111
web/core/components/core/content-overflow-HOC.tsx
Normal file
111
web/core/components/core/content-overflow-HOC.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
@@ -29,7 +29,7 @@ interface StickyEditorWrapperProps
|
||||
uploadFile: (file: File) => Promise<string>;
|
||||
parentClassName?: string;
|
||||
handleColorChange: (data: Partial<TSticky>) => Promise<void>;
|
||||
handleDelete: () => Promise<void>;
|
||||
handleDelete: () => void;
|
||||
}
|
||||
|
||||
export const StickyEditor = React.forwardRef<EditorRefApi, StickyEditorWrapperProps>((props, ref) => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
27
web/core/components/home/widgets/empty-states/links.tsx
Normal file
27
web/core/components/home/widgets/empty-states/links.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
15
web/core/components/home/widgets/empty-states/recents.tsx
Normal file
15
web/core/components/home/widgets/empty-states/recents.tsx
Normal 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 don’t have any recent items yet. </div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1,9 +1,10 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// computed
|
||||
import { ContentOverflowWrapper } from "@/components/core/content-overflow-HOC";
|
||||
import { useHome } from "@/hooks/store/use-home";
|
||||
import { LinksEmptyState } from "../empty-states/links";
|
||||
import { EWidgetKeys, WidgetLoader } from "../loaders";
|
||||
import { AddLink } from "./action";
|
||||
import { ProjectLinkDetail } from "./link-detail";
|
||||
import { TLinkOperations } from "./use-links";
|
||||
|
||||
@@ -17,9 +18,6 @@ export type TProjectLinkList = {
|
||||
export const ProjectLinkList: FC<TProjectLinkList> = observer((props) => {
|
||||
// props
|
||||
const { linkOperations, workspaceSlug } = props;
|
||||
// states
|
||||
const [columnCount, setColumnCount] = useState(4);
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
// hooks
|
||||
const {
|
||||
quickLinks: { getLinksByWorkspaceId, toggleLinkModal },
|
||||
@@ -27,51 +25,23 @@ export const ProjectLinkList: FC<TProjectLinkList> = observer((props) => {
|
||||
|
||||
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.length === 0) return <LinksEmptyState handleCreate={() => toggleLinkModal(true)} />;
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-2 mb-2 flex-wrap justify-center ">
|
||||
{links &&
|
||||
links.length > 0 &&
|
||||
(showAll ? links : links.slice(0, 2 * columnCount - 1)).map((linkId) => (
|
||||
<ProjectLinkDetail key={linkId} linkId={linkId} linkOperations={linkOperations} />
|
||||
))}
|
||||
|
||||
{/* Add new link */}
|
||||
<AddLink onClick={() => toggleLinkModal(true)} />
|
||||
<ContentOverflowWrapper
|
||||
maxHeight={150}
|
||||
containerClassName="pb-2 box-border"
|
||||
fallback={<></>}
|
||||
buttonClassName="bg-custom-background-90/20"
|
||||
>
|
||||
<div>
|
||||
<div className="flex gap-2 mb-2 flex-wrap">
|
||||
{links &&
|
||||
links.length > 0 &&
|
||||
links.map((linkId) => <ProjectLinkDetail key={linkId} linkId={linkId} linkOperations={linkOperations} />)}
|
||||
</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>
|
||||
</ContentOverflowWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
import { Plus } from "lucide-react";
|
||||
import { THomeWidgetProps } from "@plane/types";
|
||||
import { useHome } from "@/hooks/store/use-home";
|
||||
import { LinkCreateUpdateModal } from "./create-update-link-modal";
|
||||
@@ -31,9 +32,22 @@ export const DashboardQuickLinks = observer((props: THomeWidgetProps) => {
|
||||
preloadedData={linkData}
|
||||
setLinkData={setLinkData}
|
||||
/>
|
||||
<div className="flex mx-auto flex-wrap pb-4 w-full justify-center">
|
||||
{/* rendering links */}
|
||||
<ProjectLinkList workspaceSlug={workspaceSlug} linkOperations={linkOperations} />
|
||||
<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 */}
|
||||
<ProjectLinkList workspaceSlug={workspaceSlug} linkOperations={linkOperations} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import range from "lodash/range";
|
||||
import { Loader } from "@plane/ui";
|
||||
|
||||
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) => (
|
||||
<Loader.Item key={index} height="56px" width="230px" />
|
||||
))}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Loader } from "@plane/ui";
|
||||
|
||||
export const RecentActivityWidgetLoader = () => (
|
||||
<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 className="flex-shrink-0">
|
||||
<Loader.Item height="32px" width="32px" />
|
||||
|
||||
@@ -11,7 +11,7 @@ import { LayersIcon } from "@plane/ui";
|
||||
import { useProject } from "@/hooks/store";
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
import { EmptyWorkspace } from "../empty-states";
|
||||
import { IssuesEmptyState } from "../empty-states/issues";
|
||||
import { RecentsEmptyState } from "../empty-states/recents";
|
||||
import { EWidgetKeys, WidgetLoader } from "../loaders";
|
||||
import { FiltersDropdown } from "./filters";
|
||||
import { RecentIssue } from "./issue";
|
||||
@@ -68,24 +68,24 @@ export const RecentActivityWidget: React.FC<THomeWidgetProps> = observer((props)
|
||||
if (!isLoading && recents?.length === 0)
|
||||
return (
|
||||
<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>
|
||||
<FiltersDropdown filters={filters} activeFilter={filter} setActiveFilter={setFilter} />
|
||||
</div>
|
||||
<div className="min-h-[400px] flex flex-col items-center justify-center">
|
||||
<IssuesEmptyState />
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<RecentsEmptyState />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
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="text-base font-semibold text-custom-text-350">Recents</div>
|
||||
|
||||
<FiltersDropdown filters={filters} activeFilter={filter} setActiveFilter={setFilter} />
|
||||
</div>
|
||||
<div className="min-h-[400px] flex flex-col">
|
||||
<div className="min-h-[250px] flex flex-col">
|
||||
{isLoading && <WidgetLoader widgetKey={WIDGET_KEY} />}
|
||||
{!isLoading &&
|
||||
recents?.length > 0 &&
|
||||
|
||||
44
web/core/components/stickies/delete-modal.tsx
Normal file
44
web/core/components/stickies/delete-modal.tsx
Normal 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? </>}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -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 = {
|
||||
handleCreate: () => void;
|
||||
@@ -7,22 +8,18 @@ type TProps = {
|
||||
export const EmptyState = (props: TProps) => {
|
||||
const { handleCreate, creatingSticky } = props;
|
||||
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={`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 className="text-custom-text-100 font-medium text-lg text-center">No stickies yet</div>
|
||||
<div className="text-custom-text-300 text-sm text-center my-2">
|
||||
<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 mb-2">
|
||||
All your stickies in this workspace will appear here.
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
className="mx-auto flex gap-1 text-sm font-medium text-custom-primary-100 my-auto"
|
||||
disabled={creatingSticky}
|
||||
>
|
||||
<Button size="sm" variant="accent-primary" className="mx-auto" onClick={handleCreate} disabled={creatingSticky}>
|
||||
<Plus className="size-4 my-auto" /> <span>Add sticky</span>
|
||||
{creatingSticky && (
|
||||
<div className="flex items-center justify-center ml-2">
|
||||
@@ -33,7 +30,7 @@ export const EmptyState = (props: TProps) => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Loader } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
import { useIntersectionObserver } from "@/hooks/use-intersection-observer";
|
||||
import { useSticky } from "@/hooks/use-stickies";
|
||||
import { ContentOverflowWrapper } from "../core/content-overflow-HOC";
|
||||
import { STICKY_COLORS } from "../editor/sticky-editor/color-pallete";
|
||||
import { EmptyState } from "./empty";
|
||||
import { StickyNote } from "./sticky";
|
||||
@@ -24,8 +25,6 @@ export const StickyAll = observer((props: TProps) => {
|
||||
const masonryRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
// states
|
||||
const [containerHeight, setContainerHeight] = useState(0);
|
||||
const [showAllStickies, setShowAllStickies] = useState(false);
|
||||
const [intersectionElement, setIntersectionElement] = useState<HTMLDivElement | null>(null);
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
@@ -59,44 +58,6 @@ export const StickyAll = observer((props: TProps) => {
|
||||
}
|
||||
}, [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%");
|
||||
|
||||
if (fetchingWorkspaceStickies && workspaceStickies.length === 0) {
|
||||
@@ -145,26 +106,16 @@ export const StickyAll = observer((props: TProps) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn("relative max-h-[625px] overflow-hidden pb-2 box-border", {
|
||||
"max-h-full overflow-scroll": showAllStickies,
|
||||
})}
|
||||
>
|
||||
<div className="h-full w-full" ref={masonryRef}>
|
||||
<div ref={containerRef}>
|
||||
<ContentOverflowWrapper
|
||||
maxHeight={650}
|
||||
containerClassName="pb-2 box-border"
|
||||
fallback={<></>}
|
||||
buttonClassName="bg-custom-background-90/20"
|
||||
>
|
||||
{/* @ts-expect-error type mismatch here */}
|
||||
<Masonry elementType="div">{childElements}</Masonry>
|
||||
</div>
|
||||
{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>
|
||||
)}
|
||||
</ContentOverflowWrapper>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ type TProps = {
|
||||
handleUpdate: DebouncedFunc<(payload: Partial<TSticky>) => Promise<void>>;
|
||||
stickyId: string | undefined;
|
||||
handleChange: (data: Partial<TSticky>) => Promise<void>;
|
||||
handleDelete: () => Promise<void>;
|
||||
handleDelete: () => void;
|
||||
};
|
||||
export const StickyInput = (props: TProps) => {
|
||||
const { stickyData, workspaceSlug, handleUpdate, stickyId, handleDelete, handleChange } = props;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { debounce } from "lodash";
|
||||
import { observer } from "mobx-react";
|
||||
import { Minimize2 } from "lucide-react";
|
||||
@@ -6,6 +6,7 @@ import { TSticky } from "@plane/types";
|
||||
import { cn } from "@plane/utils";
|
||||
import { useSticky } from "@/hooks/use-stickies";
|
||||
import { STICKY_COLORS } from "../../editor/sticky-editor/color-pallete";
|
||||
import { StickyDeleteModal } from "../delete-modal";
|
||||
import { StickyInput } from "./inputs";
|
||||
import { useStickyOperations } from "./use-operations";
|
||||
|
||||
@@ -17,6 +18,8 @@ type TProps = {
|
||||
};
|
||||
export const StickyNote = observer((props: TProps) => {
|
||||
const { onClose, workspaceSlug, className = "", stickyId } = props;
|
||||
//state
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
// hooks
|
||||
const { stickyOperations } = useStickyOperations({ workspaceSlug });
|
||||
const { stickies } = useSticky();
|
||||
@@ -49,24 +52,34 @@ export const StickyNote = observer((props: TProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("w-full flex flex-col h-fit rounded p-4 group/sticky", className)}
|
||||
style={{ backgroundColor: stickyData?.color || STICKY_COLORS[0] }}
|
||||
>
|
||||
{onClose && (
|
||||
<button className="flex w-full" onClick={onClose}>
|
||||
<Minimize2 className="size-4 m-auto mr-0" />
|
||||
</button>
|
||||
)}
|
||||
{/* inputs */}
|
||||
<StickyInput
|
||||
stickyData={stickyData}
|
||||
workspaceSlug={workspaceSlug}
|
||||
handleUpdate={debouncedFormSave}
|
||||
stickyId={stickyId}
|
||||
handleDelete={handleDelete}
|
||||
handleChange={handleChange}
|
||||
<>
|
||||
<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)}
|
||||
style={{ backgroundColor: stickyData?.color || STICKY_COLORS[0] }}
|
||||
>
|
||||
{onClose && (
|
||||
<button className="flex w-full" onClick={onClose}>
|
||||
<Minimize2 className="size-4 m-auto mr-0" />
|
||||
</button>
|
||||
)}
|
||||
{/* inputs */}
|
||||
<StickyInput
|
||||
stickyData={stickyData}
|
||||
workspaceSlug={workspaceSlug}
|
||||
handleUpdate={debouncedFormSave}
|
||||
stickyId={stickyId}
|
||||
handleDelete={() => {
|
||||
if (!stickyId) return;
|
||||
setIsDeleteModalOpen(true);
|
||||
}}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user