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 (
+ <>
+
+
+
+
+ }
+ />
+ }
+ />
+
+
+
+
+
+
+ {
+ toggleShowNewSticky(true);
+ stickyOperations.create();
+ }}
+ loading={creatingSticky}
+ >
+ Add sticky
+
+
+
+ >
+ );
+});
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",
}}
>
-
- {showAll ? "Show less" : "Show all"}
-
+ {customButton || (
+
+ {showAll ? "Show less" : "Show all"}
+
+ )}
)}
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) => (
+ {
+ handleUpdate({
+ background_color: color.key,
+ });
+ }}
+ className="h-6 w-6 rounded-md hover:ring-2 hover:ring-custom-primary focus:outline-none focus:ring-2 focus:ring-custom-primary transition-all"
+ style={{
+ backgroundColor: color.backgroundColor,
+ }}
+ />
+ ))}
+
+
+ );
+};
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) => (
- handleUpdate({ color })}
- className="h-6 w-6 rounded-md hover:ring-2 hover:ring-custom-primary focus:outline-none focus:ring-2 focus:ring-custom-primary transition-all"
- style={{ backgroundColor: color }}
- />
- ))}
-
-
- );
-};
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) => {
-
+
Manage widgets
-
+
);
};
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.{" "}
-
-
- Add quick link
-
);
-};
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 (
+
+
-
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(() => {
setShowRecentSticky(true)}
- style={{ color: stickies[recentStickyId]?.color }}
+ style={{ color: recentStickyBackgroundColor }}
>
-
+
)}
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.
-
-
- Add sticky
- {creatingSticky && (
-
- )}
-
-
-
- );
-};
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 && (
{
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..a1203c017f0 100644
--- a/web/core/components/stickies/modal/stickies.tsx
+++ b/web/core/components/stickies/modal/stickies.tsx
@@ -3,8 +3,7 @@ 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 { StickiesLayout } from "../stickies-layout";
+import { StickiesTruncated } from "../layout/stickies-truncated";
import { useStickyOperations } from "../sticky/use-operations";
import { StickySearch } from "./search";
@@ -19,13 +18,13 @@ export const Stickies = observer((props: TProps) => {
const { stickyOperations } = useStickyOperations({ workspaceSlug: workspaceSlug?.toString() });
return (
-
+
{/* header */}
{/* Title */}
-
My Stickies
+
Your stickies
{/* actions */}
@@ -33,7 +32,7 @@ export const Stickies = observer((props: TProps) => {
{
toggleShowNewSticky(true);
- stickyOperations.create({ color: STICKY_COLORS[0] });
+ stickyOperations.create();
}}
className="flex gap-1 text-sm font-medium text-custom-primary-100 my-auto"
disabled={creatingSticky}
@@ -61,7 +60,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/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 (