From e3ee1b004c68811959067f7fc756a58f5da371a9 Mon Sep 17 00:00:00 2001 From: gakshita Date: Tue, 7 Jan 2025 13:48:13 +0530 Subject: [PATCH 01/12] feat: added-stickies --- packages/types/src/index.d.ts | 1 + packages/types/src/stickies.d copy.ts | 8 + packages/types/src/stickies.d.ts | 8 + packages/ui/src/icons/index.ts | 2 + packages/ui/src/icons/multiple-sticky.tsx | 28 +++ packages/ui/src/icons/sticky-note-icon.tsx | 36 +++ web/ce/components/stickies/index.ts | 1 - web/ce/components/stickies/widget.tsx | 1 - web/core/components/editor/index.ts | 1 + .../editor/sticky-editor/color-pallete.tsx | 36 +++ .../editor/sticky-editor/editor.tsx | 109 +++++++++ .../components/editor/sticky-editor/index.ts | 2 + .../editor/sticky-editor/toolbar.tsx | 115 ++++++++++ .../home/home-dashboard-widgets.tsx | 2 +- web/core/components/stickies/action-bar.tsx | 129 +++++++++++ web/core/components/stickies/empty.tsx | 40 ++++ web/core/components/stickies/index.ts | 2 + web/core/components/stickies/modal/index.tsx | 15 ++ web/core/components/stickies/modal/search.tsx | 76 +++++++ .../components/stickies/modal/stickies.tsx | 68 ++++++ .../components/stickies/stickies-layout.tsx | 209 ++++++++++++++++++ web/core/components/stickies/sticky/index.ts | 1 + .../components/stickies/sticky/inputs.tsx | 110 +++++++++ web/core/components/stickies/sticky/root.tsx | 72 ++++++ .../stickies/sticky/use-operations.tsx | 92 ++++++++ web/core/components/stickies/widget.tsx | 41 ++++ web/core/hooks/use-stickies.tsx | 11 + web/core/services/sticky.service.ts | 60 +++++ web/core/store/base-command-palette.store.ts | 35 ++- web/core/store/root.store.ts | 4 + web/core/store/sticky/sticky.store.ts | 165 ++++++++++++++ 31 files changed, 1469 insertions(+), 11 deletions(-) create mode 100644 packages/types/src/stickies.d copy.ts create mode 100644 packages/types/src/stickies.d.ts create mode 100644 packages/ui/src/icons/multiple-sticky.tsx create mode 100644 packages/ui/src/icons/sticky-note-icon.tsx delete mode 100644 web/ce/components/stickies/index.ts delete mode 100644 web/ce/components/stickies/widget.tsx create mode 100644 web/core/components/editor/sticky-editor/color-pallete.tsx create mode 100644 web/core/components/editor/sticky-editor/editor.tsx create mode 100644 web/core/components/editor/sticky-editor/index.ts create mode 100644 web/core/components/editor/sticky-editor/toolbar.tsx create mode 100644 web/core/components/stickies/action-bar.tsx create mode 100644 web/core/components/stickies/empty.tsx create mode 100644 web/core/components/stickies/index.ts create mode 100644 web/core/components/stickies/modal/index.tsx create mode 100644 web/core/components/stickies/modal/search.tsx create mode 100644 web/core/components/stickies/modal/stickies.tsx create mode 100644 web/core/components/stickies/stickies-layout.tsx create mode 100644 web/core/components/stickies/sticky/index.ts create mode 100644 web/core/components/stickies/sticky/inputs.tsx create mode 100644 web/core/components/stickies/sticky/root.tsx create mode 100644 web/core/components/stickies/sticky/use-operations.tsx create mode 100644 web/core/components/stickies/widget.tsx create mode 100644 web/core/hooks/use-stickies.tsx create mode 100644 web/core/services/sticky.service.ts create mode 100644 web/core/store/sticky/sticky.store.ts diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index 7a9cd8b3327..9ec3846b7c5 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -39,3 +39,4 @@ export * from "./activity"; export * from "./epics"; export * from "./charts"; export * from "./home"; +export * from "./stickies"; diff --git a/packages/types/src/stickies.d copy.ts b/packages/types/src/stickies.d copy.ts new file mode 100644 index 00000000000..55f8b23c587 --- /dev/null +++ b/packages/types/src/stickies.d copy.ts @@ -0,0 +1,8 @@ +export type TSticky = { + id: string; + name?: string; + description_html?: string; + color?: string; + createdAt?: Date; + updatedAt?: Date; +}; diff --git a/packages/types/src/stickies.d.ts b/packages/types/src/stickies.d.ts new file mode 100644 index 00000000000..55f8b23c587 --- /dev/null +++ b/packages/types/src/stickies.d.ts @@ -0,0 +1,8 @@ +export type TSticky = { + id: string; + name?: string; + description_html?: string; + color?: string; + createdAt?: Date; + updatedAt?: Date; +}; diff --git a/packages/ui/src/icons/index.ts b/packages/ui/src/icons/index.ts index 01480c78d2f..c09c26057de 100644 --- a/packages/ui/src/icons/index.ts +++ b/packages/ui/src/icons/index.ts @@ -47,3 +47,5 @@ export * from "./overview-icon"; export * from "./on-track-icon"; export * from "./off-track-icon"; export * from "./at-risk-icon"; +export * from "./multiple-sticky"; +export * from "./sticky-note-icon"; diff --git a/packages/ui/src/icons/multiple-sticky.tsx b/packages/ui/src/icons/multiple-sticky.tsx new file mode 100644 index 00000000000..9c33205e992 --- /dev/null +++ b/packages/ui/src/icons/multiple-sticky.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; + +import { ISvgIcons } from "./type"; + +export const RecentStickyIcon: React.FC = ({ className = "text-current", ...rest }) => ( + + + + +); diff --git a/packages/ui/src/icons/sticky-note-icon.tsx b/packages/ui/src/icons/sticky-note-icon.tsx new file mode 100644 index 00000000000..87195028990 --- /dev/null +++ b/packages/ui/src/icons/sticky-note-icon.tsx @@ -0,0 +1,36 @@ +import * as React from "react"; + +import { ISvgIcons } from "./type"; + +export const StickyNoteIcon: React.FC = ({ width = "17", height = "17", className, color }) => ( + + + + + + +); diff --git a/web/ce/components/stickies/index.ts b/web/ce/components/stickies/index.ts deleted file mode 100644 index 97866ce19be..00000000000 --- a/web/ce/components/stickies/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./widget"; diff --git a/web/ce/components/stickies/widget.tsx b/web/ce/components/stickies/widget.tsx deleted file mode 100644 index 56df281e1e4..00000000000 --- a/web/ce/components/stickies/widget.tsx +++ /dev/null @@ -1 +0,0 @@ -export const StickiesWidget = () => <>; diff --git a/web/core/components/editor/index.ts b/web/core/components/editor/index.ts index e7651069ed4..674bbdf1582 100644 --- a/web/core/components/editor/index.ts +++ b/web/core/components/editor/index.ts @@ -2,3 +2,4 @@ export * from "./embeds"; export * from "./lite-text-editor"; export * from "./pdf"; export * from "./rich-text-editor"; +export * from "./sticky-editor"; diff --git a/web/core/components/editor/sticky-editor/color-pallete.tsx b/web/core/components/editor/sticky-editor/color-pallete.tsx new file mode 100644 index 00000000000..3060650f7d3 --- /dev/null +++ b/web/core/components/editor/sticky-editor/color-pallete.tsx @@ -0,0 +1,36 @@ +import { TSticky } from "@plane/types"; + +export const STICKY_COLORS = [ + "#D4DEF7", // light periwinkle + "#B4E4FF", // light blue + "#FFF2B4", // light yellow + "#E3E3E3", // light gray + "#FFE2DD", // light pink + "#F5D1A5", // light orange + "#D1F7C4", // light green + "#E5D4FF", // light purple +]; + +type TProps = { + handleUpdate: (data: Partial) => Promise; +}; + +export const ColorPalette = (props: TProps) => { + const { handleUpdate } = props; + return ( +
+
Background colors
+
+ {STICKY_COLORS.map((color, index) => ( +
+
+ ); +}; diff --git a/web/core/components/editor/sticky-editor/editor.tsx b/web/core/components/editor/sticky-editor/editor.tsx new file mode 100644 index 00000000000..a56fc119b91 --- /dev/null +++ b/web/core/components/editor/sticky-editor/editor.tsx @@ -0,0 +1,109 @@ +import React, { useState } from "react"; +// plane constants +import { EIssueCommentAccessSpecifier } from "@plane/constants"; +// plane editor +import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef } from "@plane/editor"; +// components +import { TSticky } from "@plane/types"; +// helpers +import { cn } from "@/helpers/common.helper"; +import { getEditorFileHandlers } from "@/helpers/editor.helper"; +// hooks +// plane web hooks +import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging"; +import { useFileSize } from "@/plane-web/hooks/use-file-size"; +import { Toolbar } from "./toolbar"; + +interface StickyEditorWrapperProps + extends Omit { + workspaceSlug: string; + workspaceId: string; + projectId?: string; + accessSpecifier?: EIssueCommentAccessSpecifier; + handleAccessChange?: (accessKey: EIssueCommentAccessSpecifier) => void; + showAccessSpecifier?: boolean; + showSubmitButton?: boolean; + isSubmitting?: boolean; + showToolbarInitially?: boolean; + showToolbar?: boolean; + uploadFile: (file: File) => Promise; + parentClassName?: string; + handleColorChange: (data: Partial) => Promise; + handleDelete: () => Promise; +} + +export const StickyEditor = React.forwardRef((props, ref) => { + const { + containerClassName, + workspaceSlug, + workspaceId, + projectId, + handleDelete, + handleColorChange, + showToolbarInitially = true, + showToolbar = true, + parentClassName = "", + placeholder = "Add comment...", + uploadFile, + ...rest + } = props; + // states + const [isFocused, setIsFocused] = useState(showToolbarInitially); + // editor flaggings + const { liteTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString()); + // file size + const { maxFileSize } = useFileSize(); + function isMutableRefObject(ref: React.ForwardedRef): ref is React.MutableRefObject { + return !!ref && typeof ref === "object" && "current" in ref; + } + // derived values + const editorRef = isMutableRefObject(ref) ? ref.current : null; + + return ( +
!showToolbarInitially && setIsFocused(true)} + onBlur={() => !showToolbarInitially && setIsFocused(false)} + > + <>, + }} + placeholder={placeholder} + containerClassName={cn(containerClassName, "relative")} + {...rest} + /> +
+ { + // TODO: update this while toolbar homogenization + // @ts-expect-error type mismatch here + editorRef?.executeMenuItemCommand({ + itemKey: item.itemKey, + ...item.extraProps, + }); + }} + handleDelete={handleDelete} + handleColorChange={handleColorChange} + editorRef={editorRef} + /> +
+
+ ); +}); + +StickyEditor.displayName = "StickyEditor"; diff --git a/web/core/components/editor/sticky-editor/index.ts b/web/core/components/editor/sticky-editor/index.ts new file mode 100644 index 00000000000..f73ee92ef6e --- /dev/null +++ b/web/core/components/editor/sticky-editor/index.ts @@ -0,0 +1,2 @@ +export * from "./editor"; +export * from "./toolbar"; diff --git a/web/core/components/editor/sticky-editor/toolbar.tsx b/web/core/components/editor/sticky-editor/toolbar.tsx new file mode 100644 index 00000000000..f3d0dfec246 --- /dev/null +++ b/web/core/components/editor/sticky-editor/toolbar.tsx @@ -0,0 +1,115 @@ +"use client"; + +import React, { useEffect, useState, useCallback } from "react"; +import { Palette, Trash2 } from "lucide-react"; +// editor +import { EditorRefApi } from "@plane/editor"; +// ui +import { useOutsideClickDetector } from "@plane/hooks"; +import { TSticky } from "@plane/types"; +import { Tooltip } from "@plane/ui"; +// constants +import { TOOLBAR_ITEMS, ToolbarMenuItem } from "@/constants/editor"; +// helpers +import { cn } from "@/helpers/common.helper"; +import { ColorPalette } from "./color-pallete"; + +type Props = { + executeCommand: (item: ToolbarMenuItem) => void; + editorRef: EditorRefApi | null; + handleColorChange: (data: Partial) => Promise; + handleDelete: () => void; +}; + +const toolbarItems = TOOLBAR_ITEMS.sticky; + +export const Toolbar: React.FC = (props) => { + const { executeCommand, editorRef, handleColorChange, handleDelete } = props; + + // State to manage active states of toolbar items + const [activeStates, setActiveStates] = useState>({}); + const [showColorPalette, setShowColorPalette] = useState(false); + const colorPaletteRef = React.useRef(null); + // Function to update active states + const updateActiveStates = useCallback(() => { + if (!editorRef) return; + const newActiveStates: Record = {}; + Object.values(toolbarItems) + .flat() + .forEach((item) => { + // TODO: update this while toolbar homogenization + // @ts-expect-error type mismatch here + newActiveStates[item.renderKey] = editorRef.isMenuItemActive({ + itemKey: item.itemKey, + ...item.extraProps, + }); + }); + setActiveStates(newActiveStates); + }, [editorRef]); + + // useEffect to call updateActiveStates when isActive prop changes + useEffect(() => { + if (!editorRef) return; + const unsubscribe = editorRef.onStateChange(updateActiveStates); + updateActiveStates(); + return () => unsubscribe(); + }, [editorRef, updateActiveStates]); + + useOutsideClickDetector(colorPaletteRef, () => setShowColorPalette(false)); + + return ( +
+
+ {/* color palette */} + {showColorPalette && } + + +
+
+ {Object.keys(toolbarItems).map((key) => ( +
+ {toolbarItems[key].map((item) => { + const isItemActive = activeStates[item.renderKey]; + + return ( + + {item.name} + {item.shortcut && {item.shortcut.join(" + ")}} +

+ } + > + +
+ ); + })} +
+ ))} +
+
+
+ {/* delete action */} + +
+ ); +}; diff --git a/web/core/components/home/home-dashboard-widgets.tsx b/web/core/components/home/home-dashboard-widgets.tsx index 6accd1853f3..f352a23f496 100644 --- a/web/core/components/home/home-dashboard-widgets.tsx +++ b/web/core/components/home/home-dashboard-widgets.tsx @@ -6,7 +6,7 @@ import { THomeWidgetKeys, THomeWidgetProps } from "@plane/types"; import { useHome } from "@/hooks/store/use-home"; // components import { HomePageHeader } from "@/plane-web/components/home/header"; -import { StickiesWidget } from "@/plane-web/components/stickies"; +import { StickiesWidget } from "../stickies"; import { RecentActivityWidget } from "./widgets"; import { DashboardQuickLinks } from "./widgets/links"; import { ManageWidgetsModal } from "./widgets/manage"; diff --git a/web/core/components/stickies/action-bar.tsx b/web/core/components/stickies/action-bar.tsx new file mode 100644 index 00000000000..6bbdd907ff0 --- /dev/null +++ b/web/core/components/stickies/action-bar.tsx @@ -0,0 +1,129 @@ +import { useRef, useState } from "react"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import useSWR from "swr"; +import { Plus, StickyNote as StickyIcon, X } from "lucide-react"; +import { useOutsideClickDetector } from "@plane/hooks"; +import { RecentStickyIcon, StickyNoteIcon, Tooltip } from "@plane/ui"; +import { cn } from "@plane/utils"; +import { useCommandPalette } from "@/hooks/store"; +import { useSticky } from "@/hooks/use-stickies"; +import { AllStickiesModal } from "./modal"; +import { StickyNote } from "./sticky"; + +export const StickyActionBar = observer(() => { + const { workspaceSlug } = useParams(); + const [isExpanded, setIsExpanded] = useState(false); + const [newSticky, setNewSticky] = useState(false); + const [showRecentSticky, setShowRecentSticky] = useState(false); + const ref = useRef(null); + + // hooks + const { stickies, activeStickyId, recentStickyId, updateActiveStickyId, fetchRecentSticky, toggleShowNewSticky } = + useSticky(); + const { toggleAllStickiesModal, allStickiesModal } = useCommandPalette(); + + useSWR( + workspaceSlug ? `WORKSPACE_STICKIES_RECENT_${workspaceSlug}` : null, + workspaceSlug ? () => fetchRecentSticky(workspaceSlug.toString()) : null + ); + + useOutsideClickDetector(ref, () => { + setNewSticky(false); + setShowRecentSticky(false); + setIsExpanded(false); + }); + + return ( +
+
+ + + + {recentStickyId && ( + + +
+
+ } + isMobile={false} + position="left" + disabled={showRecentSticky} + > + +
+ )} + + + +
+ + + +
+ {(newSticky || (showRecentSticky && recentStickyId)) && ( + (newSticky ? setNewSticky(false) : setShowRecentSticky(false))} + workspaceSlug={workspaceSlug.toString()} + stickyId={newSticky ? activeStickyId : recentStickyId || ""} + /> + )} +
+ + toggleAllStickiesModal(false)} /> +
+ ); +}); diff --git a/web/core/components/stickies/empty.tsx b/web/core/components/stickies/empty.tsx new file mode 100644 index 00000000000..4413ab570f9 --- /dev/null +++ b/web/core/components/stickies/empty.tsx @@ -0,0 +1,40 @@ +import { Plus, StickyNote as StickyIcon, X } from "lucide-react"; + +type TProps = { + handleCreate: () => void; + creatingSticky?: boolean; +}; +export const EmptyState = (props: TProps) => { + const { handleCreate, creatingSticky } = props; + return ( +
+
+
+ +
+
No stickies yet
+
+ All your stickies in this workspace will appear here. +
+ +
+
+ ); +}; diff --git a/web/core/components/stickies/index.ts b/web/core/components/stickies/index.ts new file mode 100644 index 00000000000..1376a85eb5f --- /dev/null +++ b/web/core/components/stickies/index.ts @@ -0,0 +1,2 @@ +export * from "./action-bar"; +export * from "./widget"; diff --git a/web/core/components/stickies/modal/index.tsx b/web/core/components/stickies/modal/index.tsx new file mode 100644 index 00000000000..8a91bd113c3 --- /dev/null +++ b/web/core/components/stickies/modal/index.tsx @@ -0,0 +1,15 @@ +import { EModalWidth, ModalCore } from "@plane/ui"; +import { Stickies } from "./stickies"; + +type TProps = { + isOpen: boolean; + handleClose: () => void; +}; +export const AllStickiesModal = (props: TProps) => { + const { isOpen, handleClose } = props; + return ( + + + + ); +}; diff --git a/web/core/components/stickies/modal/search.tsx b/web/core/components/stickies/modal/search.tsx new file mode 100644 index 00000000000..34c0a8f6d1b --- /dev/null +++ b/web/core/components/stickies/modal/search.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { FC, useRef, useState } from "react"; +import { observer } from "mobx-react"; +import { Search, X } from "lucide-react"; +// plane hooks +import { useOutsideClickDetector } from "@plane/hooks"; +// helpers +import { cn } from "@/helpers/common.helper"; +import { useSticky } from "@/hooks/use-stickies"; + +export const StickySearch: FC = observer(() => { + // hooks + const { searchQuery, updateSearchQuery } = 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); + } + }; + + return ( +
+ {!isSearchOpen && ( + + )} +
+ + updateSearchQuery(e.target.value)} + onKeyDown={handleInputKeyDown} + /> + {isSearchOpen && ( + + )} +
+
+ ); +}); diff --git a/web/core/components/stickies/modal/stickies.tsx b/web/core/components/stickies/modal/stickies.tsx new file mode 100644 index 00000000000..e11d160b1a6 --- /dev/null +++ b/web/core/components/stickies/modal/stickies.tsx @@ -0,0 +1,68 @@ +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import { Plus, X } from "lucide-react"; +import { RecentStickyIcon } from "@plane/ui"; +import { STICKY_COLORS } from "../../editor/sticky-editor/color-pallete"; +import { StickiesLayout } from "../stickies-layout"; +import { useStickyOperations } from "../sticky/use-operations"; +import { StickySearch } from "./search"; +import { useSticky } from "@/hooks/use-stickies"; + +type TProps = { + handleClose?: () => void; +}; + +export const Stickies = observer((props: TProps) => { + const { handleClose } = props; + const { workspaceSlug } = useParams(); + const { creatingSticky, toggleShowNewSticky } = useSticky(); + const { stickyOperations } = useStickyOperations({ workspaceSlug: workspaceSlug?.toString() }); + + return ( +
+ {/* header */} +
+ {/* Title */} +
+ +

My Stickies

+
+ {/* actions */} +
+ + + {handleClose && ( + + )} +
+
+ {/* content */} +
+ +
+
+ ); +}); diff --git a/web/core/components/stickies/stickies-layout.tsx b/web/core/components/stickies/stickies-layout.tsx new file mode 100644 index 00000000000..5ceecc4431a --- /dev/null +++ b/web/core/components/stickies/stickies-layout.tsx @@ -0,0 +1,209 @@ +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 { 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 [containerHeight, setContainerHeight] = useState(0); + const [showAllStickies, setShowAllStickies] = useState(false); + 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]); + + // 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) { + 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 ( +
+
+ {/* @ts-expect-error type mismatch here */} + {childElements} +
+ {containerHeight > 632.9 && ( +
+ +
+ )} +
+ ); +}); + +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/sticky/index.ts b/web/core/components/stickies/sticky/index.ts new file mode 100644 index 00000000000..1efe34c51ec --- /dev/null +++ b/web/core/components/stickies/sticky/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/web/core/components/stickies/sticky/inputs.tsx b/web/core/components/stickies/sticky/inputs.tsx new file mode 100644 index 00000000000..ce29411c3f5 --- /dev/null +++ b/web/core/components/stickies/sticky/inputs.tsx @@ -0,0 +1,110 @@ +import { useCallback, useEffect, useRef } from "react"; +import { DebouncedFunc } from "lodash"; +import { Controller, useForm } from "react-hook-form"; +import { EditorRefApi } from "@plane/editor"; +import { TSticky } from "@plane/types"; +import { TextArea } from "@plane/ui"; +import { useWorkspace } from "@/hooks/store"; +import { TProject } from "@/plane-web/types"; +import { StickyEditor } from "../../editor"; + +type TProps = { + stickyData: TSticky | undefined; + workspaceSlug: string; + handleUpdate: DebouncedFunc<(payload: Partial) => Promise>; + stickyId: string | undefined; + handleChange: (data: Partial) => Promise; + handleDelete: () => Promise; +}; +export const StickyInput = (props: TProps) => { + const { stickyData, workspaceSlug, handleUpdate, stickyId, handleDelete, handleChange } = props; + //refs + const editorRef = useRef(null); + // store hooks + const { getWorkspaceBySlug } = useWorkspace(); + // form info + const { handleSubmit, reset, control } = useForm({ + defaultValues: { + description_html: stickyData?.description_html, + name: stickyData?.name, + }, + }); + + // computed values + const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id as string; + + // reset form values + useEffect(() => { + if (!stickyId) return; + reset({ + id: stickyId, + description_html: stickyData?.description_html === "" ? "

" : stickyData?.description_html, + name: stickyData?.name, + }); + }, [stickyData, reset]); + + const handleFormSubmit = useCallback( + async (formdata: Partial) => { + if (formdata.name !== undefined) { + await handleUpdate({ + description_html: formdata.description_html ?? "

", + name: formdata.name, + }); + } else { + await handleUpdate({ + description_html: formdata.description_html ?? "

", + }); + } + }, + [handleUpdate, workspaceSlug] + ); + + return ( +
+ {/* name */} + ( +