diff --git a/apiserver/plane/app/views/workspace/preference.py b/apiserver/plane/app/views/workspace/preference.py index 4d6d9456224..e3da9184da2 100644 --- a/apiserver/plane/app/views/workspace/preference.py +++ b/apiserver/plane/app/views/workspace/preference.py @@ -31,7 +31,11 @@ def get(self, request, slug): 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 diff --git a/apiserver/plane/app/views/workspace/sticky.py b/apiserver/plane/app/views/workspace/sticky.py index d0b78aaf4a6..552d59f9259 100644 --- a/apiserver/plane/app/views/workspace/sticky.py +++ b/apiserver/plane/app/views/workspace/sticky.py @@ -39,13 +39,18 @@ def create(self, request, slug): 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) diff --git a/web/core/components/core/content-overflow-HOC.tsx b/web/core/components/core/content-overflow-HOC.tsx new file mode 100644 index 00000000000..fc3fcf455d9 --- /dev/null +++ b/web/core/components/core/content-overflow-HOC.tsx @@ -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(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 ( +
+
{children}
+ + {containerHeight > maxHeight && ( +
+ +
+ )} +
+ ); +}); diff --git a/web/core/components/editor/sticky-editor/editor.tsx b/web/core/components/editor/sticky-editor/editor.tsx index a56fc119b91..3dad67477f7 100644 --- a/web/core/components/editor/sticky-editor/editor.tsx +++ b/web/core/components/editor/sticky-editor/editor.tsx @@ -29,7 +29,7 @@ interface StickyEditorWrapperProps uploadFile: (file: File) => Promise; parentClassName?: string; handleColorChange: (data: Partial) => Promise; - handleDelete: () => Promise; + handleDelete: () => void; } export const StickyEditor = React.forwardRef((props, ref) => { diff --git a/web/core/components/home/widgets/empty-states/issues.tsx b/web/core/components/home/widgets/empty-states/issues.tsx deleted file mode 100644 index fc4548d67ab..00000000000 --- a/web/core/components/home/widgets/empty-states/issues.tsx +++ /dev/null @@ -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 ( -
-
- Assigned issues -
-

No activity to display

-
- ); -}; diff --git a/web/core/components/home/widgets/empty-states/links.tsx b/web/core/components/home/widgets/empty-states/links.tsx new file mode 100644 index 00000000000..00e91274ca3 --- /dev/null +++ b/web/core/components/home/widgets/empty-states/links.tsx @@ -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 ( +
+
+
+ +
+
No quick links yet
+
+ Add any links you need for quick access to your work.{" "} +
+ +
+
+ ); +}; diff --git a/web/core/components/home/widgets/empty-states/recents.tsx b/web/core/components/home/widgets/empty-states/recents.tsx new file mode 100644 index 00000000000..5306584fd05 --- /dev/null +++ b/web/core/components/home/widgets/empty-states/recents.tsx @@ -0,0 +1,15 @@ +import { History } from "lucide-react"; + +export const RecentsEmptyState = () => ( +
+
+
+ +
+
No recent items yet
+
You don’t have any recent items yet.
+
+
+); diff --git a/web/core/components/home/widgets/links/links.tsx b/web/core/components/home/widgets/links/links.tsx index 194b1dfc121..85feab84040 100644 --- a/web/core/components/home/widgets/links/links.tsx +++ b/web/core/components/home/widgets/links/links.tsx @@ -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 = 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 = 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 ; + if (links.length === 0) return toggleLinkModal(true)} />; return ( -
-
- {links && - links.length > 0 && - (showAll ? links : links.slice(0, 2 * columnCount - 1)).map((linkId) => ( - - ))} - - {/* Add new link */} - toggleLinkModal(true)} /> + } + buttonClassName="bg-custom-background-90/20" + > +
+
+ {links && + links.length > 0 && + links.map((linkId) => )} +
- {links.length > 2 * columnCount - 1 && ( - - )} -
+ ); }); diff --git a/web/core/components/home/widgets/links/root.tsx b/web/core/components/home/widgets/links/root.tsx index f7b6f007cc2..504a7cf1680 100644 --- a/web/core/components/home/widgets/links/root.tsx +++ b/web/core/components/home/widgets/links/root.tsx @@ -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} /> -
- {/* rendering links */} - +
+
+
Quick links
+ +
+
+ {/* rendering links */} + +
); diff --git a/web/core/components/home/widgets/loaders/quick-links.tsx b/web/core/components/home/widgets/loaders/quick-links.tsx index e4b8deafcb8..3037bf39c72 100644 --- a/web/core/components/home/widgets/loaders/quick-links.tsx +++ b/web/core/components/home/widgets/loaders/quick-links.tsx @@ -5,7 +5,7 @@ import range from "lodash/range"; import { Loader } from "@plane/ui"; export const QuickLinksWidgetLoader = () => ( - + {range(4).map((index) => ( ))} diff --git a/web/core/components/home/widgets/loaders/recent-activity.tsx b/web/core/components/home/widgets/loaders/recent-activity.tsx index 2f78db64a0a..741ef29ebf1 100644 --- a/web/core/components/home/widgets/loaders/recent-activity.tsx +++ b/web/core/components/home/widgets/loaders/recent-activity.tsx @@ -6,7 +6,7 @@ import { Loader } from "@plane/ui"; export const RecentActivityWidgetLoader = () => ( - {range(7).map((index) => ( + {range(5).map((index) => (
diff --git a/web/core/components/home/widgets/recents/index.tsx b/web/core/components/home/widgets/recents/index.tsx index 8eaecca7708..4e31928b536 100644 --- a/web/core/components/home/widgets/recents/index.tsx +++ b/web/core/components/home/widgets/recents/index.tsx @@ -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 = observer((props) if (!isLoading && recents?.length === 0) return (
-
+
Recents
-
- +
+
); return ( -
+
Recents
-
+
{isLoading && } {!isLoading && recents?.length > 0 && diff --git a/web/core/components/stickies/delete-modal.tsx b/web/core/components/stickies/delete-modal.tsx new file mode 100644 index 00000000000..2d26a53dedc --- /dev/null +++ b/web/core/components/stickies/delete-modal.tsx @@ -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 = 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 ( + Are you sure you want to delete the sticky? } + /> + ); +}); diff --git a/web/core/components/stickies/empty.tsx b/web/core/components/stickies/empty.tsx index 4413ab570f9..4a407a96957 100644 --- a/web/core/components/stickies/empty.tsx +++ b/web/core/components/stickies/empty.tsx @@ -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 ( -
+
- +
-
No stickies yet
-
+
No stickies yet
+
All your stickies in this workspace will appear here.
- +
); diff --git a/web/core/components/stickies/stickies-layout.tsx b/web/core/components/stickies/stickies-layout.tsx index 5ceecc4431a..5cd2f83efcd 100644 --- a/web/core/components/stickies/stickies-layout.tsx +++ b/web/core/components/stickies/stickies-layout.tsx @@ -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(null); const containerRef = useRef(null); // states - const [containerHeight, setContainerHeight] = useState(0); - const [showAllStickies, setShowAllStickies] = useState(false); const [intersectionElement, setIntersectionElement] = useState(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 ( -
-
+
+ } + buttonClassName="bg-custom-background-90/20" + > {/* @ts-expect-error type mismatch here */} {childElements} -
- {containerHeight > 632.9 && ( -
- -
- )} +
); }); diff --git a/web/core/components/stickies/sticky/inputs.tsx b/web/core/components/stickies/sticky/inputs.tsx index 3dd97c9c061..8e36fd0b8db 100644 --- a/web/core/components/stickies/sticky/inputs.tsx +++ b/web/core/components/stickies/sticky/inputs.tsx @@ -13,7 +13,7 @@ type TProps = { handleUpdate: DebouncedFunc<(payload: Partial) => Promise>; stickyId: string | undefined; handleChange: (data: Partial) => Promise; - handleDelete: () => Promise; + handleDelete: () => void; }; export const StickyInput = (props: TProps) => { const { stickyData, workspaceSlug, handleUpdate, stickyId, handleDelete, handleChange } = props; diff --git a/web/core/components/stickies/sticky/root.tsx b/web/core/components/stickies/sticky/root.tsx index a9b26ae5ffb..d7c8fe70ddc 100644 --- a/web/core/components/stickies/sticky/root.tsx +++ b/web/core/components/stickies/sticky/root.tsx @@ -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 ( -
- {onClose && ( - - )} - {/* inputs */} - + setIsDeleteModalOpen(false)} /> -
+
+ {onClose && ( + + )} + {/* inputs */} + { + if (!stickyId) return; + setIsDeleteModalOpen(true); + }} + handleChange={handleChange} + /> +
+ ); });