diff --git a/apiserver/plane/app/views/workspace/sticky.py b/apiserver/plane/app/views/workspace/sticky.py index 98844fb4917..4870a6abe3e 100644 --- a/apiserver/plane/app/views/workspace/sticky.py +++ b/apiserver/plane/app/views/workspace/sticky.py @@ -39,9 +39,9 @@ def create(self, request, slug): ) def list(self, request, slug): query = request.query_params.get("query", False) - stickies = self.get_queryset() + stickies = self.get_queryset().order_by("-sort_order") if query: - stickies = stickies.filter(name__icontains=query) + stickies = stickies.filter(description_stripped__icontains=query) return self.paginate( request=request, @@ -49,7 +49,7 @@ def list(self, request, slug): 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/apiserver/plane/db/models/sticky.py b/apiserver/plane/db/models/sticky.py index a0590306f69..34f37b81ecd 100644 --- a/apiserver/plane/db/models/sticky.py +++ b/apiserver/plane/db/models/sticky.py @@ -5,6 +5,9 @@ # Module imports from .base import BaseModel +# Third party imports +from plane.utils.html_processor import strip_tags + class Sticky(BaseModel): name = models.TextField(null=True, blank=True) @@ -33,6 +36,12 @@ class Meta: ordering = ("-created_at",) def save(self, *args, **kwargs): + # Strip the html tags using html parser + self.description_stripped = ( + None + if (self.description_html == "" or self.description_html is None) + else strip_tags(self.description_html) + ) if self._state.adding: # Get the maximum sequence value from the database last_id = Sticky.objects.filter(workspace=self.workspace).aggregate( diff --git a/packages/constants/src/index.ts b/packages/constants/src/index.ts index 09f038c3371..2b3964ae9fd 100644 --- a/packages/constants/src/index.ts +++ b/packages/constants/src/index.ts @@ -13,3 +13,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/packages/editor/src/styles/editor.css b/packages/editor/src/styles/editor.css index e263431f6f2..0cc43376442 100644 --- a/packages/editor/src/styles/editor.css +++ b/packages/editor/src/styles/editor.css @@ -145,7 +145,7 @@ ul[data-type="taskList"] li > label input[type="checkbox"] { position: relative; -webkit-appearance: none; appearance: none; - background-color: rgb(var(--color-background-100)); + background-color: transparent; margin: 0; cursor: pointer; width: 0.8rem; diff --git a/packages/editor/src/styles/variables.css b/packages/editor/src/styles/variables.css index d25500692bb..eace3cfc2fc 100644 --- a/packages/editor/src/styles/variables.css +++ b/packages/editor/src/styles/variables.css @@ -1,41 +1,3 @@ -:root { - /* text colors */ - --editor-colors-gray-text: #5c5e63; - --editor-colors-peach-text: #ff5b59; - --editor-colors-pink-text: #f65385; - --editor-colors-orange-text: #fd9038; - --editor-colors-green-text: #0fc27b; - --editor-colors-light-blue-text: #17bee9; - --editor-colors-dark-blue-text: #266df0; - --editor-colors-purple-text: #9162f9; - /* end text colors */ -} - -/* text background colors */ -[data-theme="light"], -[data-theme="light-contrast"] { - --editor-colors-gray-background: #d6d6d8; - --editor-colors-peach-background: #ffd5d7; - --editor-colors-pink-background: #fdd4e3; - --editor-colors-orange-background: #ffe3cd; - --editor-colors-green-background: #c3f0de; - --editor-colors-light-blue-background: #c5eff9; - --editor-colors-dark-blue-background: #c9dafb; - --editor-colors-purple-background: #e3d8fd; -} -[data-theme="dark"], -[data-theme="dark-contrast"] { - --editor-colors-gray-background: #404144; - --editor-colors-peach-background: #593032; - --editor-colors-pink-background: #562e3d; - --editor-colors-orange-background: #583e2a; - --editor-colors-green-background: #1d4a3b; - --editor-colors-light-blue-background: #1f495c; - --editor-colors-dark-blue-background: #223558; - --editor-colors-purple-background: #3d325a; -} -/* end text background colors */ - .editor-container { /* font sizes and line heights */ &.large-font { diff --git a/packages/types/src/stickies.d.ts b/packages/types/src/stickies.d.ts index 55f8b23c587..ffa19e84faf 100644 --- a/packages/types/src/stickies.d.ts +++ b/packages/types/src/stickies.d.ts @@ -1,8 +1,16 @@ +import { TLogoProps } from "./common"; + export type TSticky = { + created_at?: string | undefined; + created_by?: string | undefined; + background_color?: string | null | undefined; + description?: object | undefined; + description_html?: string | undefined; id: string; + logo_props: TLogoProps | undefined; name?: string; - description_html?: string; - color?: string; - createdAt?: Date; - updatedAt?: Date; + sort_order: number | undefined; + updated_at?: string | undefined; + updated_by?: string | undefined; + workspace: string | undefined; }; diff --git a/web/app/[workspaceSlug]/(projects)/stickies/header.tsx b/web/app/[workspaceSlug]/(projects)/stickies/header.tsx new file mode 100644 index 00000000000..9e3f1a45d5b --- /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"; + +// 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 { creatingSticky, 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..48c2cc37458 --- /dev/null +++ b/web/app/[workspaceSlug]/(projects)/stickies/page.tsx @@ -0,0 +1,16 @@ +"use client"; + +// components +import { PageHead } from "@/components/core"; +import { StickiesInfinite } from "@/components/stickies"; + +export default function WorkspaceStickiesPage() { + return ( + <> + +
+ +
+ + ); +} diff --git a/web/core/components/core/content-overflow-HOC.tsx b/web/core/components/core/content-overflow-HOC.tsx index f6a2423cd61..1b37b0429af 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; + customButton?: ReactNode; } 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, + customButton, } = props; // states @@ -131,16 +133,18 @@ export const ContentOverflowWrapper = observer((props: IContentOverflowWrapper) pointerEvents: isTransitioning ? "none" : "auto", }} > - + {customButton || ( + + )} )} diff --git a/web/core/components/editor/sticky-editor/color-palette.tsx b/web/core/components/editor/sticky-editor/color-palette.tsx new file mode 100644 index 00000000000..f6586121afc --- /dev/null +++ b/web/core/components/editor/sticky-editor/color-palette.tsx @@ -0,0 +1,78 @@ +import { TSticky } from "@plane/types"; + +export const STICKY_COLORS_LIST: { + key: string; + label: string; + backgroundColor: string; +}[] = [ + { + key: "gray", + label: "Gray", + backgroundColor: "var(--editor-colors-gray-background)", + }, + { + key: "peach", + label: "Peach", + backgroundColor: "var(--editor-colors-peach-background)", + }, + { + key: "pink", + label: "Pink", + backgroundColor: "var(--editor-colors-pink-background)", + }, + { + key: "orange", + label: "Orange", + backgroundColor: "var(--editor-colors-orange-background)", + }, + { + key: "green", + label: "Green", + backgroundColor: "var(--editor-colors-green-background)", + }, + { + key: "light-blue", + label: "Light blue", + backgroundColor: "var(--editor-colors-light-blue-background)", + }, + { + key: "dark-blue", + label: "Dark blue", + backgroundColor: "var(--editor-colors-dark-blue-background)", + }, + { + key: "purple", + label: "Purple", + backgroundColor: "var(--editor-colors-purple-background)", + }, +]; + +type TProps = { + handleUpdate: (data: Partial) => Promise; +}; + +export const ColorPalette = (props: TProps) => { + const { handleUpdate } = props; + return ( +
+
Background colors
+
+ {STICKY_COLORS_LIST.map((color) => ( +
+
+ ); +}; diff --git a/web/core/components/editor/sticky-editor/color-pallete.tsx b/web/core/components/editor/sticky-editor/color-pallete.tsx deleted file mode 100644 index 3060650f7d3..00000000000 --- a/web/core/components/editor/sticky-editor/color-pallete.tsx +++ /dev/null @@ -1,36 +0,0 @@ -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 index 3dad67477f7..376a317c8c6 100644 --- a/web/core/components/editor/sticky-editor/editor.tsx +++ b/web/core/components/editor/sticky-editor/editor.tsx @@ -82,26 +82,28 @@ export const StickyEditor = React.forwardRef -
- { - // 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} - /> -
+ {showToolbar && ( +
+ { + // 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} + /> +
+ )} ); }); diff --git a/web/core/components/editor/sticky-editor/toolbar.tsx b/web/core/components/editor/sticky-editor/toolbar.tsx index c1686b44699..8f6aa50cee4 100644 --- a/web/core/components/editor/sticky-editor/toolbar.tsx +++ b/web/core/components/editor/sticky-editor/toolbar.tsx @@ -12,7 +12,7 @@ import { Tooltip } from "@plane/ui"; import { TOOLBAR_ITEMS, ToolbarMenuItem } from "@/constants/editor"; // helpers import { cn } from "@/helpers/common.helper"; -import { ColorPalette } from "./color-pallete"; +import { ColorPalette } from "./color-palette"; type Props = { executeCommand: (item: ToolbarMenuItem) => void; diff --git a/web/core/components/empty-state/empty-state.tsx b/web/core/components/empty-state/empty-state.tsx index 883faab3884..faab4ebc290 100644 --- a/web/core/components/empty-state/empty-state.tsx +++ b/web/core/components/empty-state/empty-state.tsx @@ -8,7 +8,7 @@ import Link from "next/link"; import { useTheme } from "next-themes"; // hooks // components -import { Button, TButtonVariant } from "@plane/ui"; +import { Button, TButtonSizes, TButtonVariant } from "@plane/ui"; // constant import { EMPTY_STATE_DETAILS, EmptyStateType } from "@/constants/empty-state"; // helpers @@ -18,10 +18,14 @@ import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; import { ComicBoxButton } from "./comic-box-button"; export type EmptyStateProps = { + size?: TButtonSizes; type: EmptyStateType; - size?: "sm" | "md" | "lg"; layout?: "screen-detailed" | "screen-simple"; additionalPath?: string; + primaryButtonConfig?: { + size?: TButtonSizes; + variant?: TButtonVariant; + }; primaryButtonOnClick?: () => void; primaryButtonLink?: string; secondaryButtonOnClick?: () => void; @@ -29,10 +33,14 @@ export type EmptyStateProps = { export const EmptyState: React.FC = observer((props) => { const { - type, size = "lg", + type, layout = "screen-detailed", additionalPath = "", + primaryButtonConfig = { + size: "lg", + variant: "primary", + }, primaryButtonOnClick, primaryButtonLink, secondaryButtonOnClick, @@ -67,8 +75,8 @@ export const EmptyState: React.FC = observer((props) => { if (!primaryButton) return null; const commonProps = { - size: size, - variant: "primary" as TButtonVariant, + size: primaryButtonConfig.size, + variant: primaryButtonConfig.variant, prependIcon: primaryButton.icon, onClick: primaryButtonOnClick ? primaryButtonOnClick : undefined, disabled: !isEditingAllowed, @@ -145,12 +153,10 @@ export const EmptyState: React.FC = observer((props) => { )} {anyButton && ( - <> -
- {renderPrimaryButton()} - {renderSecondaryButton()} -
- +
+ {renderPrimaryButton()} + {renderSecondaryButton()} +
)} @@ -175,6 +181,12 @@ export const EmptyState: React.FC = observer((props) => { ) : (

{title}

)} + {anyButton && ( +
+ {renderPrimaryButton()} + {renderSecondaryButton()} +
+ )} )} diff --git a/web/core/components/home/home-dashboard-widgets.tsx b/web/core/components/home/home-dashboard-widgets.tsx index a1186106fa9..009cb9ffe74 100644 --- a/web/core/components/home/home-dashboard-widgets.tsx +++ b/web/core/components/home/home-dashboard-widgets.tsx @@ -1,54 +1,96 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -// types +// plane types import { THomeWidgetKeys, THomeWidgetProps } from "@plane/types"; +// components +import { EmptyState } from "@/components/empty-state"; +// constants +import { EmptyStateType } from "@/constants/empty-state"; // hooks import { useHome } from "@/hooks/store/use-home"; -// components +// plane web components import { HomePageHeader } from "@/plane-web/components/home/header"; import { StickiesWidget } from "../stickies"; import { RecentActivityWidget } from "./widgets"; import { DashboardQuickLinks } from "./widgets/links"; import { ManageWidgetsModal } from "./widgets/manage"; -const WIDGETS_LIST: { - [key in THomeWidgetKeys]: { component: React.FC | null; fullWidth: boolean }; +export const HOME_WIDGETS_LIST: { + [key in THomeWidgetKeys]: { + component: React.FC | null; + fullWidth: boolean; + title: string; + }; } = { - quick_links: { component: DashboardQuickLinks, fullWidth: false }, - recents: { component: RecentActivityWidget, fullWidth: false }, - my_stickies: { component: StickiesWidget, fullWidth: false }, - new_at_plane: { component: null, fullWidth: false }, - quick_tutorial: { component: null, fullWidth: false }, + quick_links: { + component: DashboardQuickLinks, + fullWidth: false, + title: "Quick links", + }, + recents: { + component: RecentActivityWidget, + fullWidth: false, + title: "Recents", + }, + my_stickies: { + component: StickiesWidget, + fullWidth: false, + title: "Your stickies", + }, + new_at_plane: { + component: null, + fullWidth: false, + title: "New at Plane", + }, + quick_tutorial: { + component: null, + fullWidth: false, + title: "Quick tutorial", + }, }; export const DashboardWidgets = observer(() => { // router const { workspaceSlug } = useParams(); // store hooks - const { toggleWidgetSettings, widgetsMap, showWidgetSettings, orderedWidgets } = useHome(); + const { toggleWidgetSettings, widgetsMap, showWidgetSettings, orderedWidgets, isAnyWidgetEnabled } = useHome(); if (!workspaceSlug) return null; return ( -
+
toggleWidgetSettings(false)} /> -
- {orderedWidgets.map((key) => { - const WidgetComponent = WIDGETS_LIST[key]?.component; - const isEnabled = widgetsMap[key]?.is_enabled; - if (!WidgetComponent || !isEnabled) return null; - return ( -
- -
- ); - })} -
+ {isAnyWidgetEnabled ? ( +
+ {orderedWidgets.map((key) => { + const WidgetComponent = HOME_WIDGETS_LIST[key]?.component; + const isEnabled = widgetsMap[key]?.is_enabled; + if (!WidgetComponent || !isEnabled) return null; + return ( +
+ +
+ ); + })} +
+ ) : ( +
+ toggleWidgetSettings(true)} + primaryButtonConfig={{ + size: "sm", + variant: "neutral-primary", + }} + /> +
+ )}
); }); diff --git a/web/core/components/home/root.tsx b/web/core/components/home/root.tsx index 2f6df1fe6f8..c662b26acc9 100644 --- a/web/core/components/home/root.tsx +++ b/web/core/components/home/root.tsx @@ -59,12 +59,11 @@ export const WorkspaceHomeView = observer(() => { <> = 768, })} > {currentUser && toggleWidgetSettings(true)} />} - diff --git a/web/core/components/home/user-greetings.tsx b/web/core/components/home/user-greetings.tsx index d9e68880123..82a94cc8062 100644 --- a/web/core/components/home/user-greetings.tsx +++ b/web/core/components/home/user-greetings.tsx @@ -1,9 +1,11 @@ import { FC } from "react"; -// hooks import { Shapes } from "lucide-react"; +// plane types import { IUser } from "@plane/types"; +// plane ui +import { Button } from "@plane/ui"; +// hooks import { useCurrentTime } from "@/hooks/use-current-time"; -// types export interface IUserGreetingsView { user: IUser; @@ -51,13 +53,10 @@ export const UserGreetingsView: FC = (props) => {
- + ); }; diff --git a/web/core/components/home/widgets/empty-states/links.tsx b/web/core/components/home/widgets/empty-states/links.tsx index 00e91274ca3..db8b05df754 100644 --- a/web/core/components/home/widgets/empty-states/links.tsx +++ b/web/core/components/home/widgets/empty-states/links.tsx @@ -1,27 +1,12 @@ -import { Link2, Plus } from "lucide-react"; -import { Button } from "@plane/ui"; +import { Link2 } from "lucide-react"; -type TProps = { - handleCreate: () => void; -}; -export const LinksEmptyState = (props: TProps) => { - const { handleCreate } = props; - return ( -
-
-
- +export const LinksEmptyState = () => ( +
+
+ +
+ Add any links you need for quick access to your work.
-
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 index 5306584fd05..dbd91b33a8c 100644 --- a/web/core/components/home/widgets/empty-states/recents.tsx +++ b/web/core/components/home/widgets/empty-states/recents.tsx @@ -1,15 +1,38 @@ -import { History } from "lucide-react"; +import { Briefcase, FileText, History } from "lucide-react"; +import { LayersIcon } from "@plane/ui"; -export const RecentsEmptyState = () => ( -
-
-
- +export const RecentsEmptyState = ({ type }: { type: string }) => { + const getDisplayContent = () => { + switch (type) { + case "project": + return { + icon: , + text: "Your recent projects will appear here once you visit one.", + }; + case "page": + return { + icon: , + text: "Your recent pages will appear here once you visit one.", + }; + case "issue": + return { + icon: , + text: "Your recent issues will appear here once you visit one.", + }; + default: + return { + icon: , + text: "You don’t have any recent items yet.", + }; + } + }; + const { icon, text } = getDisplayContent(); + + return ( +
+
+ {icon}
{text}
-
No recent items yet
-
You don’t have any recent items yet.
-
-); + ); +}; diff --git a/web/core/components/home/widgets/empty-states/root.tsx b/web/core/components/home/widgets/empty-states/root.tsx index 06606f367d0..b359f2bb486 100644 --- a/web/core/components/home/widgets/empty-states/root.tsx +++ b/web/core/components/home/widgets/empty-states/root.tsx @@ -2,17 +2,22 @@ import React from "react"; import Link from "next/link"; import { useParams } from "next/navigation"; import { Briefcase, Hotel, Users } from "lucide-react"; +// helpers import { getFileURL } from "@/helpers/file.helper"; +// hooks import { useCommandPalette, useEventTracker, useUser, useUserPermissions } from "@/hooks/store"; +// plane web constants import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants"; export const EmptyWorkspace = () => { + // navigation const { workspaceSlug } = useParams(); + // store hooks const { allowPermissions } = useUserPermissions(); const { toggleCreateProjectModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); const { data: currentUser } = useUser(); - + // derived values const canCreateProject = allowPermissions( [EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.WORKSPACE @@ -83,6 +88,7 @@ export const EmptyWorkspace = () => { }, }, ]; + return (
{EMPTY_STATE_DATA.map((item) => ( diff --git a/web/core/components/home/widgets/empty-states/stickies.tsx b/web/core/components/home/widgets/empty-states/stickies.tsx new file mode 100644 index 00000000000..657359c28d0 --- /dev/null +++ b/web/core/components/home/widgets/empty-states/stickies.tsx @@ -0,0 +1,13 @@ +// plane ui +import { RecentStickyIcon } from "@plane/ui"; + +export const StickiesEmptyState = () => ( +
+
+ +
+ No stickies yet. Add one to start making quick notes. +
+
+
+); diff --git a/web/core/components/home/widgets/links/link-detail.tsx b/web/core/components/home/widgets/links/link-detail.tsx index 26aa4f9c8b4..72e3bfded88 100644 --- a/web/core/components/home/widgets/links/link-detail.tsx +++ b/web/core/components/home/widgets/links/link-detail.tsx @@ -4,12 +4,11 @@ import { FC } from "react"; // hooks // ui import { observer } from "mobx-react"; -import { Pencil, Trash2, ExternalLink, EllipsisVertical, Link, Link2 } from "lucide-react"; +import { Pencil, Trash2, ExternalLink, EllipsisVertical, Link2, Link } from "lucide-react"; import { TOAST_TYPE, setToast, CustomMenu, TContextMenuItem } from "@plane/ui"; // helpers -import { cn } from "@plane/utils"; +import { cn, copyTextToClipboard } from "@plane/utils"; import { calculateTimeAgo } from "@/helpers/date-time.helper"; -import { copyUrlToClipboard } from "@/helpers/string.helper"; import { useHome } from "@/hooks/store/use-home"; import { TLinkOperations } from "./use-links"; @@ -37,7 +36,7 @@ export const ProjectLinkDetail: FC = observer((props) => { }; const handleCopyText = () => - copyUrlToClipboard(viewLink).then(() => { + copyTextToClipboard(viewLink).then(() => { setToast({ type: TOAST_TYPE.SUCCESS, title: "Link Copied!", @@ -74,7 +73,10 @@ export const ProjectLinkDetail: FC = observer((props) => { ]; return ( -
+
diff --git a/web/core/components/home/widgets/links/links.tsx b/web/core/components/home/widgets/links/links.tsx index 1fd5dea9f7b..415183d44e3 100644 --- a/web/core/components/home/widgets/links/links.tsx +++ b/web/core/components/home/widgets/links/links.tsx @@ -20,14 +20,14 @@ export const ProjectLinkList: FC = observer((props) => { const { linkOperations, workspaceSlug } = props; // hooks const { - quickLinks: { getLinksByWorkspaceId, toggleLinkModal }, + quickLinks: { getLinksByWorkspaceId }, } = useHome(); const links = getLinksByWorkspaceId(workspaceSlug); if (links === undefined) return ; - if (links.length === 0) return toggleLinkModal(true)} />; + if (links.length === 0) return ; return (
diff --git a/web/core/components/home/widgets/links/use-links.tsx b/web/core/components/home/widgets/links/use-links.tsx index fe107fd06a9..a6668d92efc 100644 --- a/web/core/components/home/widgets/links/use-links.tsx +++ b/web/core/components/home/widgets/links/use-links.tsx @@ -32,7 +32,6 @@ export const useLinks = (workspaceSlug: string) => { create: async (data: Partial) => { try { if (!workspaceSlug) throw new Error("Missing required fields"); - console.log("data", data, workspaceSlug); await createLink(workspaceSlug, data); setToast({ message: "The link has been successfully created", diff --git a/web/core/components/home/widgets/manage/widget-item.tsx b/web/core/components/home/widgets/manage/widget-item.tsx index 453a96588c0..bfd9ca38812 100644 --- a/web/core/components/home/widgets/manage/widget-item.tsx +++ b/web/core/components/home/widgets/manage/widget-item.tsx @@ -11,18 +11,18 @@ import { import { pointerOutsideOfPreview } from "@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview"; import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview"; import { attachInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item"; - import { observer } from "mobx-react"; -// plane helpers import { useParams } from "next/navigation"; import { createRoot } from "react-dom/client"; -// ui +// plane types import { InstructionType, TWidgetEntityData } from "@plane/types"; -// components +// plane ui import { DropIndicator, ToggleSwitch } from "@plane/ui"; -// helpers +// plane utils import { cn } from "@plane/utils"; +// hooks import { useHome } from "@/hooks/store/use-home"; +import { HOME_WIDGETS_LIST } from "../../home-dashboard-widgets"; import { WidgetItemDragHandle } from "./widget-item-drag-handle"; import { getCanDrop, getInstructionFromPayload } from "./widget.helpers"; @@ -46,6 +46,7 @@ export const WidgetItem: FC = observer((props) => { const { widgetsMap } = useHome(); // derived values const widget = widgetsMap[widgetId] as TWidgetEntityData; + const widgetTitle = HOME_WIDGETS_LIST[widget.key]?.title; // drag and drop useEffect(() => { @@ -119,7 +120,7 @@ export const WidgetItem: FC = observer((props) => {
= observer((props) => { >
-
{widget.key.replaceAll("_", " ")}
+
{widgetTitle}
= observer((props)
- +
); diff --git a/web/core/components/stickies/action-bar.tsx b/web/core/components/stickies/action-bar.tsx index 6bbdd907ff0..9b3789ea2ba 100644 --- a/web/core/components/stickies/action-bar.tsx +++ b/web/core/components/stickies/action-bar.tsx @@ -3,25 +3,37 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import useSWR from "swr"; import { Plus, StickyNote as StickyIcon, X } from "lucide-react"; +// plane hooks import { useOutsideClickDetector } from "@plane/hooks"; +// plane ui import { RecentStickyIcon, StickyNoteIcon, Tooltip } from "@plane/ui"; +// plane utils import { cn } from "@plane/utils"; +// hooks import { useCommandPalette } from "@/hooks/store"; import { useSticky } from "@/hooks/use-stickies"; +// components +import { STICKY_COLORS_LIST } from "../editor/sticky-editor/color-palette"; import { AllStickiesModal } from "./modal"; import { StickyNote } from "./sticky"; export const StickyActionBar = observer(() => { - const { workspaceSlug } = useParams(); + // states const [isExpanded, setIsExpanded] = useState(false); const [newSticky, setNewSticky] = useState(false); const [showRecentSticky, setShowRecentSticky] = useState(false); + // navigation + const { workspaceSlug } = useParams(); + // refs const ref = useRef(null); - - // hooks + // store hooks const { stickies, activeStickyId, recentStickyId, updateActiveStickyId, fetchRecentSticky, toggleShowNewSticky } = useSticky(); const { toggleAllStickiesModal, allStickiesModal } = useCommandPalette(); + // derived values + const recentStickyBackgroundColor = recentStickyId + ? STICKY_COLORS_LIST.find((c) => c.key === stickies[recentStickyId].background_color)?.backgroundColor + : STICKY_COLORS_LIST[0].backgroundColor; useSWR( workspaceSlug ? `WORKSPACE_STICKIES_RECENT_${workspaceSlug}` : null, @@ -63,7 +75,7 @@ export const StickyActionBar = observer(() => {
@@ -75,9 +87,9 @@ export const StickyActionBar = observer(() => { )} diff --git a/web/core/components/stickies/empty.tsx b/web/core/components/stickies/empty.tsx deleted file mode 100644 index 4a407a96957..00000000000 --- a/web/core/components/stickies/empty.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Plus, StickyNote as StickyIcon } from "lucide-react"; -import { Button } from "@plane/ui"; - -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 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..a81cb44766f --- /dev/null +++ b/web/core/components/stickies/layout/stickies-infinite.tsx @@ -0,0 +1,62 @@ +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, getWorkspaceStickyIds, 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; + fetchNextWorkspaceStickies(workspaceSlug?.toString()); + }; + + const hasNextPage = paginationInfo?.next_page_results && paginationInfo?.next_cursor !== undefined; + const shouldObserve = hasNextPage && loader !== "pagination"; + const workspaceStickies = getWorkspaceStickyIds(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..4da6efe7b03 --- /dev/null +++ b/web/core/components/stickies/layout/stickies-list.tsx @@ -0,0 +1,168 @@ +import { useEffect, useRef, useState } from "react"; +import type { + DropTargetRecord, + DragLocationHistory, +} from "@atlaskit/pragmatic-drag-and-drop/dist/types/internal-types"; +import type { ElementDragPayload } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; +import { observer } from "mobx-react"; +import { usePathname } from "next/navigation"; +import Masonry from "react-masonry-component"; +// plane ui +import { Loader } from "@plane/ui"; +// components +import { EmptyState } from "@/components/empty-state"; +// constants +import { EmptyStateType } from "@/constants/empty-state"; +// hooks +import { useSticky } from "@/hooks/use-stickies"; +import { useStickyOperations } from "../sticky/use-operations"; +import { StickyDNDWrapper } from "./sticky-dnd-wrapper"; +import { getInstructionFromPayload } from "./sticky.helpers"; +import { StickiesEmptyState } from "@/components/home/widgets/empty-states/stickies"; + +type TStickiesLayout = { + workspaceSlug: string; + intersectionElement?: React.ReactNode | null; +}; + +type TProps = TStickiesLayout & { + columnCount: number; +}; + +export const StickiesList = observer((props: TProps) => { + const { workspaceSlug, intersectionElement, columnCount } = props; + // navigation + const pathname = usePathname(); + // store hooks + const { getWorkspaceStickyIds, toggleShowNewSticky, searchQuery, loader } = useSticky(); + // sticky operations + const { stickyOperations } = useStickyOperations({ workspaceSlug: workspaceSlug?.toString() }); + // derived values + const workspaceStickyIds = getWorkspaceStickyIds(workspaceSlug?.toString()); + const itemWidth = `${100 / columnCount}%`; + const totalRows = Math.ceil(workspaceStickyIds.length / columnCount); + const isStickiesPage = pathname?.includes("stickies"); + + // Function to determine if an item is in first or last row + const getRowPositions = (index: number) => { + const currentRow = Math.floor(index / columnCount); + return { + isInFirstRow: currentRow === 0, + isInLastRow: currentRow === totalRows - 1 || index >= workspaceStickyIds.length - columnCount, + }; + }; + + const handleDrop = (self: DropTargetRecord, source: ElementDragPayload, location: DragLocationHistory) => { + const dropTargets = location?.current?.dropTargets ?? []; + if (!dropTargets || dropTargets.length <= 0) return; + + const dropTarget = dropTargets[0]; + if (!dropTarget?.data?.id || !source.data?.id) return; + + const instruction = getInstructionFromPayload(dropTarget, source, location); + const droppedId = dropTarget.data.id; + const sourceId = source.data.id; + + try { + if (!instruction || !droppedId || !sourceId) return; + stickyOperations.updatePosition(workspaceSlug, sourceId as string, droppedId as string, instruction); + } catch (error) { + console.error("Error reordering sticky:", error); + } + }; + + if (loader === "init-loader") { + return ( +
+ + + +
+ ); + } + + if (loader === "loaded" && workspaceStickyIds.length === 0) { + return ( +
+ {isStickiesPage ? ( + { + toggleShowNewSticky(true); + stickyOperations.create(); + }} + primaryButtonConfig={{ + size: "sm", + }} + /> + ) : ( + + )} +
+ ); + } + + return ( +
+ {/* @ts-expect-error type mismatch here */} + + {workspaceStickyIds.map((stickyId, index) => { + const { isInFirstRow, isInLastRow } = getRowPositions(index); + return ( + + ); + })} + {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..f538917b534 --- /dev/null +++ b/web/core/components/stickies/layout/stickies-truncated.tsx @@ -0,0 +1,44 @@ +import { observer } from "mobx-react"; +import Link from "next/link"; +import { useParams } from "next/navigation"; +import useSWR from "swr"; +// plane utils +import { cn } from "@plane/utils"; +// hooks +import { useSticky } from "@/hooks/use-stickies"; +// components +import { ContentOverflowWrapper } from "../../core/content-overflow-HOC"; +import { StickiesLayout } from "./stickies-list"; + +export const StickiesTruncated = observer(() => { + // navigation + const { workspaceSlug } = useParams(); + // store hooks + const { fetchWorkspaceStickies } = useSticky(); + + useSWR( + workspaceSlug ? `WORKSPACE_STICKIES_${workspaceSlug}` : null, + workspaceSlug ? () => fetchWorkspaceStickies(workspaceSlug.toString()) : null, + { revalidateIfStale: false, revalidateOnFocus: false } + ); + + return ( + + Show all + + } + > + + + ); +}); diff --git a/web/core/components/stickies/layout/sticky-dnd-wrapper.tsx b/web/core/components/stickies/layout/sticky-dnd-wrapper.tsx new file mode 100644 index 00000000000..bc775e06de3 --- /dev/null +++ b/web/core/components/stickies/layout/sticky-dnd-wrapper.tsx @@ -0,0 +1,137 @@ +import { useEffect, useRef, useState } from "react"; +import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; +import type { + DropTargetRecord, + DragLocationHistory, +} from "@atlaskit/pragmatic-drag-and-drop/dist/types/internal-types"; +import { + draggable, + dropTargetForElements, + ElementDragPayload, +} from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; +import { pointerOutsideOfPreview } from "@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview"; +import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview"; +import { attachInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item"; +import { observer } from "mobx-react"; +import { usePathname } from "next/navigation"; +import { createRoot } from "react-dom/client"; +import { InstructionType } from "@plane/types"; +import { DropIndicator } from "@plane/ui"; +import { cn } from "@plane/utils"; +import { StickyNote } from "../sticky"; +import { getInstructionFromPayload } from "./sticky.helpers"; + +// Draggable Sticky Wrapper Component +export const StickyDNDWrapper = observer( + ({ + stickyId, + workspaceSlug, + itemWidth, + isLastChild, + isInFirstRow, + isInLastRow, + handleDrop, + }: { + stickyId: string; + workspaceSlug: string; + itemWidth: string; + isLastChild: boolean; + isInFirstRow: boolean; + isInLastRow: boolean; + handleDrop: (self: DropTargetRecord, source: ElementDragPayload, location: DragLocationHistory) => void; + }) => { + const pathName = usePathname(); + const [isDragging, setIsDragging] = useState(false); + const [instruction, setInstruction] = useState(undefined); + const elementRef = useRef(null); + + useEffect(() => { + const element = elementRef.current; + if (!element) return; + + const initialData = { id: stickyId, type: "sticky" }; + + if (pathName.includes("stickies")) + return combine( + draggable({ + element, + dragHandle: element, + getInitialData: () => initialData, + onDragStart: () => { + setIsDragging(true); + }, + onDrop: () => { + setIsDragging(false); + }, + onGenerateDragPreview: ({ nativeSetDragImage }) => { + setCustomNativeDragPreview({ + getOffset: pointerOutsideOfPreview({ x: "-200px", y: "0px" }), + render: ({ container }) => { + const root = createRoot(container); + root.render( +
+
+ +
+
+ ); + return () => root.unmount(); + }, + nativeSetDragImage, + }); + }, + }), + dropTargetForElements({ + element, + canDrop: ({ source }) => source.data?.type === "sticky", + getData: ({ input, element }) => { + const blockedStates: InstructionType[] = ["make-child"]; + if (!isLastChild) { + blockedStates.push("reorder-below"); + } + + return attachInstruction(initialData, { + input, + element, + currentLevel: 1, + indentPerLevel: 0, + mode: isLastChild ? "last-in-group" : "standard", + block: blockedStates, + }); + }, + onDrag: ({ self, source, location }) => { + const instruction = getInstructionFromPayload(self, source, location); + setInstruction(instruction); + }, + onDragLeave: () => { + setInstruction(undefined); + }, + onDrop: ({ self, source, location }) => { + setInstruction(undefined); + handleDrop(self, source, location); + }, + }) + ); + }, [stickyId, isDragging]); + + return ( +
+ {!isInFirstRow && } +
+ +
+ {!isInLastRow && } +
+ ); + } +); diff --git a/web/core/components/stickies/layout/sticky.helpers.ts b/web/core/components/stickies/layout/sticky.helpers.ts new file mode 100644 index 00000000000..930eb10aa14 --- /dev/null +++ b/web/core/components/stickies/layout/sticky.helpers.ts @@ -0,0 +1,45 @@ +import { extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item"; +import { InstructionType, IPragmaticPayloadLocation, TDropTarget } from "@plane/types"; + +export type TargetData = { + id: string; + parentId: string | null; + isGroup: boolean; + isChild: boolean; +}; + +/** + * extracts the Payload and translates the instruction for the current dropTarget based on drag and drop payload + * @param dropTarget dropTarget for which the instruction is required + * @param source the dragging sticky data that is being dragged on the dropTarget + * @param location location includes the data of all the dropTargets the source is being dragged on + * @returns Instruction for dropTarget + */ +export const getInstructionFromPayload = ( + dropTarget: TDropTarget, + source: TDropTarget, + location: IPragmaticPayloadLocation +): InstructionType | undefined => { + const dropTargetData = dropTarget?.data as TargetData; + const sourceData = source?.data as TargetData; + const allDropTargets = location?.current?.dropTargets; + + // if all the dropTargets are greater than 1 meaning the source is being dragged on a group and its child at the same time + // and also if the dropTarget in question is also a group then, it should be a child of the current Droptarget + if (allDropTargets?.length > 1 && dropTargetData?.isGroup) return "make-child"; + + if (!dropTargetData || !sourceData) return undefined; + + let instruction = extractInstruction(dropTargetData)?.type; + + // If the instruction is blocked then set an instruction based on if dropTarget it is a child or not + if (instruction === "instruction-blocked") { + instruction = dropTargetData.isChild ? "reorder-above" : "make-child"; + } + + // if source that is being dragged is a group. A group cannon be a child of any other sticky, + // hence if current instruction is to be a child of dropTarget then reorder-above instead + if (instruction === "make-child" && sourceData.isGroup) instruction = "reorder-above"; + + return instruction; +}; diff --git a/web/core/components/stickies/modal/search.tsx b/web/core/components/stickies/modal/search.tsx index 34c0a8f6d1b..15e21c81126 100644 --- a/web/core/components/stickies/modal/search.tsx +++ b/web/core/components/stickies/modal/search.tsx @@ -1,7 +1,9 @@ "use client"; -import { FC, useRef, useState } from "react"; +import { FC, useCallback, useRef, useState } from "react"; +import { debounce } from "lodash"; import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; import { Search, X } from "lucide-react"; // plane hooks import { useOutsideClickDetector } from "@plane/hooks"; @@ -10,25 +12,41 @@ import { cn } from "@/helpers/common.helper"; import { useSticky } from "@/hooks/use-stickies"; 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(); + }, 500), + [fetchWorkspaceStickies] + ); + return ( -
+
{!isSearchOpen && (
{/* 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/sticky/inputs.tsx b/web/core/components/stickies/sticky/inputs.tsx index 8e36fd0b8db..26ec8420c17 100644 --- a/web/core/components/stickies/sticky/inputs.tsx +++ b/web/core/components/stickies/sticky/inputs.tsx @@ -1,10 +1,13 @@ import { useCallback, useEffect, useRef } from "react"; import { DebouncedFunc } from "lodash"; import { Controller, useForm } from "react-hook-form"; +// plane editor import { EditorRefApi } from "@plane/editor"; +// plane types import { TSticky } from "@plane/types"; -import { TextArea } from "@plane/ui"; +// hooks import { useWorkspace } from "@/hooks/store"; +// components import { StickyEditor } from "../../editor"; type TProps = { @@ -12,73 +15,45 @@ type TProps = { workspaceSlug: string; handleUpdate: DebouncedFunc<(payload: Partial) => Promise>; stickyId: string | undefined; + showToolbar?: boolean; handleChange: (data: Partial) => Promise; handleDelete: () => void; }; + export const StickyInput = (props: TProps) => { - const { stickyData, workspaceSlug, handleUpdate, stickyId, handleDelete, handleChange } = props; - //refs + const { stickyData, workspaceSlug, handleUpdate, stickyId, handleDelete, handleChange, showToolbar } = props; + // refs const editorRef = useRef(null); // store hooks const { getWorkspaceBySlug } = useWorkspace(); + // derived values + const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id?.toString() ?? ""; // 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; - + // handle description update + const handleFormSubmit = useCallback( + async (formdata: Partial) => { + await handleUpdate({ + description_html: formdata.description_html ?? "

", + }); + }, + [handleUpdate] + ); // reset form values useEffect(() => { if (!stickyId) return; reset({ id: stickyId, - description_html: stickyData?.description_html === "" ? "

" : stickyData?.description_html, - name: stickyData?.name, + description_html: stickyData?.description_html?.trim() === "" ? "

" : stickyData?.description_html, }); - }, [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] - ); + }, [stickyData, stickyId, reset]); return (
- {/* name */} - ( -