+ {filteredArray.map((icon) => (
{
onChange({
name: icon.name,
@@ -99,7 +118,10 @@ export const IconsList: React.FC = (props) => {
});
}}
>
-
+
{icon.name}
diff --git a/packages/ui/src/emoji/icons.ts b/packages/ui/src/emoji/icons.ts
index 72aacf18bb7..3d650e244ef 100644
--- a/packages/ui/src/emoji/icons.ts
+++ b/packages/ui/src/emoji/icons.ts
@@ -1,3 +1,156 @@
+import {
+ Activity,
+ Airplay,
+ AlertCircle,
+ AlertOctagon,
+ AlertTriangle,
+ AlignCenter,
+ AlignJustify,
+ AlignLeft,
+ AlignRight,
+ Anchor,
+ Aperture,
+ Archive,
+ ArrowDown,
+ ArrowLeft,
+ ArrowRight,
+ ArrowUp,
+ AtSign,
+ Award,
+ BarChart,
+ BarChart2,
+ Battery,
+ BatteryCharging,
+ Bell,
+ BellOff,
+ Book,
+ Bookmark,
+ BookOpen,
+ Box,
+ Briefcase,
+ Calendar,
+ Camera,
+ CameraOff,
+ Cast,
+ Check,
+ CheckCircle,
+ CheckSquare,
+ ChevronDown,
+ ChevronLeft,
+ ChevronRight,
+ ChevronUp,
+ Clipboard,
+ Clock,
+ Cloud,
+ CloudDrizzle,
+ CloudLightning,
+ CloudOff,
+ CloudRain,
+ CloudSnow,
+ Code,
+ Codepen,
+ Codesandbox,
+ Coffee,
+ Columns,
+ Command,
+ Compass,
+ Copy,
+ CornerDownLeft,
+ CornerDownRight,
+ CornerLeftDown,
+ CornerLeftUp,
+ CornerRightDown,
+ CornerRightUp,
+ CornerUpLeft,
+ CornerUpRight,
+ Cpu,
+ CreditCard,
+ Crop,
+ Crosshair,
+ Database,
+ Delete,
+ Disc,
+ Divide,
+ DivideCircle,
+ DivideSquare,
+ DollarSign,
+ Download,
+ DownloadCloud,
+ Dribbble,
+ Droplet,
+ Edit,
+ Edit2,
+ Edit3,
+ ExternalLink,
+ Eye,
+ EyeOff,
+ Facebook,
+ FastForward,
+ Feather,
+ Figma,
+ File,
+ FileMinus,
+ FilePlus,
+ FileText,
+ Film,
+ Filter,
+ Flag,
+ Folder,
+ FolderMinus,
+ FolderPlus,
+ Framer,
+ Frown,
+ Gift,
+ GitBranch,
+ GitCommit,
+ GitMerge,
+ GitPullRequest,
+ Github,
+ Gitlab,
+ Globe,
+ Grid,
+ HardDrive,
+ Hash,
+ Headphones,
+ Heart,
+ HelpCircle,
+ Hexagon,
+ Home,
+ Image,
+ Inbox,
+ Info,
+ Instagram,
+ Italic,
+ Key,
+ Layers,
+ Layout,
+ LifeBuoy,
+ Link,
+ Link2,
+ Linkedin,
+ List,
+ Loader,
+ Lock,
+ LogIn,
+ LogOut,
+ Mail,
+ Map,
+ MapPin,
+ Maximize,
+ Maximize2,
+ Meh,
+ Menu,
+ MessageCircle,
+ MessageSquare,
+ Mic,
+ MicOff,
+ Minimize,
+ Minimize2,
+ Minus,
+ MinusCircle,
+ MinusSquare,
+} from "lucide-react";
+
export const MATERIAL_ICONS_LIST = [
{
name: "search",
@@ -603,3 +756,156 @@ export const MATERIAL_ICONS_LIST = [
name: "skull",
},
];
+
+export const LUCIDE_ICONS_LIST = [
+ { name: "Activity", element: Activity },
+ { name: "Airplay", element: Airplay },
+ { name: "AlertCircle", element: AlertCircle },
+ { name: "AlertOctagon", element: AlertOctagon },
+ { name: "AlertTriangle", element: AlertTriangle },
+ { name: "AlignCenter", element: AlignCenter },
+ { name: "AlignJustify", element: AlignJustify },
+ { name: "AlignLeft", element: AlignLeft },
+ { name: "AlignRight", element: AlignRight },
+ { name: "Anchor", element: Anchor },
+ { name: "Aperture", element: Aperture },
+ { name: "Archive", element: Archive },
+ { name: "ArrowDown", element: ArrowDown },
+ { name: "ArrowLeft", element: ArrowLeft },
+ { name: "ArrowRight", element: ArrowRight },
+ { name: "ArrowUp", element: ArrowUp },
+ { name: "AtSign", element: AtSign },
+ { name: "Award", element: Award },
+ { name: "BarChart", element: BarChart },
+ { name: "BarChart2", element: BarChart2 },
+ { name: "Battery", element: Battery },
+ { name: "BatteryCharging", element: BatteryCharging },
+ { name: "Bell", element: Bell },
+ { name: "BellOff", element: BellOff },
+ { name: "Book", element: Book },
+ { name: "Bookmark", element: Bookmark },
+ { name: "BookOpen", element: BookOpen },
+ { name: "Box", element: Box },
+ { name: "Briefcase", element: Briefcase },
+ { name: "Calendar", element: Calendar },
+ { name: "Camera", element: Camera },
+ { name: "CameraOff", element: CameraOff },
+ { name: "Cast", element: Cast },
+ { name: "Check", element: Check },
+ { name: "CheckCircle", element: CheckCircle },
+ { name: "CheckSquare", element: CheckSquare },
+ { name: "ChevronDown", element: ChevronDown },
+ { name: "ChevronLeft", element: ChevronLeft },
+ { name: "ChevronRight", element: ChevronRight },
+ { name: "ChevronUp", element: ChevronUp },
+ { name: "Clipboard", element: Clipboard },
+ { name: "Clock", element: Clock },
+ { name: "Cloud", element: Cloud },
+ { name: "CloudDrizzle", element: CloudDrizzle },
+ { name: "CloudLightning", element: CloudLightning },
+ { name: "CloudOff", element: CloudOff },
+ { name: "CloudRain", element: CloudRain },
+ { name: "CloudSnow", element: CloudSnow },
+ { name: "Code", element: Code },
+ { name: "Codepen", element: Codepen },
+ { name: "Codesandbox", element: Codesandbox },
+ { name: "Coffee", element: Coffee },
+ { name: "Columns", element: Columns },
+ { name: "Command", element: Command },
+ { name: "Compass", element: Compass },
+ { name: "Copy", element: Copy },
+ { name: "CornerDownLeft", element: CornerDownLeft },
+ { name: "CornerDownRight", element: CornerDownRight },
+ { name: "CornerLeftDown", element: CornerLeftDown },
+ { name: "CornerLeftUp", element: CornerLeftUp },
+ { name: "CornerRightDown", element: CornerRightDown },
+ { name: "CornerRightUp", element: CornerRightUp },
+ { name: "CornerUpLeft", element: CornerUpLeft },
+ { name: "CornerUpRight", element: CornerUpRight },
+ { name: "Cpu", element: Cpu },
+ { name: "CreditCard", element: CreditCard },
+ { name: "Crop", element: Crop },
+ { name: "Crosshair", element: Crosshair },
+ { name: "Database", element: Database },
+ { name: "Delete", element: Delete },
+ { name: "Disc", element: Disc },
+ { name: "Divide", element: Divide },
+ { name: "DivideCircle", element: DivideCircle },
+ { name: "DivideSquare", element: DivideSquare },
+ { name: "DollarSign", element: DollarSign },
+ { name: "Download", element: Download },
+ { name: "DownloadCloud", element: DownloadCloud },
+ { name: "Dribbble", element: Dribbble },
+ { name: "Droplet", element: Droplet },
+ { name: "Edit", element: Edit },
+ { name: "Edit2", element: Edit2 },
+ { name: "Edit3", element: Edit3 },
+ { name: "ExternalLink", element: ExternalLink },
+ { name: "Eye", element: Eye },
+ { name: "EyeOff", element: EyeOff },
+ { name: "Facebook", element: Facebook },
+ { name: "FastForward", element: FastForward },
+ { name: "Feather", element: Feather },
+ { name: "Figma", element: Figma },
+ { name: "File", element: File },
+ { name: "FileMinus", element: FileMinus },
+ { name: "FilePlus", element: FilePlus },
+ { name: "FileText", element: FileText },
+ { name: "Film", element: Film },
+ { name: "Filter", element: Filter },
+ { name: "Flag", element: Flag },
+ { name: "Folder", element: Folder },
+ { name: "FolderMinus", element: FolderMinus },
+ { name: "FolderPlus", element: FolderPlus },
+ { name: "Framer", element: Framer },
+ { name: "Frown", element: Frown },
+ { name: "Gift", element: Gift },
+ { name: "GitBranch", element: GitBranch },
+ { name: "GitCommit", element: GitCommit },
+ { name: "GitMerge", element: GitMerge },
+ { name: "GitPullRequest", element: GitPullRequest },
+ { name: "Github", element: Github },
+ { name: "Gitlab", element: Gitlab },
+ { name: "Globe", element: Globe },
+ { name: "Grid", element: Grid },
+ { name: "HardDrive", element: HardDrive },
+ { name: "Hash", element: Hash },
+ { name: "Headphones", element: Headphones },
+ { name: "Heart", element: Heart },
+ { name: "HelpCircle", element: HelpCircle },
+ { name: "Hexagon", element: Hexagon },
+ { name: "Home", element: Home },
+ { name: "Image", element: Image },
+ { name: "Inbox", element: Inbox },
+ { name: "Info", element: Info },
+ { name: "Instagram", element: Instagram },
+ { name: "Italic", element: Italic },
+ { name: "Key", element: Key },
+ { name: "Layers", element: Layers },
+ { name: "Layout", element: Layout },
+ { name: "LifeBuoy", element: LifeBuoy },
+ { name: "Link", element: Link },
+ { name: "Link2", element: Link2 },
+ { name: "Linkedin", element: Linkedin },
+ { name: "List", element: List },
+ { name: "Loader", element: Loader },
+ { name: "Lock", element: Lock },
+ { name: "LogIn", element: LogIn },
+ { name: "LogOut", element: LogOut },
+ { name: "Mail", element: Mail },
+ { name: "Map", element: Map },
+ { name: "MapPin", element: MapPin },
+ { name: "Maximize", element: Maximize },
+ { name: "Maximize2", element: Maximize2 },
+ { name: "Meh", element: Meh },
+ { name: "Menu", element: Menu },
+ { name: "MessageCircle", element: MessageCircle },
+ { name: "MessageSquare", element: MessageSquare },
+ { name: "Mic", element: Mic },
+ { name: "MicOff", element: MicOff },
+ { name: "Minimize", element: Minimize },
+ { name: "Minimize2", element: Minimize2 },
+ { name: "Minus", element: Minus },
+ { name: "MinusCircle", element: MinusCircle },
+ { name: "MinusSquare", element: MinusSquare },
+];
diff --git a/packages/ui/src/emoji/index.ts b/packages/ui/src/emoji/index.ts
index 97345413903..128b802925e 100644
--- a/packages/ui/src/emoji/index.ts
+++ b/packages/ui/src/emoji/index.ts
@@ -1 +1,4 @@
+export * from "./emoji-icon-picker-new";
export * from "./emoji-icon-picker";
+export * from "./emoji-icon-helper";
+export * from "./icons";
diff --git a/packages/ui/src/emoji/lucide-icons-list.tsx b/packages/ui/src/emoji/lucide-icons-list.tsx
new file mode 100644
index 00000000000..799f0919dcc
--- /dev/null
+++ b/packages/ui/src/emoji/lucide-icons-list.tsx
@@ -0,0 +1,128 @@
+import React, { useEffect, useState } from "react";
+// components
+import { Input } from "../form-fields";
+// helpers
+import { cn } from "../../helpers";
+import { DEFAULT_COLORS, TIconsListProps, adjustColorForContrast } from "./emoji-icon-helper";
+// icons
+import { InfoIcon } from "../icons";
+// constants
+import { LUCIDE_ICONS_LIST } from "./icons";
+import { Search } from "lucide-react";
+
+export const LucideIconsList: React.FC
= (props) => {
+ const { defaultColor, onChange } = props;
+ // states
+ const [activeColor, setActiveColor] = useState(defaultColor);
+ const [showHexInput, setShowHexInput] = useState(false);
+ const [hexValue, setHexValue] = useState("");
+ const [isInputFocused, setIsInputFocused] = useState(false);
+ const [query, setQuery] = useState("");
+
+ useEffect(() => {
+ if (DEFAULT_COLORS.includes(defaultColor.toLowerCase())) setShowHexInput(false);
+ else {
+ setHexValue(defaultColor.slice(1, 7));
+ setShowHexInput(true);
+ }
+ }, [defaultColor]);
+
+ const filteredArray = LUCIDE_ICONS_LIST.filter((icon) => icon.name.toLowerCase().includes(query.toLowerCase()));
+
+ return (
+ <>
+
+
setIsInputFocused(true)}
+ onBlur={() => setIsInputFocused(false)}
+ >
+
+ setQuery(e.target.value)}
+ className="text-[1rem] border-none p-0 h-full w-full "
+ />
+
+
+
+ {showHexInput ? (
+
+
+ HEX
+ #
+ {
+ const value = e.target.value;
+ setHexValue(value);
+ if (/^[0-9A-Fa-f]{6}$/.test(value)) setActiveColor(adjustColorForContrast(`#${value}`));
+ }}
+ className="flex-grow pl-0 text-xs text-custom-text-200"
+ mode="true-transparent"
+ autoFocus
+ />
+
+ ) : (
+ DEFAULT_COLORS.map((curCol) => (
+
{
+ setActiveColor(curCol);
+ setHexValue(curCol.slice(1, 7));
+ }}
+ >
+
+
+ ))
+ )}
+
{
+ setShowHexInput((prevData) => !prevData);
+ setHexValue(activeColor.slice(1, 7));
+ }}
+ >
+ {showHexInput ? (
+
+ ) : (
+ #
+ )}
+
+
+
+
+
Colors will be adjusted to ensure sufficient contrast.
+
+
+ {filteredArray.map((icon) => (
+ {
+ onChange({
+ name: icon.name,
+ color: activeColor,
+ });
+ }}
+ >
+
+
+ ))}
+
+ >
+ );
+};
diff --git a/packages/ui/src/form-fields/checkbox.tsx b/packages/ui/src/form-fields/checkbox.tsx
index 887bc60740d..3c45cf4f574 100644
--- a/packages/ui/src/form-fields/checkbox.tsx
+++ b/packages/ui/src/form-fields/checkbox.tsx
@@ -3,15 +3,26 @@ import * as React from "react";
import { cn } from "../../helpers";
export interface CheckboxProps extends React.InputHTMLAttributes {
- intermediate?: boolean;
- className?: string;
+ containerClassName?: string;
+ iconClassName?: string;
+ indeterminate?: boolean;
}
const Checkbox = React.forwardRef((props, ref) => {
- const { id, name, checked, intermediate = false, disabled, className = "", ...rest } = props;
+ const {
+ id,
+ name,
+ checked,
+ indeterminate = false,
+ disabled,
+ containerClassName,
+ iconClassName,
+ className,
+ ...rest
+ } = props;
return (
-
+
((props, ref)
name={name}
checked={checked}
className={cn(
- "appearance-none shrink-0 w-4 h-4 border rounded-[3px] focus:outline-1 focus:outline-offset-4 focus:outline-custom-primary-50",
+ "appearance-none shrink-0 size-4 border rounded-[3px] focus:outline-1 focus:outline-offset-4 focus:outline-custom-primary-50 cursor-pointer",
{
"border-custom-border-200 bg-custom-background-80 cursor-not-allowed": disabled,
- "cursor-pointer border-custom-border-300 hover:border-custom-border-400 bg-white": !disabled,
- "border-custom-primary-40 bg-custom-primary-100 hover:bg-custom-primary-200":
- !disabled && (checked || intermediate),
- }
+ "border-custom-border-300 hover:border-custom-border-400 bg-transparent": !disabled,
+ "border-custom-primary-40 hover:border-custom-primary-40 bg-custom-primary-100 hover:bg-custom-primary-200":
+ !disabled && (checked || indeterminate),
+ },
+ className
)}
disabled={disabled}
{...rest}
/>
((props, ref)
= ({ className = "text-current", ...rest }) => (
+
+
+
+
+
+);
diff --git a/packages/ui/src/icons/user-group-icon.tsx b/packages/ui/src/icons/user-group-icon.tsx
deleted file mode 100644
index 7cad96d231c..00000000000
--- a/packages/ui/src/icons/user-group-icon.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import * as React from "react";
-
-import { ISvgIcons } from "./type";
-
-export const UserGroupIcon: React.FC = ({ className = "text-current", ...rest }) => (
-
-
-
-
-
-
-);
diff --git a/space/.gitignore b/space/.gitignore
index a2a963ee7e0..a64f113f12d 100644
--- a/space/.gitignore
+++ b/space/.gitignore
@@ -37,3 +37,6 @@ next-env.d.ts
# env
.env
+
+# Sentry Config File
+.env.sentry-build-plugin
diff --git a/space/components/common/project-logo.tsx b/space/components/common/project-logo.tsx
index 9b69e96167d..dfb3a4b80e2 100644
--- a/space/components/common/project-logo.tsx
+++ b/space/components/common/project-logo.tsx
@@ -1,11 +1,11 @@
+// types
+import { TLogoProps } from "@plane/types";
// helpers
-import { TProjectLogoProps } from "@plane/types";
import { cn } from "@/helpers/common.helper";
-// types
type Props = {
className?: string;
- logo: TProjectLogoProps;
+ logo: TLogoProps;
};
export const ProjectLogo: React.FC = (props) => {
diff --git a/space/instrumentation.ts b/space/instrumentation.ts
new file mode 100644
index 00000000000..7b89a972e15
--- /dev/null
+++ b/space/instrumentation.ts
@@ -0,0 +1,9 @@
+export async function register() {
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
+ await import('./sentry.server.config');
+ }
+
+ if (process.env.NEXT_RUNTIME === 'edge') {
+ await import('./sentry.edge.config');
+ }
+}
diff --git a/space/next.config.js b/space/next.config.js
index eb9dde88a32..d18ce805f4d 100644
--- a/space/next.config.js
+++ b/space/next.config.js
@@ -28,12 +28,46 @@ const nextConfig = {
},
};
-if (parseInt(process.env.NEXT_PUBLIC_ENABLE_SENTRY || "0", 10)) {
- module.exports = withSentryConfig(
- nextConfig,
- { silent: true, authToken: process.env.SENTRY_AUTH_TOKEN },
- { hideSourceMaps: true }
- );
+
+const sentryConfig = {
+ // For all available options, see:
+ // https://github.com/getsentry/sentry-webpack-plugin#options
+
+ org: process.env.SENTRY_ORG_ID || "plane-hq",
+ project: process.env.SENTRY_PROJECT_ID || "plane-space",
+ authToken: process.env.SENTRY_AUTH_TOKEN,
+ // Only print logs for uploading source maps in CI
+ silent: true,
+
+ // For all available options, see:
+ // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
+
+ // Upload a larger set of source maps for prettier stack traces (increases build time)
+ widenClientFileUpload: true,
+
+ // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
+ // This can increase your server load as well as your hosting bill.
+ // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
+ // side errors will fail.
+ tunnelRoute: "/monitoring",
+
+ // Hides source maps from generated client bundles
+ hideSourceMaps: true,
+
+ // Automatically tree-shake Sentry logger statements to reduce bundle size
+ disableLogger: true,
+
+ // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
+ // See the following for more information:
+ // https://docs.sentry.io/product/crons/
+ // https://vercel.com/docs/cron-jobs
+ automaticVercelMonitors: true,
+}
+
+
+if (parseInt(process.env.SENTRY_MONITORING_ENABLED || "0", 10)) {
+ module.exports = withSentryConfig(nextConfig, sentryConfig);
} else {
module.exports = nextConfig;
}
+
diff --git a/space/package.json b/space/package.json
index a084c143b9c..e3dadbff8e5 100644
--- a/space/package.json
+++ b/space/package.json
@@ -1,6 +1,6 @@
{
"name": "space",
- "version": "0.20.0",
+ "version": "0.21.0",
"private": true,
"scripts": {
"dev": "turbo run develop",
@@ -23,7 +23,7 @@
"@plane/rich-text-editor": "*",
"@plane/types": "*",
"@plane/ui": "*",
- "@sentry/nextjs": "^7.108.0",
+ "@sentry/nextjs": "^8",
"axios": "^1.3.4",
"clsx": "^2.0.0",
"dompurify": "^3.0.11",
diff --git a/space/sentry.client.config.js b/space/sentry.client.config.js
deleted file mode 100644
index ca473045b45..00000000000
--- a/space/sentry.client.config.js
+++ /dev/null
@@ -1,18 +0,0 @@
-// This file configures the initialization of Sentry on the browser.
-// The config you add here will be used whenever a page is visited.
-// https://docs.sentry.io/platforms/javascript/guides/nextjs/
-
-import * as Sentry from "@sentry/nextjs";
-
-const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
-
-Sentry.init({
- dsn: SENTRY_DSN,
- environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
- // Adjust this value in production, or use tracesSampler for greater control
- tracesSampleRate: 1.0,
- // ...
- // Note: if you want to override the automatic release value, do not set a
- // `release` value here - use the environment variable `SENTRY_RELEASE`, so
- // that it will also get attached to your source maps
-});
diff --git a/space/sentry.client.config.ts b/space/sentry.client.config.ts
new file mode 100644
index 00000000000..c8103062290
--- /dev/null
+++ b/space/sentry.client.config.ts
@@ -0,0 +1,31 @@
+// This file configures the initialization of Sentry on the client.
+// The config you add here will be used whenever a users loads a page in their browser.
+// https://docs.sentry.io/platforms/javascript/guides/nextjs/
+
+import * as Sentry from "@sentry/nextjs";
+
+Sentry.init({
+ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
+ environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
+
+ // Adjust this value in production, or use tracesSampler for greater control
+ tracesSampleRate: 1,
+
+ // Setting this option to true will print useful information to the console while you're setting up Sentry.
+ debug: false,
+
+ replaysOnErrorSampleRate: 1.0,
+
+ // This sets the sample rate to be 10%. You may want this to be 100% while
+ // in development and sample at a lower rate in production
+ replaysSessionSampleRate: 0.1,
+
+ // You can remove this option if you're not planning to use the Sentry Session Replay feature:
+ integrations: [
+ Sentry.replayIntegration({
+ // Additional Replay configuration goes in here, for example:
+ maskAllText: true,
+ blockAllMedia: true,
+ }),
+ ],
+});
diff --git a/space/sentry.edge.config.js b/space/sentry.edge.config.js
deleted file mode 100644
index 8374ed4101e..00000000000
--- a/space/sentry.edge.config.js
+++ /dev/null
@@ -1,18 +0,0 @@
-// This file configures the initialization of Sentry on the server.
-// The config you add here will be used whenever middleware or an Edge route handles a request.
-// https://docs.sentry.io/platforms/javascript/guides/nextjs/
-
-import * as Sentry from "@sentry/nextjs";
-
-const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
-
-Sentry.init({
- dsn: SENTRY_DSN,
- environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
- // Adjust this value in production, or use tracesSampler for greater control
- tracesSampleRate: 1.0,
- // ...
- // Note: if you want to override the automatic release value, do not set a
- // `release` value here - use the environment variable `SENTRY_RELEASE`, so
- // that it will also get attached to your source maps
-});
diff --git a/space/sentry.edge.config.ts b/space/sentry.edge.config.ts
new file mode 100644
index 00000000000..2dbc6e93afb
--- /dev/null
+++ b/space/sentry.edge.config.ts
@@ -0,0 +1,17 @@
+// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
+// The config you add here will be used whenever one of the edge features is loaded.
+// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
+// https://docs.sentry.io/platforms/javascript/guides/nextjs/
+
+import * as Sentry from "@sentry/nextjs";
+
+Sentry.init({
+ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
+ environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
+
+ // Adjust this value in production, or use tracesSampler for greater control
+ tracesSampleRate: 1,
+
+ // Setting this option to true will print useful information to the console while you're setting up Sentry.
+ debug: false,
+});
diff --git a/space/sentry.server.config.js b/space/sentry.server.config.ts
similarity index 56%
rename from space/sentry.server.config.js
rename to space/sentry.server.config.ts
index d2acb07e154..e578f1530c0 100644
--- a/space/sentry.server.config.js
+++ b/space/sentry.server.config.ts
@@ -4,15 +4,16 @@
import * as Sentry from "@sentry/nextjs";
-const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
-
Sentry.init({
- dsn: SENTRY_DSN,
+ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
+
// Adjust this value in production, or use tracesSampler for greater control
- tracesSampleRate: 1.0,
- // ...
- // Note: if you want to override the automatic release value, do not set a
- // `release` value here - use the environment variable `SENTRY_RELEASE`, so
- // that it will also get attached to your source maps
+ tracesSampleRate: 1,
+
+ // Setting this option to true will print useful information to the console while you're setting up Sentry.
+ debug: false,
+
+ // Uncomment the line below to enable Spotlight (https://spotlightjs.com)
+ // spotlight: process.env.NODE_ENV === 'development',
});
diff --git a/space/types/project.d.ts b/space/types/project.d.ts
index 99dbfec8bb5..90c89ed80c4 100644
--- a/space/types/project.d.ts
+++ b/space/types/project.d.ts
@@ -1,4 +1,4 @@
-import { TProjectLogoProps } from "@plane/types";
+import { TLogoProps } from "@plane/types";
export type TWorkspaceDetails = {
name: string;
@@ -19,7 +19,7 @@ export type TProjectDetails = {
identifier: string;
name: string;
cover_image: string | undefined;
- logo_props: TProjectLogoProps;
+ logo_props: TLogoProps;
description: string;
};
diff --git a/turbo.json b/turbo.json
index c08733c85c4..fde4ffc79b5 100644
--- a/turbo.json
+++ b/turbo.json
@@ -8,10 +8,6 @@
"NEXT_PUBLIC_SPACE_BASE_URL",
"NEXT_PUBLIC_SPACE_BASE_PATH",
"NEXT_PUBLIC_WEB_BASE_URL",
- "NEXT_PUBLIC_SENTRY_DSN",
- "NEXT_PUBLIC_SENTRY_ENVIRONMENT",
- "NEXT_PUBLIC_ENABLE_SENTRY",
- "NEXT_PUBLIC_TRACK_EVENTS",
"NEXT_PUBLIC_PLAUSIBLE_DOMAIN",
"NEXT_PUBLIC_CRISP_ID",
"NEXT_PUBLIC_ENABLE_SESSION_RECORDER",
@@ -21,7 +17,12 @@
"NEXT_PUBLIC_POSTHOG_HOST",
"NEXT_PUBLIC_POSTHOG_DEBUG",
"NEXT_PUBLIC_SUPPORT_EMAIL",
- "SENTRY_AUTH_TOKEN"
+ "SENTRY_AUTH_TOKEN",
+ "SENTRY_ORG_ID",
+ "SENTRY_PROJECT_ID",
+ "NEXT_PUBLIC_SENTRY_ENVIRONMENT",
+ "NEXT_PUBLIC_SENTRY_DSN",
+ "SENTRY_MONITORING_ENABLED"
],
"pipeline": {
"build": {
diff --git a/web/components/analytics/custom-analytics/sidebar/projects-list.tsx b/web/components/analytics/custom-analytics/sidebar/projects-list.tsx
index 0a61e06acef..d7080746763 100644
--- a/web/components/analytics/custom-analytics/sidebar/projects-list.tsx
+++ b/web/components/analytics/custom-analytics/sidebar/projects-list.tsx
@@ -1,10 +1,11 @@
import { observer } from "mobx-react";
-// hooks
// icons
import { Contrast, LayoutGrid, Users } from "lucide-react";
+// components
+import { Logo } from "@/components/common";
// helpers
-import { ProjectLogo } from "@/components/project";
import { truncateText } from "@/helpers/string.helper";
+// hooks
import { useProject } from "@/hooks/store";
type Props = {
@@ -29,7 +30,7 @@ export const CustomAnalyticsSidebarProjectsList: React.FC = observer((pro
{truncateText(project.name, 20)}
diff --git a/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx b/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx
index 6954a897368..ec1eb3ee35b 100644
--- a/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx
+++ b/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx
@@ -1,13 +1,13 @@
import { observer } from "mobx-react";
import { useRouter } from "next/router";
-// hooks
-import { ProjectLogo } from "@/components/project";
+// components
+import { Logo } from "@/components/common";
+// constants
import { NETWORK_CHOICES } from "@/constants/project";
+// helpers
import { renderFormattedDate } from "@/helpers/date-time.helper";
+// hooks
import { useCycle, useMember, useModule, useProject } from "@/hooks/store";
-// components
-// helpers
-// constants
export const CustomAnalyticsSidebarHeader = observer(() => {
const router = useRouter();
@@ -84,7 +84,7 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
{projectDetails && (
-
+
)}
{projectDetails?.name}
diff --git a/web/components/analytics/project-modal/main-content.tsx b/web/components/analytics/project-modal/main-content.tsx
index 030760a1c65..e912828018d 100644
--- a/web/components/analytics/project-modal/main-content.tsx
+++ b/web/components/analytics/project-modal/main-content.tsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { Fragment } from "react";
import { observer } from "mobx-react";
import { Tab } from "@headlessui/react";
import { ICycle, IModule, IProject } from "@plane/types";
@@ -20,20 +20,21 @@ export const ProjectAnalyticsModalMainContent: React.FC
= observer((props
return (
-
+
{ANALYTICS_TABS.map((tab) => (
-
- `rounded-0 w-full md:w-max md:rounded-3xl border-b md:border border-custom-border-200 focus:outline-none px-0 md:px-4 py-2 text-xs hover:bg-custom-background-80 ${
- selected
- ? "border-custom-primary-100 text-custom-primary-100 md:bg-custom-background-80 md:text-custom-text-200 md:border-custom-border-200"
- : "border-transparent"
- }`
- }
- onClick={() => {}}
- >
- {tab.title}
+
+ {({ selected }) => (
+
+ {tab.title}
+
+
+ )}
))}
diff --git a/web/components/api-token/delete-token-modal.tsx b/web/components/api-token/delete-token-modal.tsx
index 014531c4240..6918df463c4 100644
--- a/web/components/api-token/delete-token-modal.tsx
+++ b/web/components/api-token/delete-token-modal.tsx
@@ -69,7 +69,7 @@ export const DeleteApiTokenModal: FC = (props) => {
= observer((props) => {
className="focus:outline-none"
>
-
+
Assign to...
diff --git a/web/components/command-palette/actions/project-actions.tsx b/web/components/command-palette/actions/project-actions.tsx
index 32e7ed04596..ed4bdcadccf 100644
--- a/web/components/command-palette/actions/project-actions.tsx
+++ b/web/components/command-palette/actions/project-actions.tsx
@@ -71,7 +71,7 @@ export const CommandPaletteProjectActions: React.FC = (props) => {
onSelect={() => {
closePalette();
setTrackElement("Command palette");
- toggleCreatePageModal(true);
+ toggleCreatePageModal({ isOpen: true });
}}
className="focus:outline-none"
>
diff --git a/web/components/command-palette/command-palette.tsx b/web/components/command-palette/command-palette.tsx
index 9143d44c77c..41afbb5e2f1 100644
--- a/web/components/command-palette/command-palette.tsx
+++ b/web/components/command-palette/command-palette.tsx
@@ -50,7 +50,7 @@ export const CommandPalette: FC = observer(() => {
toggleCreateIssueModal,
isCreateCycleModalOpen,
toggleCreateCycleModal,
- isCreatePageModalOpen,
+ createPageModal,
toggleCreatePageModal,
isCreateProjectModalOpen,
toggleCreateProjectModal,
@@ -150,7 +150,7 @@ export const CommandPalette: FC = observer(() => {
d: {
title: "Create a new page",
description: "Create a new page in the current project",
- action: () => toggleCreatePageModal(true),
+ action: () => toggleCreatePageModal({ isOpen: true }),
},
m: {
title: "Create a new module",
@@ -297,8 +297,9 @@ export const CommandPalette: FC = observer(() => {
toggleCreatePageModal(false)}
+ isModalOpen={createPageModal.isOpen}
+ pageAccess={createPageModal.pageAccess}
+ handleModalClose={() => toggleCreatePageModal({ isOpen: false })}
redirectionEnabled
/>
>
diff --git a/web/components/common/index.ts b/web/components/common/index.ts
index 816562488be..1ca40f81060 100644
--- a/web/components/common/index.ts
+++ b/web/components/common/index.ts
@@ -3,3 +3,4 @@ export * from "./empty-state";
export * from "./latest-feature-block";
export * from "./breadcrumb-link";
export * from "./logo-spinner";
+export * from "./logo";
diff --git a/web/components/common/logo.tsx b/web/components/common/logo.tsx
new file mode 100644
index 00000000000..d091dedd4c1
--- /dev/null
+++ b/web/components/common/logo.tsx
@@ -0,0 +1,69 @@
+import { FC } from "react";
+// emoji-picker-react
+import { Emoji } from "emoji-picker-react";
+// import { icons } from "lucide-react";
+import { TLogoProps } from "@plane/types";
+// helpers
+import { LUCIDE_ICONS_LIST } from "@plane/ui";
+import { emojiCodeToUnicode } from "@/helpers/emoji.helper";
+
+type Props = {
+ logo: TLogoProps;
+ size?: number;
+ type?: "lucide" | "material";
+};
+
+export const Logo: FC = (props) => {
+ const { logo, size = 16, type = "material" } = props;
+
+ // destructuring the logo object
+ const { in_use, emoji, icon } = logo;
+
+ // derived values
+ const value = in_use === "emoji" ? emoji?.value : icon?.name;
+ const color = icon?.color;
+ const lucideIcon = LUCIDE_ICONS_LIST.find((item) => item.name === value);
+
+ // if no value, return empty fragment
+ if (!value) return <>>;
+
+ // emoji
+ if (in_use === "emoji") {
+ return ;
+ }
+
+ // icon
+ if (in_use === "icon") {
+ return (
+ <>
+ {type === "lucide" ? (
+ <>
+ {lucideIcon && (
+
+ )}
+ >
+ ) : (
+
+ {value}
+
+ )}
+ >
+ );
+ }
+
+ // if no value, return empty fragment
+ return <>>;
+};
diff --git a/web/components/core/activity.tsx b/web/components/core/activity.tsx
index 97927d216af..28d84ffe47b 100644
--- a/web/components/core/activity.tsx
+++ b/web/components/core/activity.tsx
@@ -502,7 +502,7 @@ const activityDetails: {
name: {
message: (activity, showIssue) => (
<>
- set the name to {activity.new_value}
+ set the title to {activity.new_value}
{showIssue && (
<>
{" "}
diff --git a/web/components/core/index.ts b/web/components/core/index.ts
index 3f753e0258f..81649c64834 100644
--- a/web/components/core/index.ts
+++ b/web/components/core/index.ts
@@ -1,5 +1,6 @@
export * from "./filters";
export * from "./modals";
+export * from "./multiple-select";
export * from "./sidebar";
export * from "./activity";
export * from "./favorite-star";
diff --git a/web/components/core/list/list-item.tsx b/web/components/core/list/list-item.tsx
index ae32c9b3177..8527d56b501 100644
--- a/web/components/core/list/list-item.tsx
+++ b/web/components/core/list/list-item.tsx
@@ -1,7 +1,9 @@
import React, { FC } from "react";
-import Link from "next/link";
+import { useRouter } from "next/router";
// ui
-import { Tooltip } from "@plane/ui";
+import { ControlLink, Tooltip } from "@plane/ui";
+// helpers
+import { cn } from "@/helpers/common.helper";
interface IListItemProps {
title: string;
@@ -12,6 +14,8 @@ interface IListItemProps {
actionableItems?: JSX.Element;
isMobile?: boolean;
parentRef: React.RefObject;
+ disableLink?: boolean;
+ className?: string;
}
export const ListItem: FC = (props) => {
@@ -24,12 +28,28 @@ export const ListItem: FC = (props) => {
onItemClick,
isMobile = false,
parentRef,
+ disableLink = false,
+ className = "",
} = props;
+ // router
+ const router = useRouter();
+
+ // handlers
+ const handleControlLinkClick = (e: React.MouseEvent) => {
+ if (onItemClick) onItemClick(e);
+ else router.push(itemLink);
+ };
+
return (
-
-
+
+
@@ -43,7 +63,7 @@ export const ListItem: FC = (props) => {
-
+
{actionableItems && (
diff --git a/web/components/core/modals/alert-modal.tsx b/web/components/core/modals/alert-modal.tsx
index fbc5e250354..d864c2b38a0 100644
--- a/web/components/core/modals/alert-modal.tsx
+++ b/web/components/core/modals/alert-modal.tsx
@@ -1,4 +1,4 @@
-import { AlertTriangle, LucideIcon } from "lucide-react";
+import { AlertTriangle, Info, LucideIcon } from "lucide-react";
// ui
import { Button, TButtonVariant } from "@plane/ui";
// components
@@ -6,14 +6,14 @@ import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
// helpers
import { cn } from "@/helpers/common.helper";
-export type TModalVariant = "danger";
+export type TModalVariant = "danger" | "primary";
type Props = {
content: React.ReactNode | string;
handleClose: () => void;
handleSubmit: () => Promise
;
hideIcon?: boolean;
- isDeleting: boolean;
+ isSubmitting: boolean;
isOpen: boolean;
position?: EModalPosition;
primaryButtonText?: {
@@ -28,14 +28,17 @@ type Props = {
const VARIANT_ICONS: Record = {
danger: AlertTriangle,
+ primary: Info,
};
const BUTTON_VARIANTS: Record = {
danger: "danger",
+ primary: "primary",
};
const VARIANT_CLASSES: Record = {
danger: "bg-red-500/20 text-red-500",
+ primary: "bg-custom-primary-100/20 text-custom-primary-100",
};
export const AlertModalCore: React.FC = (props) => {
@@ -44,7 +47,7 @@ export const AlertModalCore: React.FC = (props) => {
handleClose,
handleSubmit,
hideIcon = false,
- isDeleting,
+ isSubmitting,
isOpen,
position = EModalPosition.CENTER,
primaryButtonText = {
@@ -81,8 +84,8 @@ export const AlertModalCore: React.FC = (props) => {
{secondaryButtonText}
-
- {isDeleting ? primaryButtonText.loading : primaryButtonText.default}
+
+ {isSubmitting ? primaryButtonText.loading : primaryButtonText.default}
diff --git a/web/components/core/modals/gpt-assistant-popover.tsx b/web/components/core/modals/gpt-assistant-popover.tsx
index a67b13f7ef9..5fd992a28c2 100644
--- a/web/components/core/modals/gpt-assistant-popover.tsx
+++ b/web/components/core/modals/gpt-assistant-popover.tsx
@@ -173,13 +173,15 @@ export const GptAssistantPopover: React.FC
= (props) => {
const generateResponseButtonText = isSubmitting
? "Generating response..."
: response === ""
- ? "Generate response"
- : "Generate again";
+ ? "Generate response"
+ : "Generate again";
return (
- {button}
+
+ {button}
+
= (props) => {
+ const { className, disabled = false, groupId, id, selectionHelpers } = props;
+ // derived values
+ const isSelected = selectionHelpers.getIsEntitySelected(id);
+
+ return (
+ {
+ e.stopPropagation();
+ selectionHelpers.handleEntityClick(e, id, groupId);
+ }}
+ checked={isSelected}
+ data-entity-group-id={groupId}
+ data-entity-id={id}
+ disabled={disabled}
+ readOnly
+ />
+ );
+};
diff --git a/web/components/core/multiple-select/group-select-action.tsx b/web/components/core/multiple-select/group-select-action.tsx
new file mode 100644
index 00000000000..ae2532153a9
--- /dev/null
+++ b/web/components/core/multiple-select/group-select-action.tsx
@@ -0,0 +1,30 @@
+// ui
+import { Checkbox } from "@plane/ui";
+// helpers
+import { cn } from "@/helpers/common.helper";
+// hooks
+import { TSelectionHelper } from "@/hooks/use-multiple-select";
+
+type Props = {
+ className?: string;
+ disabled?: boolean;
+ groupID: string;
+ selectionHelpers: TSelectionHelper;
+};
+
+export const MultipleSelectGroupAction: React.FC = (props) => {
+ const { className, disabled = false, groupID, selectionHelpers } = props;
+ // derived values
+ const groupSelectionStatus = selectionHelpers.isGroupSelected(groupID);
+
+ return (
+ selectionHelpers.handleGroupClick(groupID)}
+ checked={groupSelectionStatus === "complete"}
+ indeterminate={groupSelectionStatus === "partial"}
+ disabled={disabled}
+ />
+ );
+};
diff --git a/web/components/core/multiple-select/index.ts b/web/components/core/multiple-select/index.ts
new file mode 100644
index 00000000000..b2cdf13c361
--- /dev/null
+++ b/web/components/core/multiple-select/index.ts
@@ -0,0 +1,3 @@
+export * from "./entity-select-action";
+export * from "./group-select-action";
+export * from "./select-group";
diff --git a/web/components/core/multiple-select/select-group.tsx b/web/components/core/multiple-select/select-group.tsx
new file mode 100644
index 00000000000..6f47b063292
--- /dev/null
+++ b/web/components/core/multiple-select/select-group.tsx
@@ -0,0 +1,22 @@
+import { observer } from "mobx-react";
+// hooks
+import { TSelectionHelper, useMultipleSelect } from "@/hooks/use-multiple-select";
+
+type Props = {
+ children: (helpers: TSelectionHelper) => React.ReactNode;
+ containerRef: React.MutableRefObject;
+ entities: Record; // { groupID: entityIds[] }
+};
+
+export const MultipleSelectGroup: React.FC = observer((props) => {
+ const { children, containerRef, entities } = props;
+
+ const helpers = useMultipleSelect({
+ containerRef,
+ entities,
+ });
+
+ return <>{children(helpers)}>;
+});
+
+MultipleSelectGroup.displayName = "MultipleSelectGroup";
diff --git a/web/components/core/sidebar/progress-chart.tsx b/web/components/core/sidebar/progress-chart.tsx
index 68b1708fe82..5f0e56fb77b 100644
--- a/web/components/core/sidebar/progress-chart.tsx
+++ b/web/components/core/sidebar/progress-chart.tsx
@@ -56,21 +56,18 @@ const ProgressChart: React.FC = ({ distribution, startDate, endDate, tota
dates = eachDayOfInterval({ start, end });
}
- const maxDates = 4;
- const totalDates = dates.length;
+ if (dates.length === 0) return [];
- if (totalDates <= maxDates) return dates.map((d) => renderFormattedDateWithoutYear(d));
- else {
- const interval = Math.ceil(totalDates / maxDates);
- const limitedDates = [];
+ const formattedDates = dates.map((d) => renderFormattedDateWithoutYear(d));
+ const firstDate = formattedDates[0];
+ const lastDate = formattedDates[formattedDates.length - 1];
- for (let i = 0; i < totalDates; i += interval) limitedDates.push(renderFormattedDateWithoutYear(dates[i]));
+ if (formattedDates.length <= 2) return [firstDate, lastDate];
- if (!limitedDates.includes(renderFormattedDateWithoutYear(dates[totalDates - 1])))
- limitedDates.push(renderFormattedDateWithoutYear(dates[totalDates - 1]));
+ const middleDateIndex = Math.floor(formattedDates.length / 2);
+ const middleDate = formattedDates[middleDateIndex];
- return limitedDates;
- }
+ return [firstDate, middleDate, lastDate];
};
return (
diff --git a/web/components/core/sidebar/sidebar-progress-stats.tsx b/web/components/core/sidebar/sidebar-progress-stats.tsx
index db9d94a8fd7..0194ba01f19 100644
--- a/web/components/core/sidebar/sidebar-progress-stats.tsx
+++ b/web/components/core/sidebar/sidebar-progress-stats.tsx
@@ -1,5 +1,5 @@
import React from "react";
-
+import { observer } from "mobx-react";
import Image from "next/image";
// headless ui
import { Tab } from "@headlessui/react";
@@ -15,6 +15,7 @@ import {
// hooks
import { Avatar, StateGroupIcon } from "@plane/ui";
import { SingleProgressStats } from "@/components/core";
+import { useProjectState } from "@/hooks/store";
import useLocalStorage from "@/hooks/use-local-storage";
// images
import emptyLabel from "public/empty-state/empty_label.svg";
@@ -44,20 +45,23 @@ type Props = {
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
};
-export const SidebarProgressStats: React.FC = ({
- distribution,
- groupedIssues,
- totalIssues,
- module,
- roundedTab,
- noBackground,
- isPeekView = false,
- isCompleted = false,
- filters,
- handleFiltersUpdate,
-}) => {
+export const SidebarProgressStats: React.FC = observer((props) => {
+ const {
+ distribution,
+ groupedIssues,
+ totalIssues,
+ module,
+ roundedTab,
+ noBackground,
+ isPeekView = false,
+ isCompleted = false,
+ filters,
+ handleFiltersUpdate,
+ } = props;
const { storedValue: tab, setValue: setTab } = useLocalStorage("tab", "Assignees");
+ const { groupedProjectStates } = useProjectState();
+
const currentValue = (tab: string | null) => {
switch (tab) {
case "Assignees":
@@ -71,6 +75,12 @@ export const SidebarProgressStats: React.FC = ({
}
};
+ const getStateGroupState = (stateGroup: string) => {
+ const stateGroupStates = groupedProjectStates?.[stateGroup];
+ const stateGroupStatesId = stateGroupStates?.map((state) => state.id);
+ return stateGroupStatesId;
+ };
+
return (
= ({
}
completed={groupedIssues[group]}
total={totalIssues}
+ {...(!isPeekView &&
+ !isCompleted && {
+ onClick: () => handleFiltersUpdate("state", getStateGroupState(group) ?? []),
+ })}
/>
))}
);
-};
+});
diff --git a/web/components/cycles/active-cycle/productivity.tsx b/web/components/cycles/active-cycle/productivity.tsx
index e270b5ad8a6..32d17df759d 100644
--- a/web/components/cycles/active-cycle/productivity.tsx
+++ b/web/components/cycles/active-cycle/productivity.tsx
@@ -1,4 +1,5 @@
import { FC } from "react";
+import Link from "next/link";
// types
import { ICycle } from "@plane/types";
// components
@@ -8,14 +9,19 @@ import { EmptyState } from "@/components/empty-state";
import { EmptyStateType } from "@/constants/empty-state";
export type ActiveCycleProductivityProps = {
+ workspaceSlug: string;
+ projectId: string;
cycle: ICycle;
};
export const ActiveCycleProductivity: FC = (props) => {
- const { cycle } = props;
+ const { workspaceSlug, projectId, cycle } = props;
return (
-
+
Issue burndown
@@ -53,6 +59,6 @@ export const ActiveCycleProductivity: FC
= (props)
>
)}
-
+
);
};
diff --git a/web/components/cycles/active-cycle/progress.tsx b/web/components/cycles/active-cycle/progress.tsx
index 6aae998bed1..fd537148ce1 100644
--- a/web/components/cycles/active-cycle/progress.tsx
+++ b/web/components/cycles/active-cycle/progress.tsx
@@ -1,4 +1,5 @@
import { FC } from "react";
+import Link from "next/link";
// types
import { ICycle } from "@plane/types";
// ui
@@ -10,11 +11,13 @@ import { CYCLE_STATE_GROUPS_DETAILS } from "@/constants/cycle";
import { EmptyStateType } from "@/constants/empty-state";
export type ActiveCycleProgressProps = {
+ workspaceSlug: string;
+ projectId: string;
cycle: ICycle;
};
export const ActiveCycleProgress: FC
= (props) => {
- const { cycle } = props;
+ const { workspaceSlug, projectId, cycle } = props;
const progressIndicatorData = CYCLE_STATE_GROUPS_DETAILS.map((group, index) => ({
id: index,
@@ -31,7 +34,10 @@ export const ActiveCycleProgress: FC = (props) => {
};
return (
-
+
Progress
@@ -85,6 +91,6 @@ export const ActiveCycleProgress: FC
= (props) => {
)}
-
+
);
};
diff --git a/web/components/cycles/active-cycle/root.tsx b/web/components/cycles/active-cycle/root.tsx
index 625210fd49c..8b51a692bf7 100644
--- a/web/components/cycles/active-cycle/root.tsx
+++ b/web/components/cycles/active-cycle/root.tsx
@@ -62,13 +62,18 @@ export const ActiveCycleRoot: React.FC
= observer((props) =
cycleId={currentProjectActiveCycleId}
workspaceSlug={workspaceSlug}
projectId={projectId}
+ className="!border-b-transparent"
/>
)}
-
-
diff --git a/web/components/cycles/active-cycle/upcoming-cycles-list-item.tsx b/web/components/cycles/active-cycle/upcoming-cycles-list-item.tsx
index 9b63b0f6f61..a66af73c39f 100644
--- a/web/components/cycles/active-cycle/upcoming-cycles-list-item.tsx
+++ b/web/components/cycles/active-cycle/upcoming-cycles-list-item.tsx
@@ -2,7 +2,7 @@ import { useRef } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useRouter } from "next/router";
-import { User2 } from "lucide-react";
+import { Users } from "lucide-react";
// ui
import { Avatar, AvatarGroup, setPromiseToast } from "@plane/ui";
// components
@@ -112,9 +112,7 @@ export const UpcomingCycleListItem: React.FC
= observer((props) => {
})}
) : (
-
-
-
+
)}
= observer((props) => {
= observer((props) => {
})}
) : (
-
-
-
+
)}
diff --git a/web/components/cycles/list/cycles-list-item.tsx b/web/components/cycles/list/cycles-list-item.tsx
index 92c11dd6919..414c8081a19 100644
--- a/web/components/cycles/list/cycles-list-item.tsx
+++ b/web/components/cycles/list/cycles-list-item.tsx
@@ -22,10 +22,11 @@ type TCyclesListItem = {
handleRemoveFromFavorites?: () => void;
workspaceSlug: string;
projectId: string;
+ className?: string;
};
export const CyclesListItem: FC = observer((props) => {
- const { cycleId, workspaceSlug, projectId } = props;
+ const { cycleId, workspaceSlug, projectId, className = "" } = props;
// refs
const parentRef = useRef(null);
// router
@@ -76,13 +77,19 @@ export const CyclesListItem: FC = observer((props) => {
}
};
+ // handlers
+ const handleArchivedCycleClick = (e: MouseEvent) => {
+ openCycleOverview(e);
+ };
+
+ const handleItemClick = cycleDetails.archived_at ? handleArchivedCycleClick : undefined;
+
return (
{
- if (cycleDetails.archived_at) openCycleOverview(e);
- }}
+ onItemClick={handleItemClick}
+ className={className}
prependTitleElement={
{isCompleted ? (
diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx
index 2aa3afc48cd..595fe9b7a48 100644
--- a/web/components/cycles/sidebar.tsx
+++ b/web/components/cycles/sidebar.tsx
@@ -1,5 +1,6 @@
import React, { useCallback, useEffect, useState } from "react";
import isEmpty from "lodash/isEmpty";
+import isEqual from "lodash/isEqual";
import { observer } from "mobx-react";
import { useRouter } from "next/router";
import { Controller, useForm } from "react-hook-form";
@@ -9,10 +10,10 @@ import {
ChevronDown,
LinkIcon,
Trash2,
- UserCircle2,
AlertCircle,
ChevronRight,
CalendarClock,
+ SquareUser,
} from "lucide-react";
import { Disclosure, Transition } from "@headlessui/react";
// types
@@ -199,14 +200,18 @@ export const CycleDetailsSidebar: React.FC = observer((props) => {
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
- const newValues = issueFilters?.filters?.[key] ?? [];
+ let newValues = issueFilters?.filters?.[key] ?? [];
if (Array.isArray(value)) {
- // this validation is majorly for the filter start_date, target_date custom
- value.forEach((val) => {
- if (!newValues.includes(val)) newValues.push(val);
- else newValues.splice(newValues.indexOf(val), 1);
- });
+ if (key === "state") {
+ if (isEqual(newValues, value)) newValues = [];
+ else newValues = value;
+ } else {
+ value.forEach((val) => {
+ if (!newValues.includes(val)) newValues.push(val);
+ else newValues.splice(newValues.indexOf(val), 1);
+ });
+ }
} else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
@@ -427,7 +432,7 @@ export const CycleDetailsSidebar: React.FC = observer((props) => {
-
+
Lead
diff --git a/web/components/dashboard/widgets/recent-projects.tsx b/web/components/dashboard/widgets/recent-projects.tsx
index 24c85b6f228..803edc8e28a 100644
--- a/web/components/dashboard/widgets/recent-projects.tsx
+++ b/web/components/dashboard/widgets/recent-projects.tsx
@@ -7,8 +7,8 @@ import { TRecentProjectsWidgetResponse } from "@plane/types";
// ui
import { Avatar, AvatarGroup } from "@plane/ui";
// components
+import { Logo } from "@/components/common";
import { WidgetLoader, WidgetProps } from "@/components/dashboard/widgets";
-import { ProjectLogo } from "@/components/project";
// constants
import { PROJECT_BACKGROUND_COLORS } from "@/constants/dashboard";
import { EUserWorkspaceRoles } from "@/constants/workspace";
@@ -38,7 +38,7 @@ const ProjectListItem: React.FC
= observer((props) => {
className={`grid h-[3.375rem] w-[3.375rem] flex-shrink-0 place-items-center rounded border border-transparent ${randomBgColor}`}
>
diff --git a/web/components/dropdowns/member/avatar.tsx b/web/components/dropdowns/member/avatar.tsx
index 15e1fbd8c8f..868c286654f 100644
--- a/web/components/dropdowns/member/avatar.tsx
+++ b/web/components/dropdowns/member/avatar.tsx
@@ -1,16 +1,19 @@
import { observer } from "mobx-react";
+// icons
+import { LucideIcon, Users } from "lucide-react";
+// ui
+import { Avatar, AvatarGroup } from "@plane/ui";
// hooks
-import { Avatar, AvatarGroup, UserGroupIcon } from "@plane/ui";
import { useMember } from "@/hooks/store";
-// ui
type AvatarProps = {
showTooltip: boolean;
userIds: string | string[] | null;
+ icon?: LucideIcon;
};
export const ButtonAvatars: React.FC
= observer((props) => {
- const { showTooltip, userIds } = props;
+ const { showTooltip, userIds, icon: Icon } = props;
// store hooks
const { getUserDetails } = useMember();
@@ -33,5 +36,9 @@ export const ButtonAvatars: React.FC = observer((props) => {
}
}
- return ;
+ return Icon ? (
+
+ ) : (
+
+ );
});
diff --git a/web/components/dropdowns/member/index.tsx b/web/components/dropdowns/member/index.tsx
index d14cf316fad..7af6f4fe1b6 100644
--- a/web/components/dropdowns/member/index.tsx
+++ b/web/components/dropdowns/member/index.tsx
@@ -1,6 +1,6 @@
import { Fragment, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
-import { ChevronDown } from "lucide-react";
+import { ChevronDown, LucideIcon } from "lucide-react";
// headless ui
import { Combobox } from "@headlessui/react";
// helpers
@@ -19,6 +19,7 @@ import { MemberDropdownProps } from "./types";
type Props = {
projectId?: string;
+ icon?: LucideIcon;
onClose?: () => void;
} & MemberDropdownProps;
@@ -43,6 +44,7 @@ export const MemberDropdown: React.FC = observer((props) => {
showTooltip = false,
tabIndex,
value,
+ icon,
} = props;
// states
const [isOpen, setIsOpen] = useState(false);
@@ -115,7 +117,7 @@ export const MemberDropdown: React.FC = observer((props) => {
showTooltip={showTooltip}
variant={buttonVariant}
>
- {!hideIcon && }
+ {!hideIcon && }
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
{Array.isArray(value) && value.length > 0
diff --git a/web/components/dropdowns/priority.tsx b/web/components/dropdowns/priority.tsx
index 1bf0bc933c1..382911b082f 100644
--- a/web/components/dropdowns/priority.tsx
+++ b/web/components/dropdowns/priority.tsx
@@ -1,7 +1,7 @@
import { Fragment, ReactNode, useRef, useState } from "react";
import { useTheme } from "next-themes";
import { usePopper } from "react-popper";
-import { Check, ChevronDown, Search } from "lucide-react";
+import { Check, ChevronDown, Search, SignalHigh } from "lucide-react";
import { Combobox } from "@headlessui/react";
// types
import { TIssuePriorities } from "@plane/types";
@@ -26,7 +26,7 @@ type Props = TDropdownProps & {
highlightUrgent?: boolean;
onChange: (val: TIssuePriorities) => void;
onClose?: () => void;
- value: TIssuePriorities;
+ value: TIssuePriorities | undefined;
};
type ButtonProps = {
@@ -37,7 +37,8 @@ type ButtonProps = {
hideText?: boolean;
isActive?: boolean;
highlightUrgent: boolean;
- priority: TIssuePriorities;
+ placeholder: string;
+ priority: TIssuePriorities | undefined;
showTooltip: boolean;
};
@@ -49,6 +50,7 @@ const BorderButton = (props: ButtonProps) => {
hideIcon = false,
hideText = false,
highlightUrgent,
+ placeholder,
priority,
showTooltip,
} = props;
@@ -75,7 +77,7 @@ const BorderButton = (props: ButtonProps) => {
{
className
)}
>
- {!hideIcon && (
-
- )}
- {!hideText &&
{priorityDetails?.title} }
+ >
+
+
+ ) : (
+
+ ))}
+ {!hideText && {priorityDetails?.title ?? placeholder} }
{dropdownArrow && (
)}
@@ -125,6 +130,7 @@ const BackgroundButton = (props: ButtonProps) => {
hideIcon = false,
hideText = false,
highlightUrgent,
+ placeholder,
priority,
showTooltip,
} = props;
@@ -151,7 +157,7 @@ const BackgroundButton = (props: ButtonProps) => {
{
className
)}
>
- {!hideIcon && (
-
- )}
- {!hideText &&
{priorityDetails?.title} }
+ >
+
+
+ ) : (
+
+ ))}
+ {!hideText && {priorityDetails?.title ?? placeholder} }
{dropdownArrow && (
)}
@@ -202,6 +211,7 @@ const TransparentButton = (props: ButtonProps) => {
hideText = false,
isActive = false,
highlightUrgent,
+ placeholder,
priority,
showTooltip,
} = props;
@@ -228,7 +238,7 @@ const TransparentButton = (props: ButtonProps) => {
{
className
)}
>
- {!hideIcon && (
-
- )}
- {!hideText &&
{priorityDetails?.title} }
+ >
+
+
+ ) : (
+
+ ))}
+ {!hideText && {priorityDetails?.title ?? placeholder} }
{dropdownArrow && (
)}
@@ -285,6 +298,7 @@ export const PriorityDropdown: React.FC = (props) => {
highlightUrgent = true,
onChange,
onClose,
+ placeholder = "Priority",
placement,
showTooltip = false,
tabIndex,
@@ -400,6 +414,7 @@ export const PriorityDropdown: React.FC = (props) => {
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
+ placeholder={placeholder}
showTooltip={showTooltip}
hideText={BUTTON_VARIANTS_WITHOUT_TEXT.includes(buttonVariant)}
/>
diff --git a/web/components/dropdowns/project.tsx b/web/components/dropdowns/project.tsx
index c6a0c1bb4a0..ea7dea5490f 100644
--- a/web/components/dropdowns/project.tsx
+++ b/web/components/dropdowns/project.tsx
@@ -6,7 +6,7 @@ import { Combobox } from "@headlessui/react";
// types
import { IProject } from "@plane/types";
// components
-import { ProjectLogo } from "@/components/project";
+import { Logo } from "@/components/common";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
@@ -83,7 +83,7 @@ export const ProjectDropdown: React.FC = observer((props) => {
{projectDetails && (
-
+
)}
{projectDetails?.name}
@@ -157,7 +157,7 @@ export const ProjectDropdown: React.FC
= observer((props) => {
>
{!hideIcon && selectedProject && (
-
+
)}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
diff --git a/web/components/dropdowns/state.tsx b/web/components/dropdowns/state.tsx
index c6f54f99321..7408a8d1650 100644
--- a/web/components/dropdowns/state.tsx
+++ b/web/components/dropdowns/state.tsx
@@ -24,7 +24,8 @@ type Props = TDropdownProps & {
onChange: (val: string) => void;
onClose?: () => void;
projectId: string;
- value: string;
+ showDefaultState?: boolean;
+ value: string | undefined;
};
export const StateDropdown: React.FC = observer((props) => {
@@ -42,6 +43,7 @@ export const StateDropdown: React.FC = observer((props) => {
onClose,
placement,
projectId,
+ showDefaultState = true,
showTooltip = false,
tabIndex,
value,
@@ -72,8 +74,8 @@ export const StateDropdown: React.FC = observer((props) => {
const { workspaceSlug } = useAppRouter();
const { fetchProjectStates, getProjectStates, getStateById } = useProjectState();
const statesList = getProjectStates(projectId);
- const defaultStateList = statesList?.find((state) => state.default);
- const stateValue = value ? value : defaultStateList?.id;
+ const defaultState = statesList?.find((state) => state.default);
+ const stateValue = value ?? (showDefaultState ? defaultState?.id : undefined);
const options = statesList?.map((state) => ({
value: state.id,
@@ -170,7 +172,7 @@ export const StateDropdown: React.FC = observer((props) => {
{!hideIcon && (
)}
diff --git a/web/components/estimates/delete-estimate-modal.tsx b/web/components/estimates/delete-estimate-modal.tsx
index e77c3ecb033..d0949316f7c 100644
--- a/web/components/estimates/delete-estimate-modal.tsx
+++ b/web/components/estimates/delete-estimate-modal.tsx
@@ -64,7 +64,7 @@ export const DeleteEstimateModal: React.FC = observer((props) => {
{
icon={
currentProjectDetails && (
-
+
)
}
diff --git a/web/components/headers/cycles.tsx b/web/components/headers/cycles.tsx
index 7b78e27fd23..76493bd5109 100644
--- a/web/components/headers/cycles.tsx
+++ b/web/components/headers/cycles.tsx
@@ -4,9 +4,8 @@ import { useRouter } from "next/router";
// ui
import { Breadcrumbs, Button, ContrastIcon } from "@plane/ui";
// components
-import { BreadcrumbLink } from "@/components/common";
+import { BreadcrumbLink, Logo } from "@/components/common";
import { CyclesViewHeader } from "@/components/cycles";
-import { ProjectLogo } from "@/components/project";
// constants
import { EUserProjectRoles } from "@/constants/project";
// hooks
@@ -41,7 +40,7 @@ export const CyclesHeader: FC = observer(() => {
icon={
currentProjectDetails && (
-
+
)
}
diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx
index 538eca2cde8..119cb9a9447 100644
--- a/web/components/headers/module-issues.tsx
+++ b/web/components/headers/module-issues.tsx
@@ -10,9 +10,8 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip } from "@plane/ui";
// components
import { ProjectAnalyticsModal } from "@/components/analytics";
-import { BreadcrumbLink } from "@/components/common";
+import { BreadcrumbLink, Logo } from "@/components/common";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
-import { ProjectLogo } from "@/components/project";
// constants
import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
import { EUserProjectRoles } from "@/constants/project";
@@ -170,7 +169,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
icon={
currentProjectDetails && (
-
+
)
}
diff --git a/web/components/headers/modules-list.tsx b/web/components/headers/modules-list.tsx
index 90866d73ebb..0e1fd53fc0a 100644
--- a/web/components/headers/modules-list.tsx
+++ b/web/components/headers/modules-list.tsx
@@ -3,9 +3,8 @@ import { useRouter } from "next/router";
// ui
import { Breadcrumbs, Button, DiceIcon } from "@plane/ui";
// components
-import { BreadcrumbLink } from "@/components/common";
+import { BreadcrumbLink, Logo } from "@/components/common";
import { ModuleViewHeader } from "@/components/modules";
-import { ProjectLogo } from "@/components/project";
// constants
import { EUserProjectRoles } from "@/constants/project";
// hooks
@@ -41,7 +40,7 @@ export const ModulesListHeader: React.FC = observer(() => {
icon={
currentProjectDetails && (
-
+
)
}
diff --git a/web/components/headers/page-details.tsx b/web/components/headers/page-details.tsx
index 0a02c1528de..94ecd99574d 100644
--- a/web/components/headers/page-details.tsx
+++ b/web/components/headers/page-details.tsx
@@ -1,30 +1,56 @@
-import { FC } from "react";
+import { useState } from "react";
import { observer } from "mobx-react";
import { useRouter } from "next/router";
import { FileText } from "lucide-react";
-// hooks
+// types
+import { TLogoProps } from "@plane/types";
// ui
-import { Breadcrumbs, Button } from "@plane/ui";
-// helpers
-import { BreadcrumbLink } from "@/components/common";
+import { Breadcrumbs, Button, EmojiIconPicker, EmojiIconPickerTypes, TOAST_TYPE, setToast } from "@plane/ui";
// components
-import { ProjectLogo } from "@/components/project";
-import { useCommandPalette, usePage, useProject } from "@/hooks/store";
+import { BreadcrumbLink, Logo } from "@/components/common";
+// helper
+import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper";
+// hooks
+import { usePage, useProject } from "@/hooks/store";
+import { usePlatformOS } from "@/hooks/use-platform-os";
export interface IPagesHeaderProps {
showButton?: boolean;
}
-export const PageDetailsHeader: FC = observer((props) => {
- const { showButton = false } = props;
+export const PageDetailsHeader = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, pageId } = router.query;
+ // state
+ const [isOpen, setIsOpen] = useState(false);
// store hooks
- const { toggleCreatePageModal } = useCommandPalette();
const { currentProjectDetails } = useProject();
+ const { isContentEditable, isSubmitting, name, logo_props, updatePageLogo } = usePage(pageId?.toString() ?? "");
- const { name } = usePage(pageId?.toString() ?? "");
+ const handlePageLogoUpdate = async (data: TLogoProps) => {
+ if (data) {
+ updatePageLogo(data)
+ .then(() => {
+ setToast({
+ type: TOAST_TYPE.SUCCESS,
+ title: "Success!",
+ message: "Logo Updated successfully.",
+ });
+ })
+ .catch(() => {
+ setToast({
+ type: TOAST_TYPE.ERROR,
+ title: "Error!",
+ message: "Something went wrong. Please try again.",
+ });
+ });
+ }
+ };
+ // use platform
+ const { platform } = usePlatformOS();
+ // derived values
+ const isMac = platform === "MacOS";
return (
@@ -42,7 +68,7 @@ export const PageDetailsHeader: FC
= observer((props) => {
icon={
currentProjectDetails && (
-
+
)
}
@@ -71,18 +97,72 @@ export const PageDetailsHeader: FC = observer((props) => {
} />
+ setIsOpen(val)}
+ className="flex items-center justify-center"
+ buttonClassName="flex items-center justify-center"
+ label={
+ <>
+ {logo_props?.in_use ? (
+
+ ) : (
+
+ )}
+ >
+ }
+ onChange={(val) => {
+ let logoValue = {};
+
+ if (val?.type === "emoji")
+ logoValue = {
+ value: convertHexEmojiToDecimal(val.value.unified),
+ url: val.value.imageUrl,
+ };
+ else if (val?.type === "icon") logoValue = val.value;
+
+ handlePageLogoUpdate({
+ in_use: val?.type,
+ [val?.type]: logoValue,
+ }).finally(() => setIsOpen(false));
+ }}
+ defaultIconColor={
+ logo_props?.in_use && logo_props.in_use === "icon" ? logo_props?.icon?.color : undefined
+ }
+ defaultOpen={
+ logo_props?.in_use && logo_props?.in_use === "emoji"
+ ? EmojiIconPickerTypes.EMOJI
+ : EmojiIconPickerTypes.ICON
+ }
+ />
+ }
+ />
}
/>
- {showButton && (
-
- toggleCreatePageModal(true)}>
- Add Page
-
-
+ {isContentEditable && (
+ {
+ // ctrl/cmd + s to save the changes
+ const event = new KeyboardEvent("keydown", {
+ key: "s",
+ ctrlKey: !isMac,
+ metaKey: isMac,
+ });
+ window.dispatchEvent(event);
+ }}
+ className="flex-shrink-0"
+ loading={isSubmitting === "submitting"}
+ >
+ {isSubmitting === "submitting" ? "Saving" : "Save changes"}
+
)}
);
diff --git a/web/components/headers/pages.tsx b/web/components/headers/pages.tsx
index 7ab9cb75d1a..893c4409df0 100644
--- a/web/components/headers/pages.tsx
+++ b/web/components/headers/pages.tsx
@@ -1,21 +1,20 @@
import { observer } from "mobx-react";
import { useRouter } from "next/router";
import { FileText } from "lucide-react";
-// hooks
// ui
import { Breadcrumbs, Button } from "@plane/ui";
// helpers
-import { BreadcrumbLink } from "@/components/common";
-import { ProjectLogo } from "@/components/project";
-import { EUserProjectRoles } from "@/constants/project";
+import { BreadcrumbLink, Logo } from "@/components/common";
// constants
-// components
+import { EPageAccess } from "@/constants/page";
+import { EUserProjectRoles } from "@/constants/project";
+// hooks
import { useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store";
export const PagesHeader = observer(() => {
// router
const router = useRouter();
- const { workspaceSlug } = router.query;
+ const { workspaceSlug, type: pageType } = router.query;
// store hooks
const { toggleCreatePageModal } = useCommandPalette();
const {
@@ -41,7 +40,7 @@ export const PagesHeader = observer(() => {
icon={
currentProjectDetails && (
-
+
)
}
@@ -62,7 +61,10 @@ export const PagesHeader = observer(() => {
size="sm"
onClick={() => {
setTrackElement("Project pages page");
- toggleCreatePageModal(true);
+ toggleCreatePageModal({
+ isOpen: true,
+ pageAccess: pageType === "private" ? EPageAccess.PRIVATE : EPageAccess.PUBLIC,
+ });
}}
>
Add Page
diff --git a/web/components/headers/project-archived-issue-details.tsx b/web/components/headers/project-archived-issue-details.tsx
index e32528e821b..c874745a4bc 100644
--- a/web/components/headers/project-archived-issue-details.tsx
+++ b/web/components/headers/project-archived-issue-details.tsx
@@ -4,8 +4,7 @@ import { useRouter } from "next/router";
import useSWR from "swr";
// hooks
import { ArchiveIcon, Breadcrumbs, LayersIcon } from "@plane/ui";
-import { BreadcrumbLink } from "@/components/common";
-import { ProjectLogo } from "@/components/project";
+import { BreadcrumbLink, Logo } from "@/components/common";
import { ISSUE_DETAILS } from "@/constants/fetch-keys";
import { useProject } from "@/hooks/store";
// components
@@ -52,7 +51,7 @@ export const ProjectArchivedIssueDetailsHeader: FC = observer(() => {
icon={
currentProjectDetails && (
-
+
)
}
diff --git a/web/components/headers/project-archives.tsx b/web/components/headers/project-archives.tsx
index 6e5638c7144..5022414613d 100644
--- a/web/components/headers/project-archives.tsx
+++ b/web/components/headers/project-archives.tsx
@@ -4,8 +4,7 @@ import { useRouter } from "next/router";
// ui
import { ArchiveIcon, Breadcrumbs, Tooltip } from "@plane/ui";
// components
-import { BreadcrumbLink } from "@/components/common";
-import { ProjectLogo } from "@/components/project";
+import { BreadcrumbLink, Logo } from "@/components/common";
// constants
import { PROJECT_ARCHIVES_BREADCRUMB_LIST } from "@/constants/archives";
import { EIssuesStoreType } from "@/constants/issue";
@@ -49,7 +48,7 @@ export const ProjectArchivesHeader: FC = observer(() => {
icon={
currentProjectDetails && (
-
+
)
}
diff --git a/web/components/headers/project-draft-issues.tsx b/web/components/headers/project-draft-issues.tsx
index f6de97b5240..8c8a25c9efd 100644
--- a/web/components/headers/project-draft-issues.tsx
+++ b/web/components/headers/project-draft-issues.tsx
@@ -6,9 +6,8 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
// ui
import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui";
// components
-import { BreadcrumbLink } from "@/components/common";
+import { BreadcrumbLink, Logo } from "@/components/common";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
-import { ProjectLogo } from "@/components/project";
// constants
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
// helpers
@@ -101,7 +100,7 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
icon={
currentProjectDetails && (
-
+
)
}
diff --git a/web/components/headers/project-inbox.tsx b/web/components/headers/project-inbox.tsx
index d61e2492db3..ce76f3e4066 100644
--- a/web/components/headers/project-inbox.tsx
+++ b/web/components/headers/project-inbox.tsx
@@ -5,9 +5,8 @@ import { RefreshCcw } from "lucide-react";
// ui
import { Breadcrumbs, Button, LayersIcon } from "@plane/ui";
// components
-import { BreadcrumbLink } from "@/components/common";
+import { BreadcrumbLink, Logo } from "@/components/common";
import { InboxIssueCreateEditModalRoot } from "@/components/inbox";
-import { ProjectLogo } from "@/components/project";
// hooks
import { useProject, useProjectInbox } from "@/hooks/store";
@@ -35,7 +34,7 @@ export const ProjectInboxHeader: FC = observer(() => {
icon={
currentProjectDetails && (
-
+
)
}
@@ -46,7 +45,7 @@ export const ProjectInboxHeader: FC = observer(() => {
} />
+
} />
}
/>
diff --git a/web/components/headers/project-issue-details.tsx b/web/components/headers/project-issue-details.tsx
index 176732ca5d8..890bd59e50d 100644
--- a/web/components/headers/project-issue-details.tsx
+++ b/web/components/headers/project-issue-details.tsx
@@ -4,8 +4,7 @@ import { useRouter } from "next/router";
// hooks
import { PanelRight } from "lucide-react";
import { Breadcrumbs, LayersIcon } from "@plane/ui";
-import { BreadcrumbLink } from "@/components/common";
-import { ProjectLogo } from "@/components/project";
+import { BreadcrumbLink, Logo } from "@/components/common";
import { cn } from "@/helpers/common.helper";
import { useAppTheme, useIssueDetail, useProject } from "@/hooks/store";
// ui
@@ -42,7 +41,7 @@ export const ProjectIssueDetailsHeader: FC = observer(() => {
icon={
currentProjectDetails && (
-
+
)
}
diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx
index 8ba44719e93..7042d2c28d6 100644
--- a/web/components/headers/project-issues.tsx
+++ b/web/components/headers/project-issues.tsx
@@ -9,9 +9,8 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
import { Breadcrumbs, Button, LayersIcon, Tooltip } from "@plane/ui";
// components
import { ProjectAnalyticsModal } from "@/components/analytics";
-import { BreadcrumbLink } from "@/components/common";
+import { BreadcrumbLink, Logo } from "@/components/common";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
-import { ProjectLogo } from "@/components/project";
// constants
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
import { EUserProjectRoles } from "@/constants/project";
@@ -130,7 +129,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
currentProjectDetails ? (
currentProjectDetails && (
-
+
)
) : (
diff --git a/web/components/headers/project-settings.tsx b/web/components/headers/project-settings.tsx
index f25bfe8040f..2fe48969d42 100644
--- a/web/components/headers/project-settings.tsx
+++ b/web/components/headers/project-settings.tsx
@@ -2,22 +2,16 @@ import { FC } from "react";
import { observer } from "mobx-react";
import { useRouter } from "next/router";
// ui
+import { Settings } from "lucide-react";
import { Breadcrumbs, CustomMenu } from "@plane/ui";
-// helper
-import { BreadcrumbLink } from "@/components/common";
-import { ProjectLogo } from "@/components/project";
+// components
+import { BreadcrumbLink, Logo } from "@/components/common";
+// constants
import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "@/constants/project";
// hooks
import { useProject, useUser } from "@/hooks/store";
-// constants
-// components
-
-export interface IProjectSettingHeader {
- title: string;
-}
-export const ProjectSettingHeader: FC
= observer((props) => {
- const { title } = props;
+export const ProjectSettingHeader: FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@@ -44,7 +38,7 @@ export const ProjectSettingHeader: FC = observer((props)
icon={
currentProjectDetails && (
-
+
)
}
@@ -52,7 +46,12 @@ export const ProjectSettingHeader: FC = observer((props)
}
/>
- } />
+ } />
+ }
+ />
@@ -62,7 +61,7 @@ export const ProjectSettingHeader: FC = observer((props)
maxHeight="lg"
customButton={
- {title}
+ Settings
}
placement="bottom-start"
diff --git a/web/components/headers/project-view-issues.tsx b/web/components/headers/project-view-issues.tsx
index 297c976eeab..0e8f59e6c3d 100644
--- a/web/components/headers/project-view-issues.tsx
+++ b/web/components/headers/project-view-issues.tsx
@@ -7,9 +7,8 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
// ui
import { Breadcrumbs, Button, CustomMenu, PhotoFilterIcon } from "@plane/ui";
// components
-import { BreadcrumbLink } from "@/components/common";
+import { BreadcrumbLink, Logo } from "@/components/common";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
-import { ProjectLogo } from "@/components/project";
// constants
import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
import { EUserProjectRoles } from "@/constants/project";
@@ -141,7 +140,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
icon={
currentProjectDetails && (
-
+
)
}
@@ -164,7 +163,11 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
-
+ {viewDetails?.logo_props?.in_use ? (
+
+ ) : (
+
+ )}
{viewDetails?.name && truncateText(viewDetails.name, 40)}
>
}
@@ -182,7 +185,11 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
href={`/${workspaceSlug}/projects/${projectId}/views/${viewId}`}
className="flex items-center gap-1.5"
>
-
+ {view?.logo_props?.in_use ? (
+
+ ) : (
+
+ )}
{truncateText(view.name, 40)}
diff --git a/web/components/headers/project-views.tsx b/web/components/headers/project-views.tsx
index 3cd5788470d..7f1d1a725eb 100644
--- a/web/components/headers/project-views.tsx
+++ b/web/components/headers/project-views.tsx
@@ -1,14 +1,13 @@
import { observer } from "mobx-react";
import { useRouter } from "next/router";
-// hooks
-// components
+// ui
import { Breadcrumbs, PhotoFilterIcon, Button } from "@plane/ui";
-import { BreadcrumbLink } from "@/components/common";
-// helpers
-import { ProjectLogo } from "@/components/project";
+// components
+import { BreadcrumbLink, Logo } from "@/components/common";
import { ViewListHeader } from "@/components/views";
-import { EUserProjectRoles } from "@/constants/project";
// constants
+import { EUserProjectRoles } from "@/constants/project";
+// hooks
import { useCommandPalette, useProject, useUser } from "@/hooks/store";
export const ProjectViewsHeader: React.FC = observer(() => {
@@ -40,7 +39,7 @@ export const ProjectViewsHeader: React.FC = observer(() => {
icon={
currentProjectDetails && (
-
+
)
}
diff --git a/web/components/headers/workspace-settings.tsx b/web/components/headers/workspace-settings.tsx
index c73d06547ce..2d3e9649e1e 100644
--- a/web/components/headers/workspace-settings.tsx
+++ b/web/components/headers/workspace-settings.tsx
@@ -1,21 +1,15 @@
import { FC } from "react";
-import { observer } from "mobx-react";
-import { useRouter } from "next/router";
+import { observer } from "mobx-react";;
import { Settings } from "lucide-react";
// ui
import { Breadcrumbs } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
+// hooks
+import { useWorkspace } from "@/hooks/store";
-export interface IWorkspaceSettingHeader {
- title: string;
-}
-
-export const WorkspaceSettingHeader: FC = observer((props) => {
- const { title } = props;
- const router = useRouter();
-
- const { workspaceSlug } = router.query;
+export const WorkspaceSettingHeader: FC = observer(() => {
+ const { currentWorkspace } = useWorkspace();
return (
@@ -26,13 +20,13 @@ export const WorkspaceSettingHeader: FC = observer((pro
type="text"
link={
}
/>
}
/>
- } />
+ } />
diff --git a/web/components/inbox/content/inbox-issue-header.tsx b/web/components/inbox/content/inbox-issue-header.tsx
index 7fd038faade..8a3401569bc 100644
--- a/web/components/inbox/content/inbox-issue-header.tsx
+++ b/web/components/inbox/content/inbox-issue-header.tsx
@@ -52,7 +52,7 @@ export const InboxIssueActionsHeader: FC = observer((p
const [declineIssueModal, setDeclineIssueModal] = useState(false);
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
// store
- const { currentTab, deleteInboxIssue, inboxIssuesArray } = useProjectInbox();
+ const { currentTab, deleteInboxIssue, inboxIssueIds } = useProjectInbox();
const { data: currentUser } = useUser();
const {
membership: { currentProjectRole },
@@ -76,11 +76,11 @@ export const InboxIssueActionsHeader: FC = observer((p
const redirectIssue = (): string | undefined => {
let nextOrPreviousIssueId: string | undefined = undefined;
- const currentIssueIndex = inboxIssuesArray.findIndex((i) => i.issue.id === currentInboxIssueId);
- if (inboxIssuesArray[currentIssueIndex + 1])
- nextOrPreviousIssueId = inboxIssuesArray[currentIssueIndex + 1].issue.id;
- else if (inboxIssuesArray[currentIssueIndex - 1])
- nextOrPreviousIssueId = inboxIssuesArray[currentIssueIndex - 1].issue.id;
+ const currentIssueIndex = inboxIssueIds.findIndex((id) => id === currentInboxIssueId);
+ if (inboxIssueIds[currentIssueIndex + 1])
+ nextOrPreviousIssueId = inboxIssueIds[currentIssueIndex + 1];
+ else if (inboxIssueIds[currentIssueIndex - 1])
+ nextOrPreviousIssueId = inboxIssueIds[currentIssueIndex - 1];
else nextOrPreviousIssueId = undefined;
return nextOrPreviousIssueId;
};
@@ -134,22 +134,22 @@ export const InboxIssueActionsHeader: FC = observer((p
})
);
- const currentIssueIndex = inboxIssuesArray.findIndex((issue) => issue.issue.id === currentInboxIssueId) ?? 0;
+ const currentIssueIndex = inboxIssueIds.findIndex((issueId) => issueId === currentInboxIssueId) ?? 0;
const handleInboxIssueNavigation = useCallback(
(direction: "next" | "prev") => {
- if (!inboxIssuesArray || !currentInboxIssueId) return;
+ if (!inboxIssueIds || !currentInboxIssueId) return;
const activeElement = document.activeElement as HTMLElement;
if (activeElement && (activeElement.classList.contains("tiptap") || activeElement.id === "title-input")) return;
const nextIssueIndex =
direction === "next"
- ? (currentIssueIndex + 1) % inboxIssuesArray.length
- : (currentIssueIndex - 1 + inboxIssuesArray.length) % inboxIssuesArray.length;
- const nextIssueId = inboxIssuesArray[nextIssueIndex].issue.id;
+ ? (currentIssueIndex + 1) % inboxIssueIds.length
+ : (currentIssueIndex - 1 + inboxIssueIds.length) % inboxIssueIds.length;
+ const nextIssueId = inboxIssueIds[nextIssueIndex];
if (!nextIssueId) return;
router.push(`/${workspaceSlug}/projects/${projectId}/inbox?inboxIssueId=${nextIssueId}`);
},
- [currentInboxIssueId, currentIssueIndex, inboxIssuesArray, projectId, router, workspaceSlug]
+ [currentInboxIssueId, currentIssueIndex, inboxIssueIds, projectId, router, workspaceSlug]
);
const onKeyDown = useCallback(
diff --git a/web/components/inbox/content/issue-properties.tsx b/web/components/inbox/content/issue-properties.tsx
index 9074f67ca5a..92205e626cd 100644
--- a/web/components/inbox/content/issue-properties.tsx
+++ b/web/components/inbox/content/issue-properties.tsx
@@ -1,9 +1,9 @@
import React from "react";
import { observer } from "mobx-react";
import { useRouter } from "next/router";
-import { CalendarCheck2, CopyPlus, Signal, Tag } from "lucide-react";
+import { CalendarCheck2, CopyPlus, Signal, Tag, Users } from "lucide-react";
import { TInboxDuplicateIssueDetails, TIssue } from "@plane/types";
-import { ControlLink, DoubleCircleIcon, Tooltip, UserGroupIcon } from "@plane/ui";
+import { ControlLink, DoubleCircleIcon, Tooltip } from "@plane/ui";
// components
import { DateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "@/components/dropdowns";
import { IssueLabel, TIssueOperations } from "@/components/issues";
@@ -64,7 +64,7 @@ export const InboxIssueContentProperties: React.FC = observer((props) =>
{/* Assignee */}
-
+
Assignees
= observer((props) => {
const { workspaceSlug, projectId, inboxIssueId, isMobileSidebar, setIsMobileSidebar } = props;
+ /// router
+ const router = useRouter();
// states
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
// hooks
- const { fetchInboxIssueById, getIssueInboxByIssueId } = useProjectInbox();
+ const { currentTab, fetchInboxIssueById, getIssueInboxByIssueId, getIsIssueAvailable } = useProjectInbox();
const inboxIssue = getIssueInboxByIssueId(inboxIssueId);
const {
membership: { currentProjectRole },
} = useUser();
+ // derived values
+ const isIssueAvailable = getIsIssueAvailable(inboxIssueId?.toString() || "");
+
+ useEffect(() => {
+ if (!isIssueAvailable && inboxIssueId) {
+ router.replace(`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${currentTab}`);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isIssueAvailable]);
useSWR(
workspaceSlug && projectId && inboxIssueId
diff --git a/web/components/inbox/modals/create-edit-modal/create-root.tsx b/web/components/inbox/modals/create-edit-modal/create-root.tsx
index 27e710a5fb0..36f1b0abe58 100644
--- a/web/components/inbox/modals/create-edit-modal/create-root.tsx
+++ b/web/components/inbox/modals/create-edit-modal/create-root.tsx
@@ -18,6 +18,7 @@ import { ISSUE_CREATED } from "@/constants/event-tracker";
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
// hooks
import { useEventTracker, useProjectInbox, useWorkspace } from "@/hooks/store";
+import useKeypress from "@/hooks/use-keypress";
type TInboxIssueCreateRoot = {
workspaceSlug: string;
@@ -42,6 +43,7 @@ export const InboxIssueCreateRoot: FC = observer((props)
const router = useRouter();
// refs
const descriptionEditorRef = useRef(null);
+ const submitBtnRef = useRef(null);
// hooks
const { captureIssueEvent } = useEventTracker();
const { createInboxIssue } = useProjectInbox();
@@ -61,8 +63,33 @@ export const InboxIssueCreateRoot: FC = observer((props)
[formData]
);
+ const handleEscKeyDown = (event: KeyboardEvent) => {
+ if (descriptionEditorRef.current?.isEditorReadyToDiscard()) {
+ handleModalClose();
+ } else {
+ setToast({
+ type: TOAST_TYPE.ERROR,
+ title: "Error!",
+ message: "Editor is still processing changes. Please wait before proceeding.",
+ });
+ event.preventDefault(); // Prevent default action if editor is not ready to discard
+ }
+ };
+
+ useKeypress("Escape", handleEscKeyDown);
+
const handleFormSubmit = async (event: FormEvent) => {
event.preventDefault();
+
+ if (!descriptionEditorRef.current?.isEditorReadyToDiscard()) {
+ setToast({
+ type: TOAST_TYPE.ERROR,
+ title: "Error!",
+ message: "Editor is still processing changes. Please wait before proceeding.",
+ });
+ return;
+ }
+
const payload: Partial = {
name: formData.name || "",
description_html: formData.description_html || "
",
@@ -139,6 +166,7 @@ export const InboxIssueCreateRoot: FC = observer((props)
handleData={handleFormData}
editorRef={descriptionEditorRef}
containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
+ onEnterKeyPress={() => submitBtnRef?.current?.click()}
/>
@@ -153,11 +181,27 @@ export const InboxIssueCreateRoot: FC = observer((props)
Create more
-
+ {
+ if (descriptionEditorRef.current?.isEditorReadyToDiscard()) {
+ handleModalClose();
+ } else {
+ setToast({
+ type: TOAST_TYPE.ERROR,
+ title: "Error!",
+ message: "Editor is still processing changes. Please wait before proceeding.",
+ });
+ }
+ }}
+ >
Discard
= observer((props) => {
const router = useRouter();
// refs
const descriptionEditorRef = useRef(null);
+ const submitBtnRef = useRef(null);
// store hooks
const { captureIssueEvent } = useEventTracker();
const { currentProjectDetails } = useProject();
@@ -148,6 +149,7 @@ export const InboxIssueEditRoot: FC = observer((props) => {
handleData={handleFormData}
editorRef={descriptionEditorRef}
containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
+ onEnterKeyPress={() => submitBtnRef?.current?.click()}
/>
@@ -160,6 +162,7 @@ export const InboxIssueEditRoot: FC
= observer((props) => {
variant="primary"
size="sm"
type="button"
+ ref={submitBtnRef}
loading={formSubmitting}
disabled={isTitleLengthMoreThan255Character}
onClick={handleFormSubmit}
diff --git a/web/components/inbox/modals/create-edit-modal/issue-description.tsx b/web/components/inbox/modals/create-edit-modal/issue-description.tsx
index 882fb0f955e..4b4cb261e74 100644
--- a/web/components/inbox/modals/create-edit-modal/issue-description.tsx
+++ b/web/components/inbox/modals/create-edit-modal/issue-description.tsx
@@ -18,11 +18,13 @@ type TInboxIssueDescription = {
data: Partial;
handleData: (issueKey: keyof Partial, issueValue: Partial[keyof Partial]) => void;
editorRef: RefObject;
+ onEnterKeyPress?: (e?: any) => void;
};
// TODO: have to implement GPT Assistance
export const InboxIssueDescription: FC = observer((props) => {
- const { containerClassName, workspaceSlug, projectId, workspaceId, data, handleData, editorRef } = props;
+ const { containerClassName, workspaceSlug, projectId, workspaceId, data, handleData, editorRef, onEnterKeyPress } =
+ props;
// hooks
const { loader } = useProjectInbox();
@@ -44,6 +46,7 @@ export const InboxIssueDescription: FC = observer((props
onChange={(_description: object, description_html: string) => handleData("description_html", description_html)}
placeholder={getDescriptionPlaceholder}
containerClassName={containerClassName}
+ onEnterKeyPress={onEnterKeyPress}
/>
);
});
diff --git a/web/components/inbox/modals/decline-issue-modal.tsx b/web/components/inbox/modals/decline-issue-modal.tsx
index 5c7b35a1c2e..4ca784ec150 100644
--- a/web/components/inbox/modals/decline-issue-modal.tsx
+++ b/web/components/inbox/modals/decline-issue-modal.tsx
@@ -36,7 +36,7 @@ export const DeclineIssueModal: React.FC = (props) => {
= observer(({ isOpen, onClos
void;
};
export const InboxIssueListItem: FC = observer((props) => {
- const { workspaceSlug, projectId, inboxIssue, projectIdentifier, setIsMobileSidebar } = props;
+ const { workspaceSlug, projectId, inboxIssueId, projectIdentifier, setIsMobileSidebar } = props;
// router
const router = useRouter();
- const { inboxIssueId } = router.query;
+ const { inboxIssueId: selectedInboxIssueId } = router.query;
// store
- const { currentTab } = useProjectInbox();
+ const { currentTab, getIssueInboxByIssueId } = useProjectInbox();
const { projectLabels } = useLabel();
const { isMobile } = usePlatformOS();
const { getUserDetails } = useMember();
- const issue = inboxIssue.issue;
+ const inboxIssue = getIssueInboxByIssueId(inboxIssueId);
+ const issue = inboxIssue?.issue;
const handleIssueRedirection = (event: MouseEvent, currentIssueId: string | undefined) => {
- if (inboxIssueId === currentIssueId) event.preventDefault();
+ if (selectedInboxIssueId === currentIssueId) event.preventDefault();
setIsMobileSidebar(false);
};
@@ -55,7 +54,7 @@ export const InboxIssueListItem: FC = observer((props)
diff --git a/web/components/inbox/sidebar/inbox-list.tsx b/web/components/inbox/sidebar/inbox-list.tsx
index be435cd7752..95d692b6059 100644
--- a/web/components/inbox/sidebar/inbox-list.tsx
+++ b/web/components/inbox/sidebar/inbox-list.tsx
@@ -2,30 +2,28 @@ import { FC, Fragment } from "react";
import { observer } from "mobx-react";
// components
import { InboxIssueListItem } from "@/components/inbox";
-// store
-import { IInboxIssueStore } from "@/store/inbox/inbox-issue.store";
export type InboxIssueListProps = {
workspaceSlug: string;
projectId: string;
projectIdentifier?: string;
- inboxIssues: IInboxIssueStore[];
+ inboxIssueIds: string[];
setIsMobileSidebar: (value: boolean) => void;
};
export const InboxIssueList: FC
= observer((props) => {
- const { workspaceSlug, projectId, projectIdentifier, inboxIssues, setIsMobileSidebar } = props;
+ const { workspaceSlug, projectId, projectIdentifier, inboxIssueIds, setIsMobileSidebar } = props;
return (
<>
- {inboxIssues.map((inboxIssue) => (
-
+ {inboxIssueIds.map((inboxIssueId) => (
+
))}
diff --git a/web/components/inbox/sidebar/root.tsx b/web/components/inbox/sidebar/root.tsx
index f33cb3c2f24..ed6d0cdd2d9 100644
--- a/web/components/inbox/sidebar/root.tsx
+++ b/web/components/inbox/sidebar/root.tsx
@@ -44,7 +44,7 @@ export const InboxSidebar: FC = observer((props) => {
currentTab,
handleCurrentTab,
loader,
- inboxIssuesArray,
+ inboxIssueIds,
inboxIssuePaginationInfo,
fetchInboxPaginationIssues,
getAppliedFiltersCount,
@@ -56,13 +56,9 @@ export const InboxSidebar: FC = observer((props) => {
if (!workspaceSlug || !projectId) return;
fetchInboxPaginationIssues(workspaceSlug.toString(), projectId.toString());
}, [workspaceSlug, projectId, fetchInboxPaginationIssues]);
+
// page observer
- useIntersectionObserver({
- containerRef,
- elementRef,
- callback: fetchNextPages,
- rootMargin: "20%",
- });
+ useIntersectionObserver(containerRef, elementRef, fetchNextPages, "20%");
return (
@@ -108,13 +104,13 @@ export const InboxSidebar: FC
= observer((props) => {
className="w-full h-full overflow-hidden overflow-y-auto vertical-scrollbar scrollbar-md"
ref={containerRef}
>
- {inboxIssuesArray.length > 0 ? (
+ {inboxIssueIds.length > 0 ? (
) : (
@@ -130,15 +126,14 @@ export const InboxSidebar: FC = observer((props) => {
/>
)}
-
-
- {inboxIssuePaginationInfo?.next_page_results && (
+ {inboxIssuePaginationInfo?.next_page_results && (
+
- )}
-
+
+ )}
)}
diff --git a/web/components/integration/github/root.tsx b/web/components/integration/github/root.tsx
index d5866e95a0c..7e9322a5a61 100644
--- a/web/components/integration/github/root.tsx
+++ b/web/components/integration/github/root.tsx
@@ -10,9 +10,9 @@ import useSWR, { mutate } from "swr";
// react-hook-form
// services
// components
-import { ArrowLeft, Check, List, Settings, UploadCloud } from "lucide-react";
+import { ArrowLeft, Check, List, Settings, UploadCloud, Users } from "lucide-react";
import { IGithubRepoCollaborator, IGithubServiceImportFormData } from "@plane/types";
-import { UserGroupIcon, TOAST_TYPE, setToast } from "@plane/ui";
+import { TOAST_TYPE, setToast } from "@plane/ui";
import {
GithubImportConfigure,
GithubImportData,
@@ -72,7 +72,7 @@ const integrationWorkflowData = [
{
title: "Users",
key: "import-users",
- icon: UserGroupIcon,
+ icon: Users,
},
{
title: "Confirm",
diff --git a/web/components/integration/jira/root.tsx b/web/components/integration/jira/root.tsx
index b95ec198602..1b98c27ba9d 100644
--- a/web/components/integration/jira/root.tsx
+++ b/web/components/integration/jira/root.tsx
@@ -5,12 +5,12 @@ import { useRouter } from "next/router";
import { FormProvider, useForm } from "react-hook-form";
import { mutate } from "swr";
// icons
-import { ArrowLeft, Check, List, Settings } from "lucide-react";
+import { ArrowLeft, Check, List, Settings, Users } from "lucide-react";
import { IJiraImporterForm } from "@plane/types";
// services
// fetch keys
// components
-import { Button, UserGroupIcon } from "@plane/ui";
+import { Button } from "@plane/ui";
import { IMPORTER_SERVICES_LIST } from "@/constants/fetch-keys";
// assets
import { JiraImporterService } from "@/services/integrations";
@@ -44,7 +44,7 @@ const integrationWorkflowData: Array<{
{
title: "Users",
key: "import-users",
- icon: UserGroupIcon,
+ icon: Users,
},
{
title: "Confirm",
diff --git a/web/components/issues/attachment/delete-attachment-modal.tsx b/web/components/issues/attachment/delete-attachment-modal.tsx
index 94ff60c947e..98687f53875 100644
--- a/web/components/issues/attachment/delete-attachment-modal.tsx
+++ b/web/components/issues/attachment/delete-attachment-modal.tsx
@@ -35,7 +35,7 @@ export const IssueAttachmentDeleteModal: FC
= (props) => {
handleDeletion(data.id)}
- isDeleting={loader}
+ isSubmitting={loader}
isOpen={isOpen}
title="Delete attachment"
content={
diff --git a/web/components/issues/delete-issue-modal.tsx b/web/components/issues/delete-issue-modal.tsx
index 49c1a870099..46f8e733de8 100644
--- a/web/components/issues/delete-issue-modal.tsx
+++ b/web/components/issues/delete-issue-modal.tsx
@@ -66,7 +66,7 @@ export const DeleteIssueModal: React.FC = (props) => {
= observer((props
if (!activity) return <>>;
return (
}
+ icon={ }
activityId={activityId}
ends={ends}
>
diff --git a/web/components/issues/issue-detail/parent-select.tsx b/web/components/issues/issue-detail/parent-select.tsx
index d8399fc02be..402319af4c9 100644
--- a/web/components/issues/issue-detail/parent-select.tsx
+++ b/web/components/issues/issue-detail/parent-select.tsx
@@ -103,7 +103,7 @@ export const IssueParentSelect: React.FC = observer((props)
= observer((props) => {
Sibling issues
-
+
issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: null })}
diff --git a/web/components/issues/issue-detail/parent/sibling-item.tsx b/web/components/issues/issue-detail/parent/sibling-item.tsx
index c6eef2a9e32..c66a1889943 100644
--- a/web/components/issues/issue-detail/parent/sibling-item.tsx
+++ b/web/components/issues/issue-detail/parent/sibling-item.tsx
@@ -1,4 +1,5 @@
import { FC } from "react";
+import { observer } from "mobx-react";
import Link from "next/link";
// ui
import { CustomMenu, LayersIcon } from "@plane/ui";
@@ -6,15 +7,15 @@ import { CustomMenu, LayersIcon } from "@plane/ui";
import { useIssueDetail, useProject } from "@/hooks/store";
type TIssueParentSiblingItem = {
+ workspaceSlug: string;
issueId: string;
};
-export const IssueParentSiblingItem: FC = (props) => {
- const { issueId } = props;
+export const IssueParentSiblingItem: FC = observer((props) => {
+ const { workspaceSlug, issueId } = props;
// hooks
const { getProjectById } = useProject();
const {
- peekIssue,
issue: { getIssueById },
} = useIssueDetail();
@@ -27,7 +28,7 @@ export const IssueParentSiblingItem: FC = (props) => {
<>
@@ -36,4 +37,4 @@ export const IssueParentSiblingItem: FC = (props) => {
>
);
-};
+});
diff --git a/web/components/issues/issue-detail/parent/siblings.tsx b/web/components/issues/issue-detail/parent/siblings.tsx
index 56e93fc0f11..e23d8a595fe 100644
--- a/web/components/issues/issue-detail/parent/siblings.tsx
+++ b/web/components/issues/issue-detail/parent/siblings.tsx
@@ -1,4 +1,5 @@
import { FC } from "react";
+import { observer } from "mobx-react";
import useSWR from "swr";
import { TIssue } from "@plane/types";
// components
@@ -8,25 +9,25 @@ import { useIssueDetail } from "@/hooks/store";
import { IssueParentSiblingItem } from "./sibling-item";
export type TIssueParentSiblings = {
+ workspaceSlug: string;
currentIssue: TIssue;
parentIssue: TIssue;
};
-export const IssueParentSiblings: FC = (props) => {
- const { currentIssue, parentIssue } = props;
+export const IssueParentSiblings: FC = observer((props) => {
+ const { workspaceSlug, currentIssue, parentIssue } = props;
// hooks
const {
- peekIssue,
fetchSubIssues,
subIssues: { subIssuesByIssueId },
} = useIssueDetail();
const { isLoading } = useSWR(
- peekIssue && parentIssue && parentIssue.project_id
- ? `ISSUE_PARENT_CHILD_ISSUES_${peekIssue?.workspaceSlug}_${parentIssue.project_id}_${parentIssue.id}`
+ parentIssue && parentIssue.project_id
+ ? `ISSUE_PARENT_CHILD_ISSUES_${workspaceSlug}_${parentIssue.project_id}_${parentIssue.id}`
: null,
- peekIssue && parentIssue && parentIssue.project_id
- ? () => fetchSubIssues(peekIssue?.workspaceSlug, parentIssue.project_id, parentIssue.id)
+ parentIssue && parentIssue.project_id
+ ? () => fetchSubIssues(workspaceSlug, parentIssue.project_id, parentIssue.id)
: null
);
@@ -40,7 +41,10 @@ export const IssueParentSiblings: FC = (props) => {
) : subIssueIds && subIssueIds.length > 0 ? (
subIssueIds.map(
- (issueId) => currentIssue.id != issueId &&
+ (issueId) =>
+ currentIssue.id != issueId && (
+
+ )
)
) : (
@@ -49,4 +53,4 @@ export const IssueParentSiblings: FC = (props) => {
)}
);
-};
+});
diff --git a/web/components/issues/issue-detail/sidebar.tsx b/web/components/issues/issue-detail/sidebar.tsx
index a9a23ebdcbe..e5526a4a81a 100644
--- a/web/components/issues/issue-detail/sidebar.tsx
+++ b/web/components/issues/issue-detail/sidebar.tsx
@@ -12,6 +12,7 @@ import {
Tag,
Trash2,
Triangle,
+ Users,
XCircle,
} from "lucide-react";
// hooks
@@ -24,7 +25,6 @@ import {
RelatedIcon,
TOAST_TYPE,
Tooltip,
- UserGroupIcon,
setToast,
} from "@plane/ui";
import {
@@ -48,7 +48,7 @@ import {
} from "@/components/issues";
// helpers
// types
-import { STATE_GROUPS } from "@/constants/state";
+import { ARCHIVABLE_STATE_GROUPS } from "@/constants/state";
import { cn } from "@/helpers/common.helper";
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
@@ -117,8 +117,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => {
const stateDetails = getStateById(issue.state_id);
// auth
const isArchivingAllowed = !is_archived && issueOperations.archive && isEditable;
- const isInArchivableGroup =
- !!stateDetails && [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group);
+ const isInArchivableGroup = !!stateDetails && ARCHIVABLE_STATE_GROUPS.includes(stateDetails?.group);
const minDate = issue.start_date ? getDate(issue.start_date) : null;
minDate?.setDate(minDate.getDate());
@@ -219,7 +218,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => {
-
+
Assignees
= observer((props) => {
? EmptyStateType.PROJECT_EMPTY_FILTER
: EmptyStateType.PROJECT_CYCLE_NO_ISSUES;
const additionalPath = isCompletedAndEmpty ? undefined : activeLayout ?? "list";
- const emptyStateSize = isEmptyFilters ? "lg" : "sm";
return (
<>
@@ -84,7 +83,6 @@ export const CycleEmptyState: React.FC = observer((props) => {
{
diff --git a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx
index 07cb70ceb37..7fc6811c857 100644
--- a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx
+++ b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx
@@ -41,14 +41,12 @@ export const ProjectDraftEmptyState: React.FC = observer(() => {
const emptyStateType =
issueFilterCount > 0 ? EmptyStateType.PROJECT_DRAFT_EMPTY_FILTER : EmptyStateType.PROJECT_DRAFT_NO_ISSUES;
const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined;
- const emptyStateSize = issueFilterCount > 0 ? "lg" : "sm";
return (
0 ? handleClearAllFilters : undefined}
/>
diff --git a/web/components/issues/issue-layouts/empty-states/module.tsx b/web/components/issues/issue-layouts/empty-states/module.tsx
index a75cec391a7..536bd985be5 100644
--- a/web/components/issues/issue-layouts/empty-states/module.tsx
+++ b/web/components/issues/issue-layouts/empty-states/module.tsx
@@ -55,7 +55,6 @@ export const ModuleEmptyState: React.FC = observer((props) => {
const emptyStateType = isEmptyFilters ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_MODULE_ISSUES;
const additionalPath = activeLayout ?? "list";
- const emptyStateSize = isEmptyFilters ? "lg" : "sm";
return (
<>
@@ -71,7 +70,6 @@ export const ModuleEmptyState: React.FC = observer((props) => {
{
const emptyStateType = issueFilterCount > 0 ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_NO_ISSUES;
const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined;
- const emptyStateSize = issueFilterCount > 0 ? "lg" : "sm";
return (
0
? undefined
diff --git a/web/components/issues/issue-layouts/filters/applied-filters/project.tsx b/web/components/issues/issue-layouts/filters/applied-filters/project.tsx
index 54dc039191b..190d9f1fa15 100644
--- a/web/components/issues/issue-layouts/filters/applied-filters/project.tsx
+++ b/web/components/issues/issue-layouts/filters/applied-filters/project.tsx
@@ -1,9 +1,9 @@
import { observer } from "mobx-react";
import { X } from "lucide-react";
+// components
+import { Logo } from "@/components/common";
// hooks
-import { ProjectLogo } from "@/components/project";
import { useProject } from "@/hooks/store";
-// components
type Props = {
handleRemove: (val: string) => void;
@@ -26,7 +26,7 @@ export const AppliedProjectFilters: React.FC = observer((props) => {
return (
-
+
{projectDetails.name}
{editable && (
diff --git a/web/components/issues/issue-layouts/filters/header/filters/project.tsx b/web/components/issues/issue-layouts/filters/header/filters/project.tsx
index 26b0bb46bae..d739674813e 100644
--- a/web/components/issues/issue-layouts/filters/header/filters/project.tsx
+++ b/web/components/issues/issue-layouts/filters/header/filters/project.tsx
@@ -1,15 +1,13 @@
import React, { useMemo, useState } from "react";
import sortBy from "lodash/sortBy";
import { observer } from "mobx-react";
-// components
+// ui
import { Loader } from "@plane/ui";
+// components
+import { Logo } from "@/components/common";
import { FilterHeader, FilterOption } from "@/components/issues";
// hooks
-import { ProjectLogo } from "@/components/project";
import { useProject } from "@/hooks/store";
-// components
-// ui
-// helpers
type Props = {
appliedFilters: string[] | null;
@@ -65,7 +63,7 @@ export const FilterProjects: React.FC
= observer((props) => {
onClick={() => handleUpdate(project.id)}
icon={
-
+
}
title={project.name}
diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx
index b8060d7cf82..831c3119ef9 100644
--- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx
+++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx
@@ -182,14 +182,14 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas
};
const handleKanbanFilters = (toggle: "group_by" | "sub_group_by", value: string) => {
- if (workspaceSlug && projectId) {
+ if (workspaceSlug) {
let kanbanFilters = issuesFilter?.issueFilters?.kanbanFilters?.[toggle] || [];
if (kanbanFilters.includes(value)) {
kanbanFilters = kanbanFilters.filter((_value) => _value != value);
} else {
kanbanFilters.push(value);
}
- updateFilters(projectId.toString(), EIssueFilterType.KANBAN_FILTERS, {
+ updateFilters(projectId?.toString() ?? "", EIssueFilterType.KANBAN_FILTERS, {
[toggle]: kanbanFilters,
});
}
diff --git a/web/components/issues/issue-layouts/kanban/kanban-group.tsx b/web/components/issues/issue-layouts/kanban/kanban-group.tsx
index da46e862775..fec907a7888 100644
--- a/web/components/issues/issue-layouts/kanban/kanban-group.tsx
+++ b/web/components/issues/issue-layouts/kanban/kanban-group.tsx
@@ -169,9 +169,9 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
preloadedData = { ...preloadedData, state_id: subGroupValue };
} else if (subGroupByKey === "priority") {
preloadedData = { ...preloadedData, priority: subGroupValue };
- } else if (groupByKey === "cycle") {
+ } else if (subGroupByKey === "cycle") {
preloadedData = { ...preloadedData, cycle_id: subGroupValue };
- } else if (groupByKey === "module") {
+ } else if (subGroupByKey === "module") {
preloadedData = { ...preloadedData, module_ids: [subGroupValue] };
} else if (subGroupByKey === "labels" && subGroupValue != "None") {
preloadedData = { ...preloadedData, label_ids: [subGroupValue] };
diff --git a/web/components/issues/issue-layouts/list/block.tsx b/web/components/issues/issue-layouts/list/block.tsx
index 3cf82cd319b..2ff31ab1d4c 100644
--- a/web/components/issues/issue-layouts/list/block.tsx
+++ b/web/components/issues/issue-layouts/list/block.tsx
@@ -67,8 +67,8 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
!getIsIssuePeeked(issue.id) &&
setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id, nestingLevel: nestingLevel });
- const issue = issuesMap[issueId];
- const subIssuesCount = issue.sub_issues_count;
+ const issue = issuesMap[issueId];
+ const subIssuesCount = issue?.sub_issues_count ?? 0;
const { isMobile } = usePlatformOS();
@@ -131,8 +131,14 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
-
-
+
+
{subIssuesCount > 0 && (
= observer((props) => {
const currentLayout = `${activeLayout} layout`;
// derived values
const stateDetails = getStateById(issue.state_id);
- const subIssueCount = issue.sub_issues_count;
+ const subIssueCount = issue?.sub_issues_count ?? 0;
const issueOperations = useMemo(
() => ({
diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx
index a6168874954..b56a6f7ebdc 100644
--- a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx
+++ b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx
@@ -11,7 +11,7 @@ import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, set
import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues";
// constants
import { EIssuesStoreType } from "@/constants/issue";
-import { STATE_GROUPS } from "@/constants/state";
+import { ARCHIVABLE_STATE_GROUPS } from "@/constants/state";
// helpers
import { cn } from "@/helpers/common.helper";
import { copyUrlToClipboard } from "@/helpers/string.helper";
@@ -48,8 +48,7 @@ export const AllIssueQuickActions: React.FC = observer((props
const isEditingAllowed = !readOnly;
// auth
const isArchivingAllowed = handleArchive && isEditingAllowed;
- const isInArchivableGroup =
- !!stateDetails && [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group);
+ const isInArchivableGroup = !!stateDetails && ARCHIVABLE_STATE_GROUPS.includes(stateDetails?.group);
const issueLink = `${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`;
diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx
index 026d050ac50..ddfbb4a0221 100644
--- a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx
+++ b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx
@@ -12,7 +12,7 @@ import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "@/c
// constants
import { EIssuesStoreType } from "@/constants/issue";
import { EUserProjectRoles } from "@/constants/project";
-import { STATE_GROUPS } from "@/constants/state";
+import { ARCHIVABLE_STATE_GROUPS } from "@/constants/state";
// helpers
import { cn } from "@/helpers/common.helper";
import { copyUrlToClipboard } from "@/helpers/string.helper";
@@ -54,8 +54,7 @@ export const CycleIssueQuickActions: React.FC = observer((pro
// auth
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly;
const isArchivingAllowed = handleArchive && isEditingAllowed;
- const isInArchivableGroup =
- !!stateDetails && [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group);
+ const isInArchivableGroup = !!stateDetails && ARCHIVABLE_STATE_GROUPS.includes(stateDetails?.group);
const isDeletingAllowed = isEditingAllowed;
const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`;
diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx
index 7f862262674..e8a950b2547 100644
--- a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx
+++ b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx
@@ -12,7 +12,7 @@ import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "@/c
// constants
import { EIssuesStoreType } from "@/constants/issue";
import { EUserProjectRoles } from "@/constants/project";
-import { STATE_GROUPS } from "@/constants/state";
+import { ARCHIVABLE_STATE_GROUPS } from "@/constants/state";
// helpers
import { cn } from "@/helpers/common.helper";
import { copyUrlToClipboard } from "@/helpers/string.helper";
@@ -54,8 +54,7 @@ export const ModuleIssueQuickActions: React.FC = observer((pr
// auth
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly;
const isArchivingAllowed = handleArchive && isEditingAllowed;
- const isInArchivableGroup =
- !!stateDetails && [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group);
+ const isInArchivableGroup = !!stateDetails && ARCHIVABLE_STATE_GROUPS.includes(stateDetails?.group);
const isDeletingAllowed = isEditingAllowed;
const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`;
diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx
index ac84af55633..14879f1fcc9 100644
--- a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx
+++ b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx
@@ -12,7 +12,7 @@ import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "@/c
// constants
import { EIssuesStoreType } from "@/constants/issue";
import { EUserProjectRoles } from "@/constants/project";
-import { STATE_GROUPS } from "@/constants/state";
+import { ARCHIVABLE_STATE_GROUPS } from "@/constants/state";
// helpers
import { cn } from "@/helpers/common.helper";
import { copyUrlToClipboard } from "@/helpers/string.helper";
@@ -54,8 +54,7 @@ export const ProjectIssueQuickActions: React.FC = observer((p
// auth
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly;
const isArchivingAllowed = handleArchive && isEditingAllowed;
- const isInArchivableGroup =
- !!stateDetails && [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group);
+ const isInArchivableGroup = !!stateDetails && ARCHIVABLE_STATE_GROUPS.includes(stateDetails?.group);
const isDeletingAllowed = isEditingAllowed;
const issueLink = `${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`;
diff --git a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx
index 51188fe7210..93a275f39fc 100644
--- a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx
+++ b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx
@@ -154,12 +154,11 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
return (
-
+
{issueIds.length === 0 ? (
0
? currentView !== "custom-view" && currentView !== "subscribed"
diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx
index c597bc698ab..8a6d26ac6c1 100644
--- a/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx
+++ b/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx
@@ -19,7 +19,7 @@ export const SpreadsheetSubIssueColumn: React.FC = observer((props: Props
// hooks
const { workspaceSlug } = useAppRouter();
// derived values
- const subIssueCount = issue.sub_issues_count;
+ const subIssueCount = issue?.sub_issues_count ?? 0;
const redirectToIssueDetail = () => {
router.push({
diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx
index 03854fcad54..eb33a13f35a 100644
--- a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx
+++ b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx
@@ -203,7 +203,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
};
const disableUserActions = !canEditProperties(issueDetail.project_id);
- const subIssuesCount = issueDetail.sub_issues_count;
+ const subIssuesCount = issueDetail?.sub_issues_count ?? 0;
return (
<>
diff --git a/web/components/issues/issue-layouts/utils.tsx b/web/components/issues/issue-layouts/utils.tsx
index 2b12244a462..78048b4b4ba 100644
--- a/web/components/issues/issue-layouts/utils.tsx
+++ b/web/components/issues/issue-layouts/utils.tsx
@@ -5,6 +5,7 @@ import pull from "lodash/pull";
import uniq from "lodash/uniq";
import scrollIntoView from "smooth-scroll-into-view-if-needed";
import { ContrastIcon } from "lucide-react";
+// types
import {
GroupByColumnTypes,
IGroupByColumn,
@@ -13,12 +14,14 @@ import {
TIssue,
TIssueGroupByOptions,
} from "@plane/types";
+// ui
import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui";
// components
-import { ProjectLogo } from "@/components/project";
-// stores
+import { Logo } from "@/components/common";
+// constants
import { ISSUE_PRIORITIES, EIssuesStoreType } from "@/constants/issue";
import { STATE_GROUPS } from "@/constants/state";
+// stores
import { ICycleStore } from "@/store/cycle.store";
import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/issue-helper.store";
import { ILabelStore } from "@/store/label.store";
@@ -26,9 +29,6 @@ import { IMemberRootStore } from "@/store/member";
import { IModuleStore } from "@/store/module.store";
import { IProjectStore } from "@/store/project/project.store";
import { IStateStore } from "@/store/state.store";
-// helpers
-// constants
-// types
export const HIGHLIGHT_CLASS = "highlight";
export const HIGHLIGHT_WITH_LINE = "highlight-with-line";
@@ -101,7 +101,7 @@ const getProjectColumns = (project: IProjectStore): IGroupByColumn[] | undefined
name: project.name,
icon: (
),
payload: { project_id: project.id },
diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx
index 4c608f83a3a..42ce948adcd 100644
--- a/web/components/issues/issue-modal/form.tsx
+++ b/web/components/issues/issue-modal/form.tsx
@@ -29,6 +29,7 @@ import { getChangedIssuefields, getDescriptionPlaceholder } from "@/helpers/issu
import { shouldRenderProject } from "@/helpers/project.helper";
// hooks
import { useAppRouter, useEstimate, useInstance, useIssueDetail, useProject, useWorkspace } from "@/hooks/store";
+import useKeypress from "@/hooks/use-keypress";
import { useProjectIssueProperties } from "@/hooks/use-project-issue-properties";
// services
import { AIService } from "@/services/ai.service";
@@ -109,6 +110,7 @@ export const IssueFormRoot: FC = observer((props) => {
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
// refs
const editorRef = useRef(null);
+ const submitBtnRef = useRef(null);
// router
const router = useRouter();
const { workspaceSlug } = router.query;
@@ -120,6 +122,21 @@ export const IssueFormRoot: FC = observer((props) => {
const { getProjectById } = useProject();
const { areEstimatesEnabledForProject } = useEstimate();
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (editorRef.current?.isEditorReadyToDiscard()) {
+ onClose();
+ } else {
+ setToast({
+ type: TOAST_TYPE.ERROR,
+ title: "Error!",
+ message: "Editor is still processing changes. Please wait before proceeding.",
+ });
+ event.preventDefault(); // Prevent default action if editor is not ready to discard
+ }
+ };
+
+ useKeypress("Escape", handleKeyDown);
+
const {
issue: { getIssueById },
} = useIssueDetail();
@@ -167,6 +184,16 @@ export const IssueFormRoot: FC = observer((props) => {
const issueName = watch("name");
const handleFormSubmit = async (formData: Partial, is_draft_issue = false) => {
+ // Check if the editor is ready to discard
+ if (!editorRef.current?.isEditorReadyToDiscard()) {
+ setToast({
+ type: TOAST_TYPE.ERROR,
+ title: "Error!",
+ message: "Editor is not ready to discard changes.",
+ });
+ return;
+ }
+
const submitData = !data?.id
? formData
: {
@@ -386,7 +413,7 @@ export const IssueFormRoot: FC = observer((props) => {
/>
{errors?.name?.message}
-
+
{data?.description_html === undefined ? (
@@ -409,11 +436,33 @@ export const IssueFormRoot: FC = observer((props) => {
) : (
<>
-
+
(
+ {
+ onChange(description_html);
+ handleFormChange();
+ }}
+ onEnterKeyPress={() => submitBtnRef?.current?.click()}
+ ref={editorRef}
+ tabIndex={getTabIndex("description_html")}
+ placeholder={getDescriptionPlaceholder}
+ containerClassName="pt-3 min-h-[150px] max-h-64 overflow-y-auto vertical-scrollbar scrollbar-sm"
+ />
+ )}
+ />
+
{issueName && issueName.trim() !== "" && config?.has_openai_configured && (
= observer((props) => {
button={
setGptAssistantModal((prevData) => !prevData)}
tabIndex={getTabIndex("ai_assistant")}
>
@@ -456,27 +505,6 @@ export const IssueFormRoot: FC = observer((props) => {
/>
)}
- (
- {
- onChange(description_html);
- handleFormChange();
- }}
- ref={editorRef}
- tabIndex={getTabIndex("description_html")}
- placeholder={getDescriptionPlaceholder}
- containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
- />
- )}
- />
>
)}
@@ -738,7 +766,22 @@ export const IssueFormRoot: FC
= observer((props) => {
)}
-
+ {
+ if (editorRef.current?.isEditorReadyToDiscard()) {
+ onClose();
+ } else {
+ setToast({
+ type: TOAST_TYPE.ERROR,
+ title: "Error!",
+ message: "Editor is still processing changes. Please wait before proceeding.",
+ });
+ }
+ }}
+ tabIndex={getTabIndex("discard_button")}
+ >
Discard
{isDraft && (
@@ -770,6 +813,7 @@ export const IssueFormRoot: FC = observer((props) => {
variant="primary"
type="submit"
size="sm"
+ ref={submitBtnRef}
loading={isSubmitting}
tabIndex={isDraft ? getTabIndex("submit_button") : getTabIndex("draft_button")}
>
diff --git a/web/components/issues/issue-modal/modal.tsx b/web/components/issues/issue-modal/modal.tsx
index 38b328df65b..6558c55c4f3 100644
--- a/web/components/issues/issue-modal/modal.tsx
+++ b/web/components/issues/issue-modal/modal.tsx
@@ -140,6 +140,7 @@ export const CreateUpdateIssueModal: React.FC = observer((prop
}
setActiveProjectId(null);
+ setChangesMade(null);
onClose();
};
diff --git a/web/components/issues/peek-overview/header.tsx b/web/components/issues/peek-overview/header.tsx
index 9f146cdc006..05337649113 100644
--- a/web/components/issues/peek-overview/header.tsx
+++ b/web/components/issues/peek-overview/header.tsx
@@ -15,7 +15,7 @@ import {
} from "@plane/ui";
// components
import { IssueSubscription, IssueUpdateStatus } from "@/components/issues";
-import { STATE_GROUPS } from "@/constants/state";
+import { ARCHIVABLE_STATE_GROUPS } from "@/constants/state";
// helpers
import { cn } from "@/helpers/common.helper";
import { copyUrlToClipboard } from "@/helpers/string.helper";
@@ -100,8 +100,7 @@ export const IssuePeekOverviewHeader: FC = observer((pr
};
// auth
const isArchivingAllowed = !isArchived && !disabled;
- const isInArchivableGroup =
- !!stateDetails && [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group);
+ const isInArchivableGroup = !!stateDetails && ARCHIVABLE_STATE_GROUPS.includes(stateDetails?.group);
const isRestoringAllowed = isArchived && !disabled;
return (
diff --git a/web/components/issues/peek-overview/properties.tsx b/web/components/issues/peek-overview/properties.tsx
index cbc35b5e295..5364dacbb39 100644
--- a/web/components/issues/peek-overview/properties.tsx
+++ b/web/components/issues/peek-overview/properties.tsx
@@ -10,10 +10,11 @@ import {
XCircle,
CalendarClock,
CalendarCheck2,
+ Users,
} from "lucide-react";
// hooks
// ui icons
-import { DiceIcon, DoubleCircleIcon, UserGroupIcon, ContrastIcon, RelatedIcon } from "@plane/ui";
+import { DiceIcon, DoubleCircleIcon, ContrastIcon, RelatedIcon } from "@plane/ui";
// components
import {
DateDropdown,
@@ -94,7 +95,7 @@ export const PeekOverviewProperties: FC = observer((pro
{/* assignee */}
-
+
Assignees
= observer((props) => {
},
issueId
);
+
const handleKeyDown = () => {
const slashCommandDropdownElement = document.querySelector("#slash-command");
const dropdownElement = document.activeElement?.tagName === "INPUT";
@@ -74,6 +75,7 @@ export const IssueView: FC = observer((props) => {
if (issueElement) issueElement?.focus();
}
};
+
useKeypress("Escape", handleKeyDown);
const handleRestore = async () => {
diff --git a/web/components/issues/select/label.tsx b/web/components/issues/select/label.tsx
index fee060d1b86..ddc0e41b3fd 100644
--- a/web/components/issues/select/label.tsx
+++ b/web/components/issues/select/label.tsx
@@ -4,13 +4,14 @@ import { useRouter } from "next/router";
import { usePopper } from "react-popper";
import { Check, Component, Plus, Search, Tag } from "lucide-react";
import { Combobox } from "@headlessui/react";
-// hooks
+// components
import { IssueLabelsList } from "@/components/ui";
+// helpers
+import { cn } from "@/helpers/common.helper";
+// hooks
import { useLabel } from "@/hooks/store";
import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
-// ui
-// icons
type Props = {
setIsOpen: React.Dispatch>;
@@ -21,10 +22,21 @@ type Props = {
disabled?: boolean;
tabIndex?: number;
createLabelEnabled?: boolean;
+ buttonClassName?: string;
};
export const IssueLabelSelect: React.FC = observer((props) => {
- const { setIsOpen, value, onChange, projectId, label, disabled = false, tabIndex, createLabelEnabled = true } = props;
+ const {
+ setIsOpen,
+ value,
+ onChange,
+ projectId,
+ label,
+ disabled = false,
+ tabIndex,
+ createLabelEnabled = true,
+ buttonClassName,
+ } = props;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
@@ -101,7 +113,7 @@ export const IssueLabelSelect: React.FC = observer((props) => {
{label ? (
diff --git a/web/components/issues/sub-issues/issue-list-item.tsx b/web/components/issues/sub-issues/issue-list-item.tsx
index a3c3e9946dc..e0c57f4c45f 100644
--- a/web/components/issues/sub-issues/issue-list-item.tsx
+++ b/web/components/issues/sub-issues/issue-list-item.tsx
@@ -61,7 +61,7 @@ export const IssueListItem: React.FC = observer((props) => {
undefined;
const subIssueHelpers = subIssueHelpersByIssueId(parentIssueId);
- const subIssueCount = issue?.sub_issues_count || 0;
+ const subIssueCount = issue?.sub_issues_count ?? 0;
const handleIssuePeekOverview = (issue: TIssue) =>
workspaceSlug &&
diff --git a/web/components/labels/create-label-modal.tsx b/web/components/labels/create-label-modal.tsx
index 0de255c8ca7..18bed7a3aee 100644
--- a/web/components/labels/create-label-modal.tsx
+++ b/web/components/labels/create-label-modal.tsx
@@ -73,9 +73,9 @@ export const CreateLabelModal: React.FC = observer((props) => {
})
.catch((error) => {
setToast({
- title: "Oops!",
+ title: "Error!",
type: TOAST_TYPE.ERROR,
- message: error?.error ?? "Error while adding the label",
+ message: error?.detail ?? "Something went wrong. Please try again later.",
});
reset(formData);
});
diff --git a/web/components/labels/create-update-label-inline.tsx b/web/components/labels/create-update-label-inline.tsx
index 2aa8851c90a..280ae55d924 100644
--- a/web/components/labels/create-update-label-inline.tsx
+++ b/web/components/labels/create-update-label-inline.tsx
@@ -63,9 +63,9 @@ export const CreateUpdateLabelInline = observer(
})
.catch((error) => {
setToast({
- title: "Oops!",
+ title: "Error!",
type: TOAST_TYPE.ERROR,
- message: error?.error ?? "Error while adding the label",
+ message: error?.detail ?? "Something went wrong. Please try again later.",
});
reset(formData);
});
diff --git a/web/components/labels/delete-label-modal.tsx b/web/components/labels/delete-label-modal.tsx
index 191667b1c02..cb1943f059c 100644
--- a/web/components/labels/delete-label-modal.tsx
+++ b/web/components/labels/delete-label-modal.tsx
@@ -56,7 +56,7 @@ export const DeleteLabelModal: React.FC = observer((props) => {
{
return (
-
+
diff --git a/web/components/modules/delete-module-modal.tsx b/web/components/modules/delete-module-modal.tsx
index ffafdb1ec11..a19afb4d4f5 100644
--- a/web/components/modules/delete-module-modal.tsx
+++ b/web/components/modules/delete-module-modal.tsx
@@ -73,7 +73,7 @@ export const DeleteModuleModal: React.FC
= observer((props) => {
= (props) => {
= observer((props) => {
) : (
-
-
-
+
)}
diff --git a/web/components/modules/module-list-item-action.tsx b/web/components/modules/module-list-item-action.tsx
index fa7d71577cf..2a5a3cdd04c 100644
--- a/web/components/modules/module-list-item-action.tsx
+++ b/web/components/modules/module-list-item-action.tsx
@@ -2,7 +2,7 @@ import React, { FC } from "react";
import { observer } from "mobx-react";
import { useRouter } from "next/router";
// icons
-import { CalendarCheck2, CalendarClock, MoveRight, User2 } from "lucide-react";
+import { CalendarCheck2, CalendarClock, MoveRight, SquareUser } from "lucide-react";
// types
import { IModule } from "@plane/types";
// ui
@@ -140,9 +140,7 @@ export const ModuleListItemAction: FC = observer((props) => {
) : (
-
-
-
+
)}
diff --git a/web/components/modules/module-list-item.tsx b/web/components/modules/module-list-item.tsx
index b745921125d..37b8856ef2d 100644
--- a/web/components/modules/module-list-item.tsx
+++ b/web/components/modules/module-list-item.tsx
@@ -59,13 +59,17 @@ export const ModuleListItem: React.FC = observer((props) => {
}
};
+ const handleArchivedModuleClick = (e: React.MouseEvent) => {
+ openModuleOverview(e);
+ };
+
+ const handleItemClick = moduleDetails.archived_at ? handleArchivedModuleClick : undefined;
+
return (
{
- if (moduleDetails.archived_at) openModuleOverview(e);
- }}
+ onItemClick={handleItemClick}
prependTitleElement={
{completedModuleCheck ? (
diff --git a/web/components/modules/sidebar.tsx b/web/components/modules/sidebar.tsx
index d2c847eccd7..9688db1890e 100644
--- a/web/components/modules/sidebar.tsx
+++ b/web/components/modules/sidebar.tsx
@@ -1,4 +1,5 @@
import React, { useCallback, useEffect, useState } from "react";
+import isEqual from "lodash/isEqual";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import { Controller, useForm } from "react-hook-form";
@@ -11,8 +12,9 @@ import {
Info,
LinkIcon,
Plus,
+ SquareUser,
Trash2,
- UserCircle2,
+ Users,
} from "lucide-react";
import { Disclosure, Transition } from "@headlessui/react";
import { IIssueFilterOptions, ILinkDetails, IModule, ModuleLink } from "@plane/types";
@@ -23,7 +25,6 @@ import {
LayersIcon,
CustomSelect,
ModuleStatusIcon,
- UserGroupIcon,
TOAST_TYPE,
setToast,
ArchiveIcon,
@@ -252,14 +253,18 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => {
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
- const newValues = issueFilters?.filters?.[key] ?? [];
+ let newValues = issueFilters?.filters?.[key] ?? [];
if (Array.isArray(value)) {
- // this validation is majorly for the filter start_date, target_date custom
- value.forEach((val) => {
- if (!newValues.includes(val)) newValues.push(val);
- else newValues.splice(newValues.indexOf(val), 1);
- });
+ if (key === "state") {
+ if (isEqual(newValues, value)) newValues = [];
+ else newValues = value;
+ } else {
+ value.forEach((val) => {
+ if (!newValues.includes(val)) newValues.push(val);
+ else newValues.splice(newValues.indexOf(val), 1);
+ });
+ }
} else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
@@ -466,7 +471,6 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => {
= observer((props) => {
-
+
Lead
= observer((props) => {
buttonVariant="background-with-text"
placeholder="Lead"
disabled={!isEditingAllowed || isArchived}
+ icon={SquareUser}
/>
)}
@@ -518,7 +523,7 @@ export const ModuleDetailsSidebar: React.FC
= observer((props) => {
-
+
Members
= (props) => {
)}
/>
You can only edit the slug of the URL
- {slugError && Workspace URL is already taken! }
+ {slugError && Workspace URL is already taken!
}
{invalidSlug && (
- {`URL can only contain ( - ), ( _ ) & alphanumeric characters.`}
+ {`URL can only contain ( - ), ( _ ) & alphanumeric characters.`}
)}
diff --git a/web/components/pages/dropdowns/quick-actions.tsx b/web/components/pages/dropdowns/quick-actions.tsx
index e218c1389a0..ab0438f84d3 100644
--- a/web/components/pages/dropdowns/quick-actions.tsx
+++ b/web/components/pages/dropdowns/quick-actions.tsx
@@ -85,12 +85,7 @@ export const PageQuickActions: React.FC = observer((props) => {
return (
<>
- setDeletePageModal(false)}
- pageId={pageId}
- projectId={projectId}
- />
+ setDeletePageModal(false)} pageId={pageId} />
{MENU_ITEMS.map((item) => {
diff --git a/web/components/pages/editor/editor-body.tsx b/web/components/pages/editor/editor-body.tsx
index a896bcc5802..28f8790131d 100644
--- a/web/components/pages/editor/editor-body.tsx
+++ b/web/components/pages/editor/editor-body.tsx
@@ -1,8 +1,7 @@
import { useEffect } from "react";
import { observer } from "mobx-react";
import { useRouter } from "next/router";
-import { Control, Controller } from "react-hook-form";
-// document editor
+// document-editor
import {
DocumentEditorWithRef,
DocumentReadOnlyEditorWithRef,
@@ -11,15 +10,15 @@ import {
IMarking,
} from "@plane/document-editor";
// types
-import { IUserLite, TPage } from "@plane/types";
+import { IUserLite } from "@plane/types";
// components
import { PageContentBrowser, PageContentLoader, PageEditorTitle } from "@/components/pages";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useMember, useMention, useUser, useWorkspace } from "@/hooks/store";
+import { usePageDescription } from "@/hooks/use-page-description";
import { usePageFilters } from "@/hooks/use-page-filters";
-import useReloadConfirmations from "@/hooks/use-reload-confirmation";
// services
import { FileService } from "@/services/file.service";
// store
@@ -28,13 +27,10 @@ import { IPageStore } from "@/store/pages/page.store";
const fileService = new FileService();
type Props = {
- control: Control;
editorRef: React.RefObject;
readOnlyEditorRef: React.RefObject;
- swrPageDetails: TPage | undefined;
- handleSubmit: () => void;
markings: IMarking[];
- pageStore: IPageStore;
+ page: IPageStore;
sidePeekVisible: boolean;
handleEditorReady: (value: boolean) => void;
handleReadOnlyEditorReady: (value: boolean) => void;
@@ -43,15 +39,12 @@ type Props = {
export const PageEditorBody: React.FC = observer((props) => {
const {
- control,
handleReadOnlyEditorReady,
handleEditorReady,
editorRef,
markings,
readOnlyEditorRef,
- handleSubmit,
- pageStore,
- swrPageDetails,
+ page,
sidePeekVisible,
updateMarkings,
} = props;
@@ -67,11 +60,19 @@ export const PageEditorBody: React.FC = observer((props) => {
} = useMember();
// derived values
const workspaceId = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString())?.id ?? "" : "";
- const pageTitle = pageStore?.name ?? "";
- const pageDescription = pageStore?.description_html;
- const { description_html, isContentEditable, updateTitle, isSubmitting, setIsSubmitting } = pageStore;
+ const pageId = page?.id;
+ const pageTitle = page?.name ?? "";
+ const pageDescription = page?.description_html;
+ const { isContentEditable, updateTitle, setIsSubmitting } = page;
const projectMemberIds = projectId ? getProjectMemberIds(projectId.toString()) : [];
const projectMemberDetails = projectMemberIds?.map((id) => getUserDetails(id) as IUserLite);
+ // project-description
+ const { handleDescriptionChange, isDescriptionReady, pageDescriptionYJS } = usePageDescription({
+ editorRef,
+ page,
+ projectId,
+ workspaceSlug,
+ });
// use-mention
const { mentionHighlights, mentionSuggestions } = useMention({
workspaceSlug: workspaceSlug?.toString() ?? "",
@@ -82,13 +83,11 @@ export const PageEditorBody: React.FC = observer((props) => {
// page filters
const { isFullWidth } = usePageFilters();
- const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
-
useEffect(() => {
- updateMarkings(description_html ?? "
");
- }, [description_html, updateMarkings]);
+ updateMarkings(pageDescription ?? "
");
+ }, [pageDescription, updateMarkings]);
- if (pageDescription === undefined) return ;
+ if (pageId === undefined || !pageDescriptionYJS || !isDescriptionReady) return ;
return (
@@ -122,35 +121,24 @@ export const PageEditorBody: React.FC
= observer((props) => {
/>
{isContentEditable ? (
- (
- "}
- value={swrPageDetails?.description_html ?? "
"}
- ref={editorRef}
- containerClassName="p-0 pb-64"
- editorClassName="lg:px-10 pl-8"
- onChange={(_description_json, description_html) => {
- setIsSubmitting("submitting");
- setShowAlert(true);
- onChange(description_html);
- handleSubmit();
- }}
- mentionHandler={{
- highlights: mentionHighlights,
- suggestions: mentionSuggestions,
- }}
- />
- )}
+
) : (
= observer((props) => {
initialValue={pageDescription ?? "
"}
handleEditorReady={handleReadOnlyEditorReady}
containerClassName="p-0 pb-64 border-none"
- editorClassName="lg:px-10 pl-8"
+ editorClassName="pl-10"
mentionHandler={{
highlights: mentionHighlights,
}}
diff --git a/web/components/pages/editor/header/extra-options.tsx b/web/components/pages/editor/header/extra-options.tsx
index dee77d19ec3..63279984647 100644
--- a/web/components/pages/editor/header/extra-options.tsx
+++ b/web/components/pages/editor/header/extra-options.tsx
@@ -1,6 +1,6 @@
import { useState } from "react";
import { observer } from "mobx-react";
-import { Lock, RefreshCw, Sparkle } from "lucide-react";
+import { Lock, Sparkle } from "lucide-react";
// editor
import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/document-editor";
// ui
@@ -9,7 +9,6 @@ import { ArchiveIcon } from "@plane/ui";
import { GptAssistantPopover } from "@/components/core";
import { PageInfoPopover, PageOptionsDropdown } from "@/components/pages";
// helpers
-import { cn } from "@/helpers/common.helper";
import { renderFormattedDate } from "@/helpers/date-time.helper";
// hooks
import { useInstance } from "@/hooks/store";
@@ -19,20 +18,19 @@ import { IPageStore } from "@/store/pages/page.store";
type Props = {
editorRef: React.RefObject;
handleDuplicatePage: () => void;
- isSyncing: boolean;
- pageStore: IPageStore;
+ page: IPageStore;
projectId: string;
readOnlyEditorRef: React.RefObject;
};
export const PageExtraOptions: React.FC = observer((props) => {
- const { editorRef, handleDuplicatePage, isSyncing, pageStore, projectId, readOnlyEditorRef } = props;
+ const { editorRef, handleDuplicatePage, page, projectId, readOnlyEditorRef } = props;
// states
const [gptModalOpen, setGptModal] = useState(false);
// store hooks
const { config } = useInstance();
// derived values
- const { archived_at, isContentEditable, isSubmitting, is_locked } = pageStore;
+ const { archived_at, isContentEditable, is_locked } = page;
const handleAiAssistance = async (response: string) => {
if (!editorRef) return;
@@ -41,22 +39,6 @@ export const PageExtraOptions: React.FC = observer((props) => {
return (
- {isContentEditable && (
-
- {isSubmitting === "submitting" && }
- {isSubmitting === "submitting" ? "Saving..." : "Saved"}
-
- )}
- {isSyncing && (
-
-
- Syncing...
-
- )}
{is_locked && (
@@ -93,11 +75,11 @@ export const PageExtraOptions: React.FC
= observer((props) => {
className="!min-w-[38rem]"
/>
)}
-
+
);
diff --git a/web/components/pages/editor/header/info-popover.tsx b/web/components/pages/editor/header/info-popover.tsx
index 55b4b28fb70..270da934b97 100644
--- a/web/components/pages/editor/header/info-popover.tsx
+++ b/web/components/pages/editor/header/info-popover.tsx
@@ -7,11 +7,11 @@ import { renderFormattedDate } from "@/helpers/date-time.helper";
import { IPageStore } from "@/store/pages/page.store";
type Props = {
- pageStore: IPageStore;
+ page: IPageStore;
};
export const PageInfoPopover: React.FC
= (props) => {
- const { pageStore } = props;
+ const { page } = props;
// states
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
// refs
@@ -22,7 +22,7 @@ export const PageInfoPopover: React.FC = (props) => {
placement: "bottom-start",
});
// derived values
- const { created_at, updated_at } = pageStore;
+ const { created_at, updated_at } = page;
return (
setIsPopoverOpen(true)} onMouseLeave={() => setIsPopoverOpen(false)}>
diff --git a/web/components/pages/editor/header/mobile-root.tsx b/web/components/pages/editor/header/mobile-root.tsx
index de0425879ea..44cd9d38b41 100644
--- a/web/components/pages/editor/header/mobile-root.tsx
+++ b/web/components/pages/editor/header/mobile-root.tsx
@@ -11,9 +11,8 @@ type Props = {
editorRef: React.RefObject
;
readOnlyEditorRef: React.RefObject;
handleDuplicatePage: () => void;
- isSyncing: boolean;
markings: IMarking[];
- pageStore: IPageStore;
+ page: IPageStore;
projectId: string;
sidePeekVisible: boolean;
setSidePeekVisible: (sidePeekState: boolean) => void;
@@ -29,14 +28,13 @@ export const PageEditorMobileHeaderRoot: React.FC = observer((props) => {
markings,
readOnlyEditorReady,
handleDuplicatePage,
- isSyncing,
- pageStore,
+ page,
projectId,
sidePeekVisible,
setSidePeekVisible,
} = props;
// derived values
- const { isContentEditable } = pageStore;
+ const { isContentEditable } = page;
// page filters
const { isFullWidth } = usePageFilters();
@@ -57,8 +55,7 @@ export const PageEditorMobileHeaderRoot: React.FC = observer((props) => {
diff --git a/web/components/pages/editor/header/options-dropdown.tsx b/web/components/pages/editor/header/options-dropdown.tsx
index 9d3c8627bf5..9aeb2a679e5 100644
--- a/web/components/pages/editor/header/options-dropdown.tsx
+++ b/web/components/pages/editor/header/options-dropdown.tsx
@@ -16,11 +16,11 @@ import { IPageStore } from "@/store/pages/page.store";
type Props = {
editorRef: EditorRefApi | EditorReadOnlyRefApi | null;
handleDuplicatePage: () => void;
- pageStore: IPageStore;
+ page: IPageStore;
};
export const PageOptionsDropdown: React.FC = observer((props) => {
- const { editorRef, handleDuplicatePage, pageStore } = props;
+ const { editorRef, handleDuplicatePage, page } = props;
// store values
const {
archived_at,
@@ -33,7 +33,7 @@ export const PageOptionsDropdown: React.FC = observer((props) => {
canCurrentUserDuplicatePage,
canCurrentUserLockPage,
restore,
- } = pageStore;
+ } = page;
// store hooks
const { workspaceSlug, projectId } = useAppRouter();
// page filters
diff --git a/web/components/pages/editor/header/root.tsx b/web/components/pages/editor/header/root.tsx
index 7234f3ad4c2..7f17c43c3b8 100644
--- a/web/components/pages/editor/header/root.tsx
+++ b/web/components/pages/editor/header/root.tsx
@@ -13,9 +13,8 @@ type Props = {
editorRef: React.RefObject;
readOnlyEditorRef: React.RefObject;
handleDuplicatePage: () => void;
- isSyncing: boolean;
markings: IMarking[];
- pageStore: IPageStore;
+ page: IPageStore;
projectId: string;
sidePeekVisible: boolean;
setSidePeekVisible: (sidePeekState: boolean) => void;
@@ -31,14 +30,13 @@ export const PageEditorHeaderRoot: React.FC = observer((props) => {
markings,
readOnlyEditorReady,
handleDuplicatePage,
- isSyncing,
- pageStore,
+ page,
projectId,
sidePeekVisible,
setSidePeekVisible,
} = props;
// derived values
- const { isContentEditable } = pageStore;
+ const { isContentEditable } = page;
// page filters
const { isFullWidth } = usePageFilters();
@@ -67,8 +65,7 @@ export const PageEditorHeaderRoot: React.FC = observer((props) => {
@@ -81,8 +78,7 @@ export const PageEditorHeaderRoot: React.FC = observer((props) => {
readOnlyEditorReady={readOnlyEditorReady}
markings={markings}
handleDuplicatePage={handleDuplicatePage}
- isSyncing={isSyncing}
- pageStore={pageStore}
+ page={page}
projectId={projectId}
sidePeekVisible={sidePeekVisible}
setSidePeekVisible={setSidePeekVisible}
diff --git a/web/components/pages/editor/title.tsx b/web/components/pages/editor/title.tsx
index f472ecb6dca..0c9473690da 100644
--- a/web/components/pages/editor/title.tsx
+++ b/web/components/pages/editor/title.tsx
@@ -33,7 +33,6 @@ export const PageEditorTitle: React.FC = observer((props) => {
) : (
<>