From efcfd6954c60ff073a8e56a90e4e410c866c3ed8 Mon Sep 17 00:00:00 2001 From: gakshita Date: Fri, 10 Jan 2025 14:42:00 +0530 Subject: [PATCH 01/16] feat: added independent stickies page --- packages/constants/src/index.ts | 1 + packages/constants/src/stickies.ts | 1 + .../(projects)/stickies/header.tsx | 59 +++++++ .../(projects)/stickies/layout.tsx | 13 ++ .../(projects)/stickies/page.tsx | 27 +++ .../components/core/content-overflow-HOC.tsx | 4 +- web/core/components/stickies/empty.tsx | 43 +++-- web/core/components/stickies/index.ts | 1 + web/core/components/stickies/layout/index.ts | 3 + .../stickies/layout/stickies-infinite.tsx | 63 +++++++ .../stickies/layout/stickies-list.tsx | 105 ++++++++++++ .../stickies/layout/stickies-truncated.tsx | 32 ++++ web/core/components/stickies/modal/search.tsx | 32 +++- .../components/stickies/modal/stickies.tsx | 6 +- .../components/stickies/stickies-layout.tsx | 160 ------------------ web/core/components/stickies/widget.tsx | 4 +- web/core/services/sticky.service.ts | 10 +- web/core/store/sticky/sticky.store.ts | 105 ++++++++---- 18 files changed, 446 insertions(+), 223 deletions(-) create mode 100644 packages/constants/src/stickies.ts create mode 100644 web/app/[workspaceSlug]/(projects)/stickies/header.tsx create mode 100644 web/app/[workspaceSlug]/(projects)/stickies/layout.tsx create mode 100644 web/app/[workspaceSlug]/(projects)/stickies/page.tsx create mode 100644 web/core/components/stickies/layout/index.ts create mode 100644 web/core/components/stickies/layout/stickies-infinite.tsx create mode 100644 web/core/components/stickies/layout/stickies-list.tsx create mode 100644 web/core/components/stickies/layout/stickies-truncated.tsx delete mode 100644 web/core/components/stickies/stickies-layout.tsx diff --git a/packages/constants/src/index.ts b/packages/constants/src/index.ts index eeab193bb39..acd72f36110 100644 --- a/packages/constants/src/index.ts +++ b/packages/constants/src/index.ts @@ -10,3 +10,4 @@ export * from "./state"; export * from "./swr"; export * from "./user"; export * from "./workspace"; +export * from "./stickies"; diff --git a/packages/constants/src/stickies.ts b/packages/constants/src/stickies.ts new file mode 100644 index 00000000000..6bf6fd20b92 --- /dev/null +++ b/packages/constants/src/stickies.ts @@ -0,0 +1 @@ +export const STICKIES_PER_PAGE = 30; diff --git a/web/app/[workspaceSlug]/(projects)/stickies/header.tsx b/web/app/[workspaceSlug]/(projects)/stickies/header.tsx new file mode 100644 index 00000000000..a4e97ad993b --- /dev/null +++ b/web/app/[workspaceSlug]/(projects)/stickies/header.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { observer } from "mobx-react"; +// ui +import { useParams } from "next/navigation"; +import { Breadcrumbs, Button, Header, RecentStickyIcon } from "@plane/ui"; +// components +import { BreadcrumbLink } from "@/components/common"; +import { STICKY_COLORS } from "@/components/editor/sticky-editor/color-pallete"; + +// hooks +import { StickySearch } from "@/components/stickies/modal/search"; +import { useStickyOperations } from "@/components/stickies/sticky/use-operations"; +// plane-web +import { useSticky } from "@/hooks/use-stickies"; + +export const WorkspaceStickyHeader = observer(() => { + const { workspaceSlug } = useParams(); + // hooks + const { toggleShowNewSticky } = useSticky(); + const { stickyOperations } = useStickyOperations({ workspaceSlug: workspaceSlug?.toString() }); + + return ( + <> +
+ +
+ + } + /> + } + /> + +
+
+ + + + + +
+ + ); +}); diff --git a/web/app/[workspaceSlug]/(projects)/stickies/layout.tsx b/web/app/[workspaceSlug]/(projects)/stickies/layout.tsx new file mode 100644 index 00000000000..b1d7e6b92ed --- /dev/null +++ b/web/app/[workspaceSlug]/(projects)/stickies/layout.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { AppHeader, ContentWrapper } from "@/components/core"; +import { WorkspaceStickyHeader } from "./header"; + +export default function WorkspaceStickiesLayout({ children }: { children: React.ReactNode }) { + return ( + <> + } /> + {children} + + ); +} diff --git a/web/app/[workspaceSlug]/(projects)/stickies/page.tsx b/web/app/[workspaceSlug]/(projects)/stickies/page.tsx new file mode 100644 index 00000000000..b77aaf4da65 --- /dev/null +++ b/web/app/[workspaceSlug]/(projects)/stickies/page.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { useParams } from "next/navigation"; +// components +import { PageHead } from "@/components/core"; +import { StickiesInfinite } from "@/components/stickies"; + +const WorkspaceStickiesPage = () => { + // router + const { workspaceSlug: routeWorkspaceSlug } = useParams(); + const pageTitle = "My stickies"; + + // derived values + const workspaceSlug = (routeWorkspaceSlug as string) || undefined; + + if (!workspaceSlug) return null; + return ( + <> + +
+ +
+ + ); +}; + +export default WorkspaceStickiesPage; diff --git a/web/core/components/core/content-overflow-HOC.tsx b/web/core/components/core/content-overflow-HOC.tsx index f6a2423cd61..5a6838d8476 100644 --- a/web/core/components/core/content-overflow-HOC.tsx +++ b/web/core/components/core/content-overflow-HOC.tsx @@ -9,6 +9,7 @@ interface IContentOverflowWrapper { buttonClassName?: string; containerClassName?: string; fallback?: ReactNode; + customButtonAction?: () => void; } export const ContentOverflowWrapper = observer((props: IContentOverflowWrapper) => { @@ -18,6 +19,7 @@ export const ContentOverflowWrapper = observer((props: IContentOverflowWrapper) buttonClassName = "text-sm font-medium text-custom-primary-100", containerClassName, fallback = null, + customButtonAction, } = props; // states @@ -136,7 +138,7 @@ export const ContentOverflowWrapper = observer((props: IContentOverflowWrapper) "gap-1 w-full text-custom-primary-100 text-sm font-medium transition-opacity duration-300", buttonClassName )} - onClick={handleToggle} + onClick={() => (customButtonAction ? customButtonAction() : handleToggle())} disabled={isTransitioning} > {showAll ? "Show less" : "Show all"} diff --git a/web/core/components/stickies/empty.tsx b/web/core/components/stickies/empty.tsx index 4a407a96957..78bfb89ee98 100644 --- a/web/core/components/stickies/empty.tsx +++ b/web/core/components/stickies/empty.tsx @@ -4,9 +4,10 @@ import { Button } from "@plane/ui"; type TProps = { handleCreate: () => void; creatingSticky?: boolean; + query?: string; }; export const EmptyState = (props: TProps) => { - const { handleCreate, creatingSticky } = props; + const { handleCreate, creatingSticky, query } = props; return (
@@ -15,22 +16,34 @@ export const EmptyState = (props: TProps) => { >
-
No stickies yet
+
+ {query ? "No matching stickies" : "No stickies yet"} +
- All your stickies in this workspace will appear here. + {query + ? "No stickies detected with the matching criteria." + : "All your stickies in this workspace will appear here."}
- + {!query && ( + + )}
); diff --git a/web/core/components/stickies/index.ts b/web/core/components/stickies/index.ts index 1376a85eb5f..91363220e06 100644 --- a/web/core/components/stickies/index.ts +++ b/web/core/components/stickies/index.ts @@ -1,2 +1,3 @@ export * from "./action-bar"; export * from "./widget"; +export * from "./layout"; diff --git a/web/core/components/stickies/layout/index.ts b/web/core/components/stickies/layout/index.ts new file mode 100644 index 00000000000..e3afe22f924 --- /dev/null +++ b/web/core/components/stickies/layout/index.ts @@ -0,0 +1,3 @@ +export * from "./stickies-infinite"; +export * from "./stickies-list"; +export * from "./stickies-truncated"; diff --git a/web/core/components/stickies/layout/stickies-infinite.tsx b/web/core/components/stickies/layout/stickies-infinite.tsx new file mode 100644 index 00000000000..01f75f5fd80 --- /dev/null +++ b/web/core/components/stickies/layout/stickies-infinite.tsx @@ -0,0 +1,63 @@ +import { useRef, useState } from "react"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import useSWR from "swr"; +import { STICKIES_PER_PAGE } from "@plane/constants"; +import { ContentWrapper, Loader } from "@plane/ui"; +import { cn } from "@plane/utils"; +import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; +import { useSticky } from "@/hooks/use-stickies"; +import { StickiesLayout } from "./stickies-list"; + +export const StickiesInfinite = observer(() => { + const { workspaceSlug } = useParams(); + // hooks + const { fetchWorkspaceStickies, fetchNextWorkspaceStickies, getWorkspaceStickies, loader, paginationInfo } = + useSticky(); + //state + const [elementRef, setElementRef] = useState(null); + + // ref + const containerRef = useRef(null); + + useSWR( + workspaceSlug ? `WORKSPACE_STICKIES_${workspaceSlug}` : null, + workspaceSlug ? () => fetchWorkspaceStickies(workspaceSlug.toString()) : null, + { revalidateIfStale: false, revalidateOnFocus: false } + ); + + const handleLoadMore = () => { + if (loader === "pagination") return; + console.log("handleLoadMore"); + fetchNextWorkspaceStickies(workspaceSlug?.toString()); + }; + + const hasNextPage = paginationInfo?.next_page_results && paginationInfo?.next_cursor !== undefined; + const shouldObserve = hasNextPage && loader !== "pagination"; + const workspaceStickies = getWorkspaceStickies(workspaceSlug?.toString()); + useIntersectionObserver(containerRef, shouldObserve ? elementRef : null, handleLoadMore); + + return ( + + = STICKIES_PER_PAGE && ( +
+
+ + + +
+
+ ) + } + /> +
+ ); +}); diff --git a/web/core/components/stickies/layout/stickies-list.tsx b/web/core/components/stickies/layout/stickies-list.tsx new file mode 100644 index 00000000000..9c1714ba5c4 --- /dev/null +++ b/web/core/components/stickies/layout/stickies-list.tsx @@ -0,0 +1,105 @@ +import { useEffect, useRef, useState } from "react"; +import { observer } from "mobx-react"; +import Masonry from "react-masonry-component"; +import { Loader } from "@plane/ui"; +import { cn } from "@plane/utils"; +import { useSticky } from "@/hooks/use-stickies"; +import { STICKY_COLORS } from "../../editor/sticky-editor/color-pallete"; +import { EmptyState } from "../empty"; +import { StickyNote } from "../sticky"; +import { useStickyOperations } from "../sticky/use-operations"; + +type TStickiesLayout = { + workspaceSlug: string; + intersectionElement?: React.ReactNode | null; +}; + +type TProps = TStickiesLayout & { + columnCount: number; +}; + +export const StickiesList = observer((props: TProps) => { + const { workspaceSlug, intersectionElement, columnCount } = props; + // hooks + const { getWorkspaceStickies, toggleShowNewSticky, creatingSticky, searchQuery, loader } = useSticky(); + const { stickyOperations } = useStickyOperations({ workspaceSlug: workspaceSlug?.toString() }); + + //derived values + const workspaceStickies = getWorkspaceStickies(workspaceSlug?.toString()); + const itemWidth = `${100 / columnCount}%`; + + if (loader === "init-loader" || loader === "mutation") { + return ( +
+ + + +
+ ); + } + + if (loader === "loaded" && workspaceStickies.length === 0) + return ( + { + toggleShowNewSticky(true); + stickyOperations.create({ color: STICKY_COLORS[0] }); + }} + /> + ); + + return ( + <> + {/* @ts-expect-error type mismatch here */} + + {workspaceStickies.map((stickyId) => ( +
+ +
+ ))} + {intersectionElement &&
{intersectionElement}
} +
+ + ); +}); + +export const StickiesLayout = (props: TStickiesLayout) => { + // states + const [containerWidth, setContainerWidth] = useState(null); + // refs + const ref = useRef(null); + + useEffect(() => { + if (!ref?.current) return; + + setContainerWidth(ref?.current.offsetWidth); + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + setContainerWidth(entry.contentRect.width); + } + }); + + resizeObserver.observe(ref?.current); + return () => resizeObserver.disconnect(); + }, []); + + const getColumnCount = (width: number | null): number => { + if (width === null) return 4; + + if (width < 640) return 2; // sm + if (width < 768) return 3; // md + if (width < 1024) return 4; // lg + if (width < 1280) return 5; // xl + return 6; // 2xl and above + }; + + const columnCount = getColumnCount(containerWidth); + return ( +
+ +
+ ); +}; diff --git a/web/core/components/stickies/layout/stickies-truncated.tsx b/web/core/components/stickies/layout/stickies-truncated.tsx new file mode 100644 index 00000000000..b5eafff2c11 --- /dev/null +++ b/web/core/components/stickies/layout/stickies-truncated.tsx @@ -0,0 +1,32 @@ +import { observer } from "mobx-react"; +import { useParams, useRouter } from "next/navigation"; +import useSWR from "swr"; +import { useSticky } from "@/hooks/use-stickies"; +import { ContentOverflowWrapper } from "../../core/content-overflow-HOC"; +import { StickiesLayout } from "./stickies-list"; + +export const StickiesTruncated = observer(() => { + // router + const router = useRouter(); + const { workspaceSlug } = useParams(); + // hooks + const { fetchWorkspaceStickies } = useSticky(); + + useSWR( + workspaceSlug ? `WORKSPACE_STICKIES_${workspaceSlug}` : null, + workspaceSlug ? () => fetchWorkspaceStickies(workspaceSlug.toString()) : null, + { revalidateIfStale: false, revalidateOnFocus: false } + ); + + return ( + } + buttonClassName="bg-custom-background-90/20" + customButtonAction={() => router.push(`/${workspaceSlug}/stickies`)} + > + + + ); +}); diff --git a/web/core/components/stickies/modal/search.tsx b/web/core/components/stickies/modal/search.tsx index 34c0a8f6d1b..e55046fe1c1 100644 --- a/web/core/components/stickies/modal/search.tsx +++ b/web/core/components/stickies/modal/search.tsx @@ -1,6 +1,6 @@ "use client"; -import { FC, useRef, useState } from "react"; +import { FC, useCallback, useRef, useState } from "react"; import { observer } from "mobx-react"; import { Search, X } from "lucide-react"; // plane hooks @@ -8,25 +8,43 @@ import { useOutsideClickDetector } from "@plane/hooks"; // helpers import { cn } from "@/helpers/common.helper"; import { useSticky } from "@/hooks/use-stickies"; +import { debounce } from "lodash"; +import { useParams } from "next/navigation"; export const StickySearch: FC = observer(() => { + // router + const { workspaceSlug } = useParams(); // hooks - const { searchQuery, updateSearchQuery } = useSticky(); + const { searchQuery, updateSearchQuery, fetchWorkspaceStickies } = useSticky(); // refs const inputRef = useRef(null); // states const [isSearchOpen, setIsSearchOpen] = useState(false); + // outside click detector hook useOutsideClickDetector(inputRef, () => { if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false); }); const handleInputKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Escape") { - if (searchQuery && searchQuery.trim() !== "") updateSearchQuery(""); - else setIsSearchOpen(false); + if (searchQuery && searchQuery.trim() !== "") { + updateSearchQuery(""); + fetchStickies(); + } else setIsSearchOpen(false); } }; + const fetchStickies = async () => { + await fetchWorkspaceStickies(workspaceSlug.toString()); + }; + + const debouncedSearch = useCallback( + debounce(async () => { + await fetchStickies(); + }, 1000), + [fetchWorkspaceStickies] + ); + return (
{!isSearchOpen && ( @@ -55,7 +73,10 @@ export const StickySearch: FC = observer(() => { className="w-full max-w-[234px] border-none bg-transparent text-sm text-custom-text-100 placeholder:text-custom-text-400 focus:outline-none" placeholder="Search by title" value={searchQuery} - onChange={(e) => updateSearchQuery(e.target.value)} + onChange={(e) => { + updateSearchQuery(e.target.value); + debouncedSearch(); + }} onKeyDown={handleInputKeyDown} /> {isSearchOpen && ( @@ -65,6 +86,7 @@ export const StickySearch: FC = observer(() => { onClick={() => { updateSearchQuery(""); setIsSearchOpen(false); + fetchStickies(); }} > diff --git a/web/core/components/stickies/modal/stickies.tsx b/web/core/components/stickies/modal/stickies.tsx index dcd8645a023..f26e577a108 100644 --- a/web/core/components/stickies/modal/stickies.tsx +++ b/web/core/components/stickies/modal/stickies.tsx @@ -4,7 +4,7 @@ import { Plus, X } from "lucide-react"; import { RecentStickyIcon } from "@plane/ui"; import { useSticky } from "@/hooks/use-stickies"; import { STICKY_COLORS } from "../../editor/sticky-editor/color-pallete"; -import { StickiesLayout } from "../stickies-layout"; +import { StickiesTruncated } from "../layout/stickies-truncated"; import { useStickyOperations } from "../sticky/use-operations"; import { StickySearch } from "./search"; @@ -19,7 +19,7 @@ export const Stickies = observer((props: TProps) => { const { stickyOperations } = useStickyOperations({ workspaceSlug: workspaceSlug?.toString() }); return ( -
+
{/* header */}
{/* Title */} @@ -61,7 +61,7 @@ export const Stickies = observer((props: TProps) => {
{/* content */}
- +
); diff --git a/web/core/components/stickies/stickies-layout.tsx b/web/core/components/stickies/stickies-layout.tsx deleted file mode 100644 index 5cd2f83efcd..00000000000 --- a/web/core/components/stickies/stickies-layout.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import React, { useState, useEffect, useRef } from "react"; -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -import Masonry from "react-masonry-component"; -import useSWR from "swr"; -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"; -import { useStickyOperations } from "./sticky/use-operations"; - -const PER_PAGE = 10; - -type TProps = { - columnCount: number; -}; - -export const StickyAll = observer((props: TProps) => { - const { columnCount } = props; - // refs - const masonryRef = useRef(null); - const containerRef = useRef(null); - // states - const [intersectionElement, setIntersectionElement] = useState(null); - // router - const { workspaceSlug } = useParams(); - // hooks - const { stickyOperations } = useStickyOperations({ workspaceSlug: workspaceSlug?.toString() }); - - const { - fetchingWorkspaceStickies, - toggleShowNewSticky, - getWorkspaceStickies, - fetchWorkspaceStickies, - currentPage, - totalPages, - incrementPage, - creatingSticky, - } = useSticky(); - - const workspaceStickies = getWorkspaceStickies(workspaceSlug?.toString()); - const itemWidth = `${100 / columnCount}%`; - - useSWR( - workspaceSlug ? `WORKSPACE_STICKIES_${workspaceSlug}_${PER_PAGE}:${currentPage}:0` : null, - workspaceSlug - ? () => fetchWorkspaceStickies(workspaceSlug.toString(), `${PER_PAGE}:${currentPage}:0`, PER_PAGE) - : null - ); - - useEffect(() => { - if (!fetchingWorkspaceStickies && workspaceStickies.length === 0) { - toggleShowNewSticky(true); - } - }, [fetchingWorkspaceStickies, workspaceStickies, toggleShowNewSticky]); - - useIntersectionObserver(containerRef, fetchingWorkspaceStickies ? null : intersectionElement, incrementPage, "20%"); - - if (fetchingWorkspaceStickies && workspaceStickies.length === 0) { - return ( -
- - - -
- ); - } - - const getStickiesToRender = () => { - let stickies: (string | undefined)[] = workspaceStickies; - if (currentPage + 1 < totalPages && stickies.length >= PER_PAGE) { - stickies = [...stickies, undefined]; - } - return stickies; - }; - - const stickyIds = getStickiesToRender(); - - const childElements = stickyIds.map((stickyId, index) => ( -
- {index === stickyIds.length - 1 && currentPage + 1 < totalPages ? ( -
- - - -
- ) : ( - - )} -
- )); - - if (!fetchingWorkspaceStickies && workspaceStickies.length === 0) - return ( - { - toggleShowNewSticky(true); - stickyOperations.create({ color: STICKY_COLORS[0] }); - }} - /> - ); - - return ( -
- } - buttonClassName="bg-custom-background-90/20" - > - {/* @ts-expect-error type mismatch here */} - {childElements} - -
- ); -}); - -export const StickiesLayout = () => { - // states - const [containerWidth, setContainerWidth] = useState(null); - // refs - const ref = useRef(null); - - useEffect(() => { - if (!ref?.current) return; - - setContainerWidth(ref?.current.offsetWidth); - - const resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - setContainerWidth(entry.contentRect.width); - } - }); - - resizeObserver.observe(ref?.current); - return () => resizeObserver.disconnect(); - }, []); - - const getColumnCount = (width: number | null): number => { - if (width === null) return 4; - - if (width < 640) return 2; // sm - if (width < 768) return 3; // md - if (width < 1024) return 4; // lg - if (width < 1280) return 5; // xl - return 6; // 2xl and above - }; - - const columnCount = getColumnCount(containerWidth); - return ( -
- -
- ); -}; diff --git a/web/core/components/stickies/widget.tsx b/web/core/components/stickies/widget.tsx index 4b0830ec6f7..768edfe071c 100644 --- a/web/core/components/stickies/widget.tsx +++ b/web/core/components/stickies/widget.tsx @@ -2,8 +2,8 @@ import { useParams } from "next/navigation"; import { Plus } from "lucide-react"; import { useSticky } from "@/hooks/use-stickies"; import { STICKY_COLORS } from "../editor/sticky-editor/color-pallete"; +import { StickiesTruncated } from "./layout"; import { StickySearch } from "./modal/search"; -import { StickiesLayout } from "./stickies-layout"; import { useStickyOperations } from "./sticky/use-operations"; export const StickiesWidget = () => { @@ -39,7 +39,7 @@ export const StickiesWidget = () => {
- +
); diff --git a/web/core/services/sticky.service.ts b/web/core/services/sticky.service.ts index 9e0d2d4376c..5f51b9e7d0f 100644 --- a/web/core/services/sticky.service.ts +++ b/web/core/services/sticky.service.ts @@ -1,4 +1,5 @@ // helpers +import { STICKIES_PER_PAGE } from "@plane/constants"; import { TSticky } from "@plane/types"; import { API_BASE_URL } from "@/helpers/common.helper"; // services @@ -19,13 +20,14 @@ export class StickyService extends APIService { async getStickies( workspaceSlug: string, - cursor?: string, - per_page?: number + cursor: string, + query?: string ): Promise<{ results: TSticky[]; total_pages: number }> { return this.get(`/api/workspaces/${workspaceSlug}/stickies/`, { params: { - cursor: cursor || `5:0:0`, - per_page: per_page || 5, + cursor, + per_page: STICKIES_PER_PAGE, + query, }, }) .then((res) => res?.data) diff --git a/web/core/store/sticky/sticky.store.ts b/web/core/store/sticky/sticky.store.ts index 4a5d1e1d2eb..d94c8b1e908 100644 --- a/web/core/store/sticky/sticky.store.ts +++ b/web/core/store/sticky/sticky.store.ts @@ -1,18 +1,20 @@ +import { set } from "lodash"; import { observable, action, makeObservable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; -import { TSticky } from "@plane/types"; +import { STICKIES_PER_PAGE } from "@plane/constants"; +import { TLoader, TPaginationInfo, TSticky } from "@plane/types"; import { StickyService } from "@/services/sticky.service"; + export interface IStickyStore { creatingSticky: boolean; - fetchingWorkspaceStickies: boolean; + loader: TLoader; workspaceStickies: Record; // workspaceId -> stickyIds stickies: Record; // stickyId -> sticky searchQuery: string; activeStickyId: string | undefined; recentStickyId: string | undefined; showAddNewSticky: boolean; - currentPage: number; - totalPages: number; + paginationInfo: TPaginationInfo | undefined; // computed getWorkspaceStickies: (workspaceSlug: string) => string[]; @@ -20,26 +22,25 @@ export interface IStickyStore { // actions toggleShowNewSticky: (value: boolean) => void; updateSearchQuery: (query: string) => void; - fetchWorkspaceStickies: (workspaceSlug: string, cursor?: string, per_page?: number) => void; + fetchWorkspaceStickies: (workspaceSlug: string) => void; createSticky: (workspaceSlug: string, sticky: Partial) => void; updateSticky: (workspaceSlug: string, id: string, updates: Partial) => void; deleteSticky: (workspaceSlug: string, id: string) => void; updateActiveStickyId: (id: string | undefined) => void; fetchRecentSticky: (workspaceSlug: string) => void; - incrementPage: () => void; + fetchNextWorkspaceStickies: (workspaceSlug: string) => void; } export class StickyStore implements IStickyStore { + loader: TLoader = "init-loader"; creatingSticky = false; - fetchingWorkspaceStickies = true; workspaceStickies: Record = {}; stickies: Record = {}; recentStickyId: string | undefined = undefined; searchQuery = ""; activeStickyId: string | undefined = undefined; showAddNewSticky = false; - currentPage = 0; - totalPages = 0; + paginationInfo: TPaginationInfo | undefined = undefined; // services stickyService; @@ -48,33 +49,28 @@ export class StickyStore implements IStickyStore { makeObservable(this, { // observables creatingSticky: observable, - fetchingWorkspaceStickies: observable, + loader: observable, activeStickyId: observable, showAddNewSticky: observable, recentStickyId: observable, workspaceStickies: observable, stickies: observable, searchQuery: observable, - currentPage: observable, - totalPages: observable, // actions updateSearchQuery: action, updateSticky: action, deleteSticky: action, - incrementPage: action, + fetchNextWorkspaceStickies: action, + fetchWorkspaceStickies: action, + createSticky: action, + updateActiveStickyId: action, + toggleShowNewSticky: action, + fetchRecentSticky: action, }); this.stickyService = new StickyService(); } - getWorkspaceStickies = computedFn((workspaceSlug: string) => { - let filteredStickies = (this.workspaceStickies[workspaceSlug] || []).map((stickyId) => this.stickies[stickyId]); - if (this.searchQuery) { - filteredStickies = filteredStickies.filter( - (sticky) => sticky.name && sticky.name.toLowerCase().includes(this.searchQuery.toLowerCase()) - ); - } - return filteredStickies.map((sticky) => sticky.id); - }); + getWorkspaceStickies = computedFn((workspaceSlug: string) => this.workspaceStickies[workspaceSlug]); toggleShowNewSticky = (value: boolean) => { this.showAddNewSticky = value; @@ -88,34 +84,77 @@ export class StickyStore implements IStickyStore { this.activeStickyId = id; }; - incrementPage = () => { - this.currentPage += 1; - }; - fetchRecentSticky = async (workspaceSlug: string) => { - const response = await this.stickyService.getStickies(workspaceSlug, "1:0:0", 1); + const response = await this.stickyService.getStickies(workspaceSlug, "1:0:0"); runInAction(() => { this.recentStickyId = response.results[0]?.id; this.stickies[response.results[0]?.id] = response.results[0]; }); }; - fetchWorkspaceStickies = async (workspaceSlug: string, cursor?: string, per_page?: number) => { + fetchNextWorkspaceStickies = async (workspaceSlug: string) => { try { - const response = await this.stickyService.getStickies(workspaceSlug, cursor, per_page); + if (!this.paginationInfo?.next_cursor || !this.paginationInfo.next_page_results || this.loader === "pagination") { + return; + } + this.loader = "pagination"; + const response = await this.stickyService.getStickies( + workspaceSlug, + this.paginationInfo.next_cursor, + this.searchQuery + ); runInAction(() => { - response.results.forEach((sticky) => { + const { results, ...paginationInfo } = response; + + // Add new stickies to store + results.forEach((sticky) => { if (!this.workspaceStickies[workspaceSlug]?.includes(sticky.id)) { this.workspaceStickies[workspaceSlug] = [...(this.workspaceStickies[workspaceSlug] || []), sticky.id]; } this.stickies[sticky.id] = sticky; }); - this.totalPages = response.total_pages; - this.fetchingWorkspaceStickies = false; + + // Update pagination info directly from backend + set(this, "paginationInfo", paginationInfo); + set(this, "loader", "loaded"); + }); + } catch (e) { + console.error(e); + runInAction(() => { + this.loader = "loaded"; + }); + } + }; + + fetchWorkspaceStickies = async (workspaceSlug: string) => { + try { + if (this.workspaceStickies[workspaceSlug]) { + this.loader = "mutation"; + } else { + this.loader = "init-loader"; + } + + const response = await this.stickyService.getStickies( + workspaceSlug, + `${STICKIES_PER_PAGE}:0:0`, + this.searchQuery + ); + + runInAction(() => { + const { results, ...paginationInfo } = response; + + results.forEach((sticky) => { + this.stickies[sticky.id] = sticky; + }); + this.workspaceStickies[workspaceSlug] = results.map((sticky) => sticky.id); + set(this, "paginationInfo", paginationInfo); + this.loader = "loaded"; }); } catch (e) { console.error(e); - this.fetchingWorkspaceStickies = false; + runInAction(() => { + this.loader = "loaded"; + }); } }; From 3e5e506ecfcc7a1bd8ff57ef719aeed868f58000 Mon Sep 17 00:00:00 2001 From: gakshita Date: Fri, 10 Jan 2025 15:01:05 +0530 Subject: [PATCH 02/16] chore: randomized sticky color --- .../(projects)/stickies/header.tsx | 3 +-- .../stickies/layout/stickies-list.tsx | 3 +-- .../components/stickies/modal/stickies.tsx | 3 +-- .../components/stickies/sticky/inputs.tsx | 24 +------------------ web/core/components/stickies/sticky/root.tsx | 1 - .../stickies/sticky/use-operations.tsx | 18 ++++++++++---- web/core/components/stickies/widget.tsx | 3 +-- 7 files changed, 19 insertions(+), 36 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/stickies/header.tsx b/web/app/[workspaceSlug]/(projects)/stickies/header.tsx index a4e97ad993b..c63e9c9d332 100644 --- a/web/app/[workspaceSlug]/(projects)/stickies/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/stickies/header.tsx @@ -6,7 +6,6 @@ import { useParams } from "next/navigation"; import { Breadcrumbs, Button, Header, RecentStickyIcon } from "@plane/ui"; // components import { BreadcrumbLink } from "@/components/common"; -import { STICKY_COLORS } from "@/components/editor/sticky-editor/color-pallete"; // hooks import { StickySearch } from "@/components/stickies/modal/search"; @@ -47,7 +46,7 @@ export const WorkspaceStickyHeader = observer(() => { className="items-center gap-1" onClick={() => { toggleShowNewSticky(true); - stickyOperations.create({ color: STICKY_COLORS[0] }); + stickyOperations.create(); }} > Add sticky diff --git a/web/core/components/stickies/layout/stickies-list.tsx b/web/core/components/stickies/layout/stickies-list.tsx index 9c1714ba5c4..6ce9e9f5b41 100644 --- a/web/core/components/stickies/layout/stickies-list.tsx +++ b/web/core/components/stickies/layout/stickies-list.tsx @@ -4,7 +4,6 @@ import Masonry from "react-masonry-component"; import { Loader } from "@plane/ui"; import { cn } from "@plane/utils"; import { useSticky } from "@/hooks/use-stickies"; -import { STICKY_COLORS } from "../../editor/sticky-editor/color-pallete"; import { EmptyState } from "../empty"; import { StickyNote } from "../sticky"; import { useStickyOperations } from "../sticky/use-operations"; @@ -45,7 +44,7 @@ export const StickiesList = observer((props: TProps) => { creatingSticky={creatingSticky} handleCreate={() => { toggleShowNewSticky(true); - stickyOperations.create({ color: STICKY_COLORS[0] }); + stickyOperations.create(); }} /> ); diff --git a/web/core/components/stickies/modal/stickies.tsx b/web/core/components/stickies/modal/stickies.tsx index f26e577a108..87af624a47d 100644 --- a/web/core/components/stickies/modal/stickies.tsx +++ b/web/core/components/stickies/modal/stickies.tsx @@ -3,7 +3,6 @@ import { useParams } from "next/navigation"; import { Plus, X } from "lucide-react"; import { RecentStickyIcon } from "@plane/ui"; import { useSticky } from "@/hooks/use-stickies"; -import { STICKY_COLORS } from "../../editor/sticky-editor/color-pallete"; import { StickiesTruncated } from "../layout/stickies-truncated"; import { useStickyOperations } from "../sticky/use-operations"; import { StickySearch } from "./search"; @@ -33,7 +32,7 @@ export const Stickies = observer((props: TProps) => {