-
+
+
{label}
{payload?.map((item) => (
-
-
{item?.name}:
-
{item?.value}
+
+
{item?.value}
))}
diff --git a/packages/propel/src/charts/tooltip.tsx b/packages/propel/src/charts/tooltip.tsx
deleted file mode 100644
index e7f92a9cb..000000000
--- a/packages/propel/src/charts/tooltip.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import React from "react";
-import { NameType, Payload, ValueType } from "recharts/types/component/DefaultTooltipContent";
-// plane imports
-import { Card, ECardSpacing } from "@plane/ui";
-import { cn } from "@plane/utils";
-
-type Props = {
- active: boolean | undefined;
- label: string | undefined;
- payload: Payload[] | undefined;
- itemKeys: string[];
- itemDotClassNames: Record;
-};
-
-export const CustomTooltip = React.memo((props: Props) => {
- const { active, label, payload, itemKeys, itemDotClassNames } = props;
- // derived values
- const filteredPayload = payload?.filter((item) => item.dataKey && itemKeys.includes(`${item.dataKey}`));
-
- if (!active || !filteredPayload || !filteredPayload.length) return null;
- return (
-
-
- {label}
-
- {filteredPayload.map((item) => {
- if (!item.dataKey) return null;
- return (
-
- {itemDotClassNames[item?.dataKey] && (
-
- )}
-
{item?.name}:
-
{item?.value}
-
- );
- })}
-
- );
-});
-CustomTooltip.displayName = "CustomTooltip";
diff --git a/packages/propel/src/charts/tree-map/root.tsx b/packages/propel/src/charts/tree-map/root.tsx
index 47ea21d72..7add4a6b6 100644
--- a/packages/propel/src/charts/tree-map/root.tsx
+++ b/packages/propel/src/charts/tree-map/root.tsx
@@ -31,6 +31,9 @@ export const TreeMapChart = React.memo((props: TreeMapChartProps) => {
fill: "currentColor",
className: "text-custom-background-90/80 cursor-pointer",
}}
+ wrapperStyle={{
+ pointerEvents: "auto",
+ }}
/>
)}
diff --git a/packages/services/package.json b/packages/services/package.json
index 329b9ba91..952c23da6 100644
--- a/packages/services/package.json
+++ b/packages/services/package.json
@@ -1,6 +1,6 @@
{
"name": "@plane/services",
- "version": "0.25.3",
+ "version": "0.26.0",
"license": "AGPL-3.0",
"private": true,
"main": "./src/index.ts",
diff --git a/packages/shared-state/package.json b/packages/shared-state/package.json
index 333cbf7d8..78ae22dde 100644
--- a/packages/shared-state/package.json
+++ b/packages/shared-state/package.json
@@ -1,6 +1,6 @@
{
"name": "@plane/shared-state",
- "version": "0.25.3",
+ "version": "0.26.0",
"license": "AGPL-3.0",
"description": "Shared state shared across multiple apps internally",
"private": true,
diff --git a/packages/tailwind-config/package.json b/packages/tailwind-config/package.json
index 1c3da98ac..0f17d7936 100644
--- a/packages/tailwind-config/package.json
+++ b/packages/tailwind-config/package.json
@@ -1,6 +1,6 @@
{
"name": "@plane/tailwind-config",
- "version": "0.25.3",
+ "version": "0.26.0",
"license": "AGPL-3.0",
"description": "common tailwind configuration across monorepo",
"main": "tailwind.config.js",
diff --git a/packages/tailwind-config/tailwind.config.js b/packages/tailwind-config/tailwind.config.js
index 3e34ca1f0..700831d12 100644
--- a/packages/tailwind-config/tailwind.config.js
+++ b/packages/tailwind-config/tailwind.config.js
@@ -27,6 +27,7 @@ module.exports = {
theme: {
extend: {
boxShadow: {
+ "custom-shadow": "var(--color-shadow-custom)",
"custom-shadow-2xs": "var(--color-shadow-2xs)",
"custom-shadow-xs": "var(--color-shadow-xs)",
"custom-shadow-sm": "var(--color-shadow-sm)",
@@ -208,6 +209,28 @@ module.exports = {
hover: "rgba(96, 100, 108, 0.25)",
active: "rgba(96, 100, 108, 0.7)",
},
+ subscription: {
+ free: {
+ 200: convertToRGB("--color-subscription-free-200"),
+ 400: convertToRGB("--color-subscription-free-400"),
+ },
+ one: {
+ 200: convertToRGB("--color-subscription-one-200"),
+ 400: convertToRGB("--color-subscription-one-400"),
+ },
+ pro: {
+ 200: convertToRGB("--color-subscription-pro-200"),
+ 400: convertToRGB("--color-subscription-pro-400"),
+ },
+ business: {
+ 200: convertToRGB("--color-subscription-business-200"),
+ 400: convertToRGB("--color-subscription-business-400"),
+ },
+ enterprise: {
+ 200: convertToRGB("--color-subscription-enterprise-200"),
+ 400: convertToRGB("--color-subscription-enterprise-400"),
+ },
+ },
},
onboarding: {
background: {
diff --git a/packages/types/package.json b/packages/types/package.json
index a00a4d312..55861501a 100644
--- a/packages/types/package.json
+++ b/packages/types/package.json
@@ -1,6 +1,6 @@
{
"name": "@plane/types",
- "version": "0.25.3",
+ "version": "0.26.0",
"license": "AGPL-3.0",
"private": true,
"types": "./src/index.d.ts",
diff --git a/packages/types/src/charts.d.ts b/packages/types/src/charts.d.ts
index 473c1077e..b1fc2997d 100644
--- a/packages/types/src/charts.d.ts
+++ b/packages/types/src/charts.d.ts
@@ -1,3 +1,16 @@
+export type TChartLegend = {
+ align: "left" | "center" | "right";
+ verticalAlign: "top" | "middle" | "bottom";
+ layout: "horizontal" | "vertical";
+};
+
+export type TChartMargin = {
+ top?: number;
+ right?: number;
+ bottom?: number;
+ left?: number;
+};
+
export type TChartData = {
// required key
[key in K]: string | number;
@@ -7,15 +20,19 @@ type TChartProps = {
data: TChartData[];
xAxis: {
key: keyof TChartData;
- label: string;
+ label?: string;
+ strokeColor?: string;
};
yAxis: {
- key: keyof TChartData;
- label: string;
- domain?: [number, number];
allowDecimals?: boolean;
+ domain?: [number, number];
+ key: keyof TChartData;
+ label?: string;
+ strokeColor?: string;
};
className?: string;
+ legend?: TChartLegend;
+ margin?: TChartMargin;
tickCount?: {
x?: number;
y?: number;
@@ -25,11 +42,13 @@ type TChartProps = {
export type TBarItem = {
key: T;
- fillClassName: string;
+ label: string;
+ fill: string | ((payload: any) => string);
textClassName: string;
- dotClassName?: string;
showPercentage?: boolean;
stackId: string;
+ showTopBorderRadius?: (barKey: string, payload: any) => boolean;
+ showBottomBorderRadius?: (barKey: string, payload: any) => boolean;
};
export type TBarChartProps = TChartProps & {
@@ -39,9 +58,13 @@ export type TBarChartProps = TChartProps = {
key: T;
- className?: string;
+ label: string;
+ dashedLine: boolean;
+ fill: string;
+ showDot: boolean;
+ smoothCurves: boolean;
+ stroke: string;
style?: Record;
- dotClassName?: string;
};
export type TLineChartProps = TChartProps & {
@@ -50,31 +73,50 @@ export type TLineChartProps = TChartProps = {
key: T;
+ label: string;
stackId: string;
- className?: string;
+ fill: string;
+ fillOpacity: number;
+ showDot: boolean;
+ smoothCurves: boolean;
+ strokeColor: string;
+ strokeOpacity: number;
style?: Record;
- dotClassName?: string;
};
export type TAreaChartProps = TChartProps & {
areas: TAreaItem[];
+ comparisonLine?: {
+ dashedLine: boolean;
+ strokeColor: string;
+ };
};
export type TCellItem = {
key: T;
- className?: string;
- style?: Record;
- dotClassName?: string;
+ fill: string;
};
export type TPieChartProps = Pick<
TChartProps,
- "className" | "data" | "showTooltip"
+ "className" | "data" | "showTooltip" | "legend" | "margin"
> & {
dataKey: T;
cells: TCellItem[];
- innerRadius?: number;
- outerRadius?: number;
+ innerRadius?: number | string;
+ outerRadius?: number | string;
+ cornerRadius?: number;
+ paddingAngle?: number;
+ showLabel: boolean;
+ customLabel?: (value: any) => string;
+ centerLabel?: {
+ className?: string;
+ fill: string;
+ style?: React.CSSProperties;
+ text?: string | number;
+ };
+ tooltipLabel?: string | ((payload: any) => string);
+ customLegend?: (props: any) => React.ReactNode;
};
export type TreeMapItem = {
diff --git a/packages/types/src/common.d.ts b/packages/types/src/common.d.ts
index c45236a9f..b35e408d6 100644
--- a/packages/types/src/common.d.ts
+++ b/packages/types/src/common.d.ts
@@ -26,3 +26,11 @@ export type TLogoProps = {
export type TNameDescriptionLoader = "submitting" | "submitted" | "saved";
export type TFetchStatus = "partial" | "complete" | undefined;
+
+export type ICustomSearchSelectOption = {
+ value: any;
+ query: string;
+ content: React.ReactNode;
+ disabled?: boolean;
+ tooltip?: string | React.ReactNode;
+};
diff --git a/packages/types/src/description_version.d.ts b/packages/types/src/description_version.d.ts
new file mode 100644
index 000000000..8b9816b01
--- /dev/null
+++ b/packages/types/src/description_version.d.ts
@@ -0,0 +1,29 @@
+export type TDescriptionVersion = {
+ created_at: string;
+ created_by: string | null;
+ id: string;
+ last_saved_at: string;
+ owned_by: string;
+ project: string;
+ updated_at: string;
+ updated_by: string | null;
+};
+
+export type TDescriptionVersionDetails = TDescriptionVersion & {
+ description_binary: string | null;
+ description_html: string | null;
+ description_json: object | null;
+ description_stripped: string | null;
+};
+
+export type TDescriptionVersionsListResponse = {
+ cursor: string;
+ next_cursor: string | null;
+ next_page_results: boolean;
+ page_count: number;
+ prev_cursor: string | null;
+ prev_page_results: boolean;
+ results: TDescriptionVersion[];
+ total_pages: number;
+ total_results: number;
+};
diff --git a/packages/types/src/enums.ts b/packages/types/src/enums.ts
index 854c0c614..53138a1d7 100644
--- a/packages/types/src/enums.ts
+++ b/packages/types/src/enums.ts
@@ -6,6 +6,12 @@ export enum EUserPermissions {
export type TUserPermissions = EUserPermissions.ADMIN | EUserPermissions.MEMBER | EUserPermissions.GUEST;
+// project network
+export enum EProjectNetwork {
+ PRIVATE = 0,
+ PUBLIC = 2,
+}
+
// project pages
export enum EPageAccess {
PUBLIC = 0,
diff --git a/packages/types/src/estimate.d.ts b/packages/types/src/estimate.d.ts
index 145edf117..0de2019fa 100644
--- a/packages/types/src/estimate.d.ts
+++ b/packages/types/src/estimate.d.ts
@@ -14,10 +14,7 @@ export interface IEstimatePoint {
updated_by: string | undefined;
}
-export type TEstimateSystemKeys =
- | EEstimateSystem.POINTS
- | EEstimateSystem.CATEGORIES
- | EEstimateSystem.TIME;
+export type TEstimateSystemKeys = EEstimateSystem.POINTS | EEstimateSystem.CATEGORIES | EEstimateSystem.TIME;
export interface IEstimate {
id: string | undefined;
@@ -55,12 +52,14 @@ export type TEstimatePointsObject = {
export type TTemplateValues = {
title: string;
+ i18n_title: string;
values: TEstimatePointsObject[];
hide?: boolean;
};
export type TEstimateSystem = {
name: string;
+ i18n_name: string;
templates: Record;
is_available: boolean;
is_ee: boolean;
@@ -82,6 +81,4 @@ export type TEstimateTypeErrorObject = {
message: string | undefined;
};
-export type TEstimateTypeError =
- | Record
- | undefined;
+export type TEstimateTypeError = Record | undefined;
diff --git a/packages/types/src/home.d.ts b/packages/types/src/home.d.ts
index 56089bf46..f34c15380 100644
--- a/packages/types/src/home.d.ts
+++ b/packages/types/src/home.d.ts
@@ -35,6 +35,7 @@ export type TIssueEntityData = {
sequence_id: number;
project_id: string;
project_identifier: string;
+ is_epic: boolean;
};
export type TActivityEntityData = {
diff --git a/packages/types/src/inbox.d.ts b/packages/types/src/inbox.d.ts
index 69fb01f7c..e7065c6d0 100644
--- a/packages/types/src/inbox.d.ts
+++ b/packages/types/src/inbox.d.ts
@@ -1,6 +1,8 @@
+// plane constants
+import { TInboxIssue, TInboxIssueStatus } from "@plane/constants";
+// plane types
import { TPaginationInfo } from "./common";
import { TIssuePriorities } from "./issues";
-import { TIssue } from "./issues/base";
// filters
export type TInboxIssueFilterMemberKeys = "assignees" | "created_by";
diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts
index bd4e593cc..b6af3b562 100644
--- a/packages/types/src/index.d.ts
+++ b/packages/types/src/index.d.ts
@@ -3,6 +3,7 @@ export * from "./workspace";
export * from "./cycle";
export * from "./dashboard";
export * from "./de-dupe";
+export * from "./description_version";
export * from "./project";
export * from "./state";
export * from "./issues";
@@ -41,3 +42,4 @@ export * from "./charts";
export * from "./home";
export * from "./stickies";
export * from "./utils";
+export * from "./payment";
diff --git a/packages/types/src/instance/auth.d.ts b/packages/types/src/instance/auth.d.ts
index d71cfa0bb..31d3a2582 100644
--- a/packages/types/src/instance/auth.d.ts
+++ b/packages/types/src/instance/auth.d.ts
@@ -21,7 +21,8 @@ export type TInstanceGoogleAuthenticationConfigurationKeys =
export type TInstanceGithubAuthenticationConfigurationKeys =
| "GITHUB_CLIENT_ID"
- | "GITHUB_CLIENT_SECRET";
+ | "GITHUB_CLIENT_SECRET"
+ | "GITHUB_ORGANIZATION_ID";
export type TInstanceGitlabAuthenticationConfigurationKeys =
| "GITLAB_HOST"
diff --git a/packages/types/src/instance/base.d.ts b/packages/types/src/instance/base.d.ts
index 33d0734ad..dc5ee5fc7 100644
--- a/packages/types/src/instance/base.d.ts
+++ b/packages/types/src/instance/base.d.ts
@@ -37,6 +37,7 @@ export interface IInstance {
}
export interface IInstanceConfig {
+ enable_signup: boolean;
is_workspace_creation_disabled: boolean;
is_google_enabled: boolean;
is_github_enabled: boolean;
@@ -72,9 +73,7 @@ export interface IInstanceAdmin {
user_detail: IUserLite;
}
-export type TInstanceIntercomConfigurationKeys =
- | "IS_INTERCOM_ENABLED"
- | "INTERCOM_APP_ID";
+export type TInstanceIntercomConfigurationKeys = "IS_INTERCOM_ENABLED" | "INTERCOM_APP_ID";
export type TInstanceConfigurationKeys =
| TInstanceAIConfigurationKeys
diff --git a/packages/types/src/issues/activity/issue_activity.d.ts b/packages/types/src/issues/activity/issue_activity.d.ts
index 7eccebdf3..7ed0c35d6 100644
--- a/packages/types/src/issues/activity/issue_activity.d.ts
+++ b/packages/types/src/issues/activity/issue_activity.d.ts
@@ -1,3 +1,6 @@
+// plane imports
+import { EInboxIssueSource } from "@plane/constants";
+// local imports
import {
TIssueActivityWorkspaceDetail,
TIssueActivityProjectDetail,
@@ -31,7 +34,7 @@ export type TIssueActivity = {
epoch: number;
issue_comment: string | null;
source_data: {
- source: "IN_APP" | "FORM" | "EMAIL";
+ source: EInboxIssueSource;
source_email?: string;
extra: {
username?: string;
diff --git a/packages/types/src/issues/activity/issue_comment.d.ts b/packages/types/src/issues/activity/issue_comment.d.ts
index aef5134c6..e61b35585 100644
--- a/packages/types/src/issues/activity/issue_comment.d.ts
+++ b/packages/types/src/issues/activity/issue_comment.d.ts
@@ -5,7 +5,15 @@ import {
TIssueActivityUserDetail,
} from "./base";
import { EIssueCommentAccessSpecifier } from "../../enums";
+import { TFileSignedURLResponse } from "../../file";
+import { IUserLite } from "../../users";
+export type TCommentReaction = {
+ id: string;
+ reaction: string;
+ actor: string;
+ actor_detail: IUserLite;
+};
export type TIssueComment = {
id: string;
workspace: string;
@@ -17,6 +25,7 @@ export type TIssueComment = {
actor: string;
actor_detail: TIssueActivityUserDetail;
created_at: string;
+ edited_at?: string | undefined;
updated_at: string;
created_by: string | undefined;
updated_by: string | undefined;
@@ -30,6 +39,23 @@ export type TIssueComment = {
access: EIssueCommentAccessSpecifier;
};
+export type TCommentsOperations = {
+ createComment: (data: Partial) => Promise | undefined>;
+ updateComment: (commentId: string, data: Partial) => Promise;
+ removeComment: (commentId: string) => Promise;
+ uploadCommentAsset: (blockId: string, file: File, commentId?: string) => Promise;
+ addCommentReaction: (commentId: string, reactionEmoji: string) => Promise;
+ deleteCommentReaction: (commentId: string, reactionEmoji: string, userReactions: TCommentReaction[]) => Promise;
+ react: (commentId: string, reactionEmoji: string, userReactions: string[]) => Promise;
+ reactionIds: (commentId: string) =>
+ | {
+ [reaction: string]: string[];
+ }
+ | undefined;
+ userReactions: (commentId: string) => string[] | undefined;
+ getReactionUsers: (reaction: string, reactionIds: Record) => string;
+};
+
export type TIssueCommentMap = {
[issue_id: string]: TIssueComment;
};
diff --git a/packages/types/src/issues/issue.d.ts b/packages/types/src/issues/issue.d.ts
index e38810004..18a150c49 100644
--- a/packages/types/src/issues/issue.d.ts
+++ b/packages/types/src/issues/issue.d.ts
@@ -120,7 +120,7 @@ export type TBulkOperationsPayload = {
export type TIssueDetailWidget = "sub-issues" | "relations" | "links" | "attachments";
-export type TIssueServiceType = EIssueServiceType.ISSUES | EIssueServiceType.EPICS;
+export type TIssueServiceType = EIssueServiceType.ISSUES | EIssueServiceType.EPICS | EIssueServiceType.WORK_ITEMS;
export interface IPublicIssue
extends Pick<
diff --git a/packages/types/src/payment.d.ts b/packages/types/src/payment.d.ts
new file mode 100644
index 000000000..bdbab7f32
--- /dev/null
+++ b/packages/types/src/payment.d.ts
@@ -0,0 +1,36 @@
+import { EProductSubscriptionEnum } from "@plane/constants";
+
+export type TBillingFrequency = "month" | "year";
+
+export type IPaymentProductPrice = {
+ currency: string;
+ id: string;
+ product: string;
+ recurring: TBillingFrequency;
+ unit_amount: number;
+ workspace_amount: number;
+};
+
+export type TProductSubscriptionType = "FREE" | "ONE" | "PRO" | "BUSINESS" | "ENTERPRISE";
+
+export type IPaymentProduct = {
+ description: string;
+ id: string;
+ name: string;
+ type: Omit;
+ payment_quantity: number;
+ prices: IPaymentProductPrice[];
+ is_active: boolean;
+};
+
+export type TSubscriptionPrice = {
+ key: string;
+ id: string | undefined;
+ currency: string;
+ price: number;
+ recurring: TBillingFrequency;
+};
+
+export type TProductBillingFrequency = {
+ [key in EProductSubscriptionEnum]: TBillingFrequency | undefined;
+};
diff --git a/packages/types/src/project/projects.d.ts b/packages/types/src/project/projects.d.ts
index 40562d362..e1d9117a1 100644
--- a/packages/types/src/project/projects.d.ts
+++ b/packages/types/src/project/projects.d.ts
@@ -27,6 +27,7 @@ export interface IPartialProject {
inbox_view: boolean;
guest_view_all_features?: boolean;
project_lead?: IUserLite | string | null;
+ network?: number;
// Timestamps
created_at?: Date;
updated_at?: Date;
@@ -50,7 +51,6 @@ export interface IProject extends IPartialProject {
anchor?: string | null;
is_favorite?: boolean;
members?: string[];
- network?: number;
timezone?: string;
}
diff --git a/packages/types/src/state.d.ts b/packages/types/src/state.d.ts
index 120b216da..d28194dc9 100644
--- a/packages/types/src/state.d.ts
+++ b/packages/types/src/state.d.ts
@@ -24,3 +24,11 @@ export interface IStateLite {
export interface IStateResponse {
[key: string]: IState[];
}
+
+export type TStateOperationsCallbacks = {
+ createState: (data: Partial) => Promise;
+ updateState: (stateId: string, data: Partial) => Promise;
+ deleteState: (stateId: string) => Promise;
+ moveStatePosition: (stateId: string, data: Partial) => Promise;
+ markStateAsDefault: (stateId: string) => Promise;
+};
diff --git a/packages/typescript-config/node-library.json b/packages/typescript-config/node-library.json
new file mode 100644
index 000000000..afb41eff3
--- /dev/null
+++ b/packages/typescript-config/node-library.json
@@ -0,0 +1,26 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "display": "Node.js Library",
+ "extends": "./base.json",
+ "compilerOptions": {
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "target": "ES2020",
+ "lib": ["ES2020"],
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ },
+ "sourceMap": true,
+ "inlineSources": true,
+ "experimentalDecorators": true,
+ "emitDecoratorMetadata": true,
+ "resolveJsonModule": true,
+ "allowSyntheticDefaultImports": true,
+ "forceConsistentCasingInFileNames": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist", "build"]
+}
\ No newline at end of file
diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json
index cac5df873..d31bfd2b0 100644
--- a/packages/typescript-config/package.json
+++ b/packages/typescript-config/package.json
@@ -1,11 +1,12 @@
{
"name": "@plane/typescript-config",
- "version": "0.25.3",
+ "version": "0.26.0",
"license": "AGPL-3.0",
"private": true,
"files": [
"base.json",
"nextjs.json",
- "react-library.json"
+ "react-library.json",
+ "node-library.json"
]
}
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 607205c2a..45550f369 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -2,7 +2,7 @@
"name": "@plane/ui",
"description": "UI components shared across multiple apps internally",
"private": true,
- "version": "0.25.3",
+ "version": "0.26.0",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
diff --git a/packages/ui/src/collapsible/collapsible-button.tsx b/packages/ui/src/collapsible/collapsible-button.tsx
index 2a141aa41..b6198fa6c 100644
--- a/packages/ui/src/collapsible/collapsible-button.tsx
+++ b/packages/ui/src/collapsible/collapsible-button.tsx
@@ -1,10 +1,10 @@
import React, { FC } from "react";
-import { DropdownIcon } from "../icons";
import { cn } from "../../helpers";
+import { DropdownIcon } from "../icons";
type Props = {
isOpen: boolean;
- title: string;
+ title: React.ReactNode;
hideChevron?: boolean;
indicatorElement?: React.ReactNode;
actionItemElement?: React.ReactNode;
diff --git a/packages/ui/src/color-picker/color-picker.tsx b/packages/ui/src/color-picker/color-picker.tsx
new file mode 100644
index 000000000..e53fdc10d
--- /dev/null
+++ b/packages/ui/src/color-picker/color-picker.tsx
@@ -0,0 +1,38 @@
+import * as React from "react";
+
+interface ColorPickerProps {
+ value: string;
+ onChange: (color: string) => void;
+ className?: string;
+}
+
+export const ColorPicker: React.FC = (props) => {
+ const { value, onChange, className = "" } = props;
+ // refs
+ const inputRef = React.useRef(null);
+
+ // handlers
+ const handleOnClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ inputRef.current?.click();
+ };
+
+ return (
+
+
+ onChange(e.target.value)}
+ className="absolute inset-0 size-4 invisible"
+ aria-hidden="true"
+ />
+
+ );
+};
diff --git a/packages/ui/src/color-picker/index.ts b/packages/ui/src/color-picker/index.ts
new file mode 100644
index 000000000..6bad1d67e
--- /dev/null
+++ b/packages/ui/src/color-picker/index.ts
@@ -0,0 +1 @@
+export * from "./color-picker";
diff --git a/packages/ui/src/dropdown/common/options.tsx b/packages/ui/src/dropdown/common/options.tsx
index ae006a842..6be99a9d9 100644
--- a/packages/ui/src/dropdown/common/options.tsx
+++ b/packages/ui/src/dropdown/common/options.tsx
@@ -42,7 +42,7 @@ export const DropdownOptions: React.FC
)}
-
+
<>
{options ? (
options.length > 0 ? (
@@ -50,6 +50,7 @@ export const DropdownOptions: React.FC
cn(
"flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5",
@@ -66,7 +67,7 @@ export const DropdownOptions: React.FC (
<>
{renderItem ? (
- <>{renderItem({ value: keyExtractor(option), selected })}>
+ <>{renderItem({ value: keyExtractor(option), selected, disabled: option.disabled })}>
) : (
<>
{option.value}
diff --git a/packages/ui/src/dropdown/dropdown.d.ts b/packages/ui/src/dropdown/dropdown.d.ts
index 74cd0e45d..dd441d0a8 100644
--- a/packages/ui/src/dropdown/dropdown.d.ts
+++ b/packages/ui/src/dropdown/dropdown.d.ts
@@ -27,7 +27,15 @@ export interface IDropdown {
queryArray?: string[];
sortByKey?: string;
firstItem?: (optionValue: string) => boolean;
- renderItem?: ({ value, selected }: { value: string; selected: boolean }) => React.ReactNode;
+ renderItem?: ({
+ value,
+ selected,
+ disabled,
+ }: {
+ value: string;
+ selected: boolean;
+ disabled?: boolean;
+ }) => React.ReactNode;
loader?: React.ReactNode;
disableSorting?: boolean;
}
@@ -35,7 +43,8 @@ export interface IDropdown {
export interface TDropdownOption {
data: any;
value: string;
- className?: ({ active, selected }: { active: boolean; selected: boolean }) => string;
+ className?: ({ active, selected }: { active: boolean; selected?: boolean }) => string;
+ disabled?: boolean;
}
export interface IMultiSelectDropdown extends IDropdown {
@@ -82,7 +91,9 @@ export interface IDropdownOptions {
handleClose?: () => void;
keyExtractor: (option: TDropdownOption) => string;
- renderItem: (({ value, selected }: { value: string; selected: boolean }) => React.ReactNode) | undefined;
+ renderItem:
+ | (({ value, selected, disabled }: { value: string; selected: boolean; disabled?: boolean }) => React.ReactNode)
+ | undefined;
options: TDropdownOption[] | undefined;
loader?: React.ReactNode;
isMobile?: boolean;
diff --git a/packages/ui/src/dropdowns/context-menu/root.tsx b/packages/ui/src/dropdowns/context-menu/root.tsx
index e4265f100..61554d7bd 100644
--- a/packages/ui/src/dropdowns/context-menu/root.tsx
+++ b/packages/ui/src/dropdowns/context-menu/root.tsx
@@ -2,12 +2,12 @@ import React, { useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom";
// plane helpers
import { useOutsideClickDetector } from "@plane/hooks";
-// components
-import { ContextMenuItem } from "./item";
// helpers
import { cn } from "../../../helpers";
// hooks
import { usePlatformOS } from "../../hooks/use-platform-os";
+// components
+import { ContextMenuItem } from "./item";
export type TContextMenuItem = {
key: string;
diff --git a/packages/ui/src/dropdowns/custom-menu.tsx b/packages/ui/src/dropdowns/custom-menu.tsx
index f21da4381..24c8a106a 100644
--- a/packages/ui/src/dropdowns/custom-menu.tsx
+++ b/packages/ui/src/dropdowns/custom-menu.tsx
@@ -1,14 +1,14 @@
+import { Menu } from "@headlessui/react";
+import { ChevronDown, MoreHorizontal } from "lucide-react";
import * as React from "react";
import ReactDOM from "react-dom";
-import { Menu } from "@headlessui/react";
import { usePopper } from "react-popper";
-import { ChevronDown, MoreHorizontal } from "lucide-react";
// plane helpers
import { useOutsideClickDetector } from "@plane/hooks";
-// hooks
-import { useDropdownKeyDown } from "../hooks/use-dropdown-key-down";
// helpers
import { cn } from "../../helpers";
+// hooks
+import { useDropdownKeyDown } from "../hooks/use-dropdown-key-down";
// types
import { ICustomMenuDropdownProps, ICustomMenuItemProps } from "./helper";
diff --git a/packages/ui/src/dropdowns/custom-search-select.tsx b/packages/ui/src/dropdowns/custom-search-select.tsx
index 4302c12fd..e592f0dc2 100644
--- a/packages/ui/src/dropdowns/custom-search-select.tsx
+++ b/packages/ui/src/dropdowns/custom-search-select.tsx
@@ -1,18 +1,15 @@
-import React, { useRef, useState } from "react";
-import { usePopper } from "react-popper";
import { Combobox } from "@headlessui/react";
import { Check, ChevronDown, Info, Search } from "lucide-react";
+import React, { useRef, useState } from "react";
import { createPortal } from "react-dom";
-// plane helpers
+import { usePopper } from "react-popper";
+// plane imports
import { useOutsideClickDetector } from "@plane/hooks";
-// hooks
-import { useDropdownKeyDown } from "../hooks/use-dropdown-key-down";
-// helpers
+// local imports
import { cn } from "../../helpers";
-// types
-import { ICustomSearchSelectProps } from "./helper";
-// local components
+import { useDropdownKeyDown } from "../hooks/use-dropdown-key-down";
import { Tooltip } from "../tooltip";
+import { ICustomSearchSelectProps } from "./helper";
export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
const {
@@ -36,6 +33,7 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
optionsClassName = "",
value,
tabIndex,
+ noResultsMessage = "No matches found",
} = props;
const [query, setQuery] = useState("");
@@ -201,7 +199,7 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
))
) : (
- No matches found
+ {noResultsMessage}
)
) : (
Loading...
diff --git a/packages/ui/src/dropdowns/helper.tsx b/packages/ui/src/dropdowns/helper.tsx
index a7ef61c3d..0e7587051 100644
--- a/packages/ui/src/dropdowns/helper.tsx
+++ b/packages/ui/src/dropdowns/helper.tsx
@@ -1,5 +1,6 @@
// FIXME: fix this!!!
import { Placement } from "@blueprintjs/popover2";
+import { ICustomSearchSelectOption } from "@plane/types";
export interface IDropdownProps {
customButtonClassName?: string;
@@ -43,15 +44,8 @@ interface CustomSearchSelectProps {
footerOption?: JSX.Element;
onChange: any;
onClose?: () => void;
- options:
- | {
- value: any;
- query: string;
- content: React.ReactNode;
- disabled?: boolean;
- tooltip?: string | React.ReactNode;
- }[]
- | undefined;
+ noResultsMessage?: string;
+ options?: ICustomSearchSelectOption[];
}
interface SingleValueProps {
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index 19edba780..29bf6f248 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -31,3 +31,5 @@ export * from "./card";
export * from "./tag";
export * from "./tabs";
export * from "./calendar";
+export * from "./color-picker";
+export * from "./link";
diff --git a/packages/ui/src/link/block.tsx b/packages/ui/src/link/block.tsx
new file mode 100644
index 000000000..f3a615124
--- /dev/null
+++ b/packages/ui/src/link/block.tsx
@@ -0,0 +1,69 @@
+import React, { FC } from "react";
+// plane utils
+import { calculateTimeAgo, cn, getIconForLink } from "@plane/utils";
+// plane ui
+import { TContextMenuItem } from "../dropdowns/context-menu/root";
+import { CustomMenu } from "../dropdowns/custom-menu";
+
+export type TLinkItemBlockProps = {
+ title: string;
+ url: string;
+ createdAt?: Date | string;
+ menuItems?: TContextMenuItem[];
+ onClick?: () => void;
+};
+
+export const LinkItemBlock: FC = (props) => {
+ // props
+ const { title, url, createdAt, menuItems, onClick } = props;
+ // icons
+ const Icon = getIconForLink(url);
+ return (
+
+
+
+
+
+
{title}
+ {createdAt &&
{calculateTimeAgo(createdAt)}
}
+
+ {menuItems && (
+
+
+ {menuItems.map((item) => (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ item.action();
+ }}
+ className={cn("flex items-center gap-2 w-full ", {
+ "text-custom-text-400": item.disabled,
+ })}
+ disabled={item.disabled}
+ >
+ {item.icon && }
+
+
{item.title}
+ {item.description && (
+
+ {item.description}
+
+ )}
+
+
+ ))}
+
+
+ )}
+
+ );
+};
diff --git a/packages/ui/src/link/index.ts b/packages/ui/src/link/index.ts
new file mode 100644
index 000000000..086dec913
--- /dev/null
+++ b/packages/ui/src/link/index.ts
@@ -0,0 +1 @@
+export * from "./block";
diff --git a/packages/utils/package.json b/packages/utils/package.json
index bd25dae27..fc8600077 100644
--- a/packages/utils/package.json
+++ b/packages/utils/package.json
@@ -1,6 +1,6 @@
{
"name": "@plane/utils",
- "version": "0.25.3",
+ "version": "0.26.0",
"description": "Helper functions shared across multiple apps internally",
"license": "AGPL-3.0",
"private": true,
diff --git a/packages/utils/src/common.ts b/packages/utils/src/common.ts
index fff5d9d8e..d2d02c299 100644
--- a/packages/utils/src/common.ts
+++ b/packages/utils/src/common.ts
@@ -1,5 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
+import { CompleteOrEmpty } from "@plane/types";
// Support email can be configured by the application
export const getSupportEmail = (defaultEmail: string = ""): string => defaultEmail;
@@ -39,3 +40,21 @@ export const partitionValidIds = (ids: string[], validIds: string[]): { valid: s
return { valid, invalid };
};
+
+/**
+ * Checks if an object is complete (has properties) rather than empty.
+ * This helps TypeScript narrow the type from CompleteOrEmpty to T.
+ *
+ * @param obj The object to check, typed as CompleteOrEmpty
+ * @returns A boolean indicating if the object is complete (true) or empty (false)
+ */
+export const isComplete = (obj: CompleteOrEmpty): obj is T => {
+ // Check if object is not null or undefined
+ if (obj == null) return false;
+
+ // Check if it's an object
+ if (typeof obj !== "object") return false;
+
+ // Check if it has any own properties
+ return Object.keys(obj).length > 0;
+};
diff --git a/packages/utils/src/get-icon-for-link.ts b/packages/utils/src/get-icon-for-link.ts
new file mode 100644
index 000000000..0c703a81c
--- /dev/null
+++ b/packages/utils/src/get-icon-for-link.ts
@@ -0,0 +1,64 @@
+import {
+ Github,
+ Linkedin,
+ Twitter,
+ Facebook,
+ Instagram,
+ Youtube,
+ Dribbble,
+ Figma,
+ FileText,
+ FileImage,
+ FileVideo,
+ FileAudio,
+ FileArchive,
+ FileSpreadsheet,
+ FileCode,
+ Mail,
+ Chrome,
+ Link2,
+} from "lucide-react";
+
+type IconMatcher = {
+ pattern: RegExp;
+ icon: typeof Github;
+};
+
+const SOCIAL_MEDIA_MATCHERS: IconMatcher[] = [
+ { pattern: /github\.com/, icon: Github },
+ { pattern: /linkedin\.com/, icon: Linkedin },
+ { pattern: /(twitter\.com|x\.com)/, icon: Twitter },
+ { pattern: /facebook\.com/, icon: Facebook },
+ { pattern: /instagram\.com/, icon: Instagram },
+ { pattern: /youtube\.com/, icon: Youtube },
+ { pattern: /dribbble\.com/, icon: Dribbble },
+];
+
+const PRODUCTIVITY_MATCHERS: IconMatcher[] = [
+ { pattern: /figma\.com/, icon: Figma },
+ { pattern: /(google\.com|docs\.|doc\.)/, icon: FileText },
+];
+
+const FILE_TYPE_MATCHERS: IconMatcher[] = [
+ { pattern: /\.(jpg|jpeg|png|gif|bmp|svg|webp)$/, icon: FileImage },
+ { pattern: /\.(mp4|mov|avi|wmv|flv|mkv)$/, icon: FileVideo },
+ { pattern: /\.(mp3|wav|ogg)$/, icon: FileAudio },
+ { pattern: /\.(zip|rar|7z|tar|gz)$/, icon: FileArchive },
+ { pattern: /\.(xls|xlsx|csv)$/, icon: FileSpreadsheet },
+ { pattern: /\.(pdf|doc|docx|txt)$/, icon: FileText },
+ { pattern: /\.(html|js|ts|jsx|tsx|css|scss)$/, icon: FileCode },
+];
+
+const OTHER_MATCHERS: IconMatcher[] = [
+ { pattern: /^mailto:/, icon: Mail },
+ { pattern: /^http/, icon: Chrome },
+];
+
+export const getIconForLink = (url: string) => {
+ const lowerUrl = url.toLowerCase();
+
+ const allMatchers = [...SOCIAL_MEDIA_MATCHERS, ...PRODUCTIVITY_MATCHERS, ...FILE_TYPE_MATCHERS, ...OTHER_MATCHERS];
+
+ const matchedIcon = allMatchers.find(({ pattern }) => pattern.test(lowerUrl));
+ return matchedIcon?.icon ?? Link2;
+};
diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts
index 57f10c5d4..765dce49d 100644
--- a/packages/utils/src/index.ts
+++ b/packages/utils/src/index.ts
@@ -12,3 +12,8 @@ export * from "./string";
export * from "./theme";
export * from "./workspace";
export * from "./work-item";
+
+export * from "./get-icon-for-link";
+
+export * from "./subscription";
+
diff --git a/packages/utils/src/string.ts b/packages/utils/src/string.ts
index 2fc52a254..19840df4d 100644
--- a/packages/utils/src/string.ts
+++ b/packages/utils/src/string.ts
@@ -55,13 +55,6 @@ export const createSimilarString = (str: string) => {
return shuffled;
};
-/**
- * @description Copies full URL (origin + path) to clipboard
- * @param {string} path - URL path to copy
- * @returns {Promise} Promise that resolves when copying is complete
- * @example
- * await copyUrlToClipboard("issues/123") // copies "https://example.com/issues/123"
- */
/**
* @description Copies text to clipboard
* @param {string} text - Text to copy
@@ -86,8 +79,11 @@ export const copyTextToClipboard = async (text: string): Promise => {
* await copyUrlToClipboard("issues/123") // copies "https://example.com/issues/123"
*/
export const copyUrlToClipboard = async (path: string) => {
- const originUrl = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
- await copyTextToClipboard(`${originUrl}/${path}`);
+ // get origin or default to empty string if not in browser
+ const originUrl = typeof window !== "undefined" ? window.location.origin : "";
+ // create URL object and ensure proper path formatting
+ const url = new URL(path, originUrl);
+ await copyTextToClipboard(url.toString());
};
/**
diff --git a/packages/utils/src/subscription.ts b/packages/utils/src/subscription.ts
new file mode 100644
index 000000000..207f387b7
--- /dev/null
+++ b/packages/utils/src/subscription.ts
@@ -0,0 +1,106 @@
+import orderBy from "lodash/orderBy";
+// plane imports
+import { EProductSubscriptionEnum } from "@plane/constants";
+import { IPaymentProduct, TProductSubscriptionType, TSubscriptionPrice } from "@plane/types";
+
+/**
+ * Calculates the yearly discount percentage when switching from monthly to yearly billing
+ * @param monthlyPrice - The monthly subscription price
+ * @param yearlyPricePerMonth - The monthly equivalent price when billed yearly
+ * @returns The discount percentage as a whole number (floored)
+ */
+export const calculateYearlyDiscount = (monthlyPrice: number, yearlyPricePerMonth: number): number => {
+ const monthlyCost = monthlyPrice * 12;
+ const yearlyCost = yearlyPricePerMonth * 12;
+ const amountSaved = monthlyCost - yearlyCost;
+ const discountPercentage = (amountSaved / monthlyCost) * 100;
+ return Math.floor(discountPercentage);
+};
+
+/**
+ * Gets the display name for a subscription plan variant
+ * @param planVariant - The subscription plan variant enum
+ * @returns The human-readable name of the plan
+ */
+export const getSubscriptionName = (planVariant: EProductSubscriptionEnum): string => {
+ switch (planVariant) {
+ case EProductSubscriptionEnum.FREE:
+ return "Free";
+ case EProductSubscriptionEnum.ONE:
+ return "One";
+ case EProductSubscriptionEnum.PRO:
+ return "Pro";
+ case EProductSubscriptionEnum.BUSINESS:
+ return "Business";
+ case EProductSubscriptionEnum.ENTERPRISE:
+ return "Enterprise";
+ default:
+ return "--";
+ }
+};
+
+/**
+ * Gets the base subscription name for upgrade/downgrade paths
+ * @param planVariant - The current subscription plan variant
+ * @param isSelfHosted - Whether the instance is self-hosted / community
+ * @returns The name of the base subscription plan
+ *
+ * @remarks
+ * - For self-hosted / community instances, the upgrade path differs from cloud instances
+ * - Returns the immediate lower tier subscription name
+ */
+export const getBaseSubscriptionName = (planVariant: TProductSubscriptionType, isSelfHosted: boolean): string => {
+ switch (planVariant) {
+ case EProductSubscriptionEnum.ONE:
+ return getSubscriptionName(EProductSubscriptionEnum.FREE);
+ case EProductSubscriptionEnum.PRO:
+ return isSelfHosted
+ ? getSubscriptionName(EProductSubscriptionEnum.ONE)
+ : getSubscriptionName(EProductSubscriptionEnum.FREE);
+ case EProductSubscriptionEnum.BUSINESS:
+ return getSubscriptionName(EProductSubscriptionEnum.PRO);
+ case EProductSubscriptionEnum.ENTERPRISE:
+ return getSubscriptionName(EProductSubscriptionEnum.BUSINESS);
+ default:
+ return "--";
+ }
+};
+
+export type TSubscriptionPriceDetail = {
+ monthlyPriceDetails: TSubscriptionPrice;
+ yearlyPriceDetails: TSubscriptionPrice;
+};
+
+/**
+ * Gets the price details for a subscription product
+ * @param product - The payment product to get price details for
+ * @returns Array of price details for monthly and yearly plans
+ */
+export const getSubscriptionPriceDetails = (product: IPaymentProduct | undefined): TSubscriptionPriceDetail => {
+ const productPrices = product?.prices || [];
+ const monthlyPriceDetails = orderBy(productPrices, ["recurring"], ["desc"])?.find(
+ (price) => price.recurring === "month"
+ );
+ const monthlyPriceAmount = Number(((monthlyPriceDetails?.unit_amount || 0) / 100).toFixed(2));
+ const yearlyPriceDetails = orderBy(productPrices, ["recurring"], ["desc"])?.find(
+ (price) => price.recurring === "year"
+ );
+ const yearlyPriceAmount = Number(((yearlyPriceDetails?.unit_amount || 0) / 1200).toFixed(2));
+
+ return {
+ monthlyPriceDetails: {
+ key: "monthly",
+ id: monthlyPriceDetails?.id,
+ currency: "$",
+ price: monthlyPriceAmount,
+ recurring: "month",
+ },
+ yearlyPriceDetails: {
+ key: "yearly",
+ id: yearlyPriceDetails?.id,
+ currency: "$",
+ price: yearlyPriceAmount,
+ recurring: "year",
+ },
+ };
+};
diff --git a/setup.sh b/setup.sh
index 2376cb001..2dcaba80f 100755
--- a/setup.sh
+++ b/setup.sh
@@ -1,15 +1,89 @@
#!/bin/bash
-cp ./.env.example ./.env
-# Export for tr error in mac
+# Plane Project Setup Script
+# This script prepares the local development environment by setting up all necessary .env files
+# https://github.com/makeplane/plane
+
+# Set colors for output messages
+GREEN='\033[0;32m'
+BLUE='\033[0;34m'
+YELLOW='\033[1;33m'
+RED='\033[0;31m'
+BOLD='\033[1m'
+NC='\033[0m' # No Color
+
+# Print header
+echo -e "${BOLD}${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+echo -e "${BOLD}${BLUE} Plane - Project Management Tool ${NC}"
+echo -e "${BOLD}${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+echo -e "${BOLD}Setting up your development environment...${NC}\n"
+
+# Function to handle file copying with error checking
+copy_env_file() {
+ local source=$1
+ local destination=$2
+
+ if [ ! -f "$source" ]; then
+ echo -e "${RED}Error: Source file $source does not exist.${NC}"
+ return 1
+ fi
+
+ cp "$source" "$destination"
+
+ if [ $? -eq 0 ]; then
+ echo -e "${GREEN}✓${NC} Copied $destination"
+ else
+ echo -e "${RED}✗${NC} Failed to copy $destination"
+ return 1
+ fi
+}
+
+# Export character encoding settings for macOS compatibility
export LC_ALL=C
export LC_CTYPE=C
+echo -e "${YELLOW}Setting up environment files...${NC}"
-cp ./web/.env.example ./web/.env
-cp ./apiserver/.env.example ./apiserver/.env
-cp ./space/.env.example ./space/.env
-cp ./admin/.env.example ./admin/.env
-cp ./live/.env.example ./live/.env
+# Copy all environment example files
+services=("" "web" "apiserver" "space" "admin" "live")
+success=true
-# Generate the SECRET_KEY that will be used by django
-echo -e "\nSECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./apiserver/.env
+for service in "${services[@]}"; do
+ prefix="./"
+ if [ "$service" != "" ]; then
+ prefix="./$service/"
+ fi
+
+ copy_env_file "${prefix}.env.example" "${prefix}.env" || success=false
+done
+
+# Generate SECRET_KEY for Django
+if [ -f "./apiserver/.env" ]; then
+ echo -e "\n${YELLOW}Generating Django SECRET_KEY...${NC}"
+ SECRET_KEY=$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)
+
+ if [ -z "$SECRET_KEY" ]; then
+ echo -e "${RED}Error: Failed to generate SECRET_KEY.${NC}"
+ echo -e "${RED}Ensure 'tr' and 'head' commands are available on your system.${NC}"
+ success=false
+ else
+ echo -e "SECRET_KEY=\"$SECRET_KEY\"" >> ./apiserver/.env
+ echo -e "${GREEN}✓${NC} Added SECRET_KEY to apiserver/.env"
+ fi
+else
+ echo -e "${RED}✗${NC} apiserver/.env not found. SECRET_KEY not added."
+ success=false
+fi
+
+# Summary
+echo -e "\n${YELLOW}Setup status:${NC}"
+if [ "$success" = true ]; then
+ echo -e "${GREEN}✓${NC} Environment setup completed successfully!\n"
+ echo -e "${BOLD}Next steps:${NC}"
+ echo -e "1. Review the .env files in each folder if needed"
+ echo -e "2. Start the services with: ${BOLD}docker compose -f docker-compose-local.yml up -d${NC}"
+ echo -e "\n${GREEN}Happy coding! 🚀${NC}"
+else
+ echo -e "${RED}✗${NC} Some issues occurred during setup. Please check the errors above.\n"
+ echo -e "For help, visit: ${BLUE}https://github.com/makeplane/plane${NC}"
+ exit 1
+fi
diff --git a/space/core/components/account/oauth/oauth-options.tsx b/space/core/components/account/oauth/oauth-options.tsx
index d514f1b68..153516b34 100644
--- a/space/core/components/account/oauth/oauth-options.tsx
+++ b/space/core/components/account/oauth/oauth-options.tsx
@@ -21,7 +21,7 @@ export const OAuthOptions: React.FC = observer(() => {
)}
- {config?.is_github_enabled &&
}
+ {config?.is_github_enabled &&
}
{config?.is_gitlab_enabled &&
}
>
diff --git a/space/core/components/editor/lite-text-read-only-editor.tsx b/space/core/components/editor/lite-text-read-only-editor.tsx
index 5c8785e90..acb5cf14d 100644
--- a/space/core/components/editor/lite-text-read-only-editor.tsx
+++ b/space/core/components/editor/lite-text-read-only-editor.tsx
@@ -7,6 +7,8 @@ import { EditorMentionsRoot } from "@/components/editor";
// helpers
import { cn } from "@/helpers/common.helper";
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
+// store hooks
+import { useMember } from "@/hooks/store";
type LiteTextReadOnlyEditorWrapperProps = MakeOptional<
Omit,
@@ -17,22 +19,29 @@ type LiteTextReadOnlyEditorWrapperProps = MakeOptional<
};
export const LiteTextReadOnlyEditor = React.forwardRef(
- ({ anchor, workspaceId, disabledExtensions, ...props }, ref) => (
- ,
- }}
- {...props}
- // overriding the customClassName to add relative class passed
- containerClassName={cn(props.containerClassName, "relative p-2")}
- />
- )
+ ({ anchor, workspaceId, disabledExtensions, ...props }, ref) => {
+ const { getMemberById } = useMember();
+
+ return (
+ ,
+ getMentionedEntityDetails: (id: string) => ({
+ display_name: getMemberById(id)?.member__display_name ?? "",
+ }),
+ }}
+ {...props}
+ // overriding the customClassName to add relative class passed
+ containerClassName={cn(props.containerClassName, "relative p-2")}
+ />
+ );
+ }
);
LiteTextReadOnlyEditor.displayName = "LiteTextReadOnlyEditor";
diff --git a/space/core/components/editor/rich-text-editor.tsx b/space/core/components/editor/rich-text-editor.tsx
index 682036f2a..00a9078ae 100644
--- a/space/core/components/editor/rich-text-editor.tsx
+++ b/space/core/components/editor/rich-text-editor.tsx
@@ -6,6 +6,8 @@ import { MakeOptional } from "@plane/types";
import { EditorMentionsRoot } from "@/components/editor";
// helpers
import { getEditorFileHandlers } from "@/helpers/editor.helper";
+// store hooks
+import { useMember } from "@/hooks/store";
interface RichTextEditorWrapperProps
extends MakeOptional, "disabledExtensions"> {
@@ -16,11 +18,14 @@ interface RichTextEditorWrapperProps
export const RichTextEditor = forwardRef((props, ref) => {
const { anchor, containerClassName, uploadFile, workspaceId, disabledExtensions, ...rest } = props;
-
+ const { getMemberById } = useMember();
return (
,
+ getMentionedEntityDetails: (id: string) => ({
+ display_name: getMemberById(id)?.member__display_name ?? "",
+ }),
}}
ref={ref}
disabledExtensions={disabledExtensions ?? []}
@@ -31,7 +36,8 @@ export const RichTextEditor = forwardRef
);
});
diff --git a/space/core/components/editor/rich-text-read-only-editor.tsx b/space/core/components/editor/rich-text-read-only-editor.tsx
index b989e1e41..f2d386629 100644
--- a/space/core/components/editor/rich-text-read-only-editor.tsx
+++ b/space/core/components/editor/rich-text-read-only-editor.tsx
@@ -7,6 +7,8 @@ import { EditorMentionsRoot } from "@/components/editor";
// helpers
import { cn } from "@/helpers/common.helper";
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
+// store hooks
+import { useMember } from "@/hooks/store";
type RichTextReadOnlyEditorWrapperProps = MakeOptional<
Omit,
@@ -17,22 +19,29 @@ type RichTextReadOnlyEditorWrapperProps = MakeOptional<
};
export const RichTextReadOnlyEditor = React.forwardRef(
- ({ anchor, workspaceId, disabledExtensions, ...props }, ref) => (
- ,
- }}
- {...props}
- // overriding the customClassName to add relative class passed
- containerClassName={cn("relative p-0 border-none", props.containerClassName)}
- />
- )
+ ({ anchor, workspaceId, disabledExtensions, ...props }, ref) => {
+ const { getMemberById } = useMember();
+
+ return (
+ ,
+ getMentionedEntityDetails: (id: string) => ({
+ display_name: getMemberById(id)?.member__display_name ?? "",
+ }),
+ }}
+ {...props}
+ // overriding the customClassName to add relative class passed
+ containerClassName={cn("relative p-0 border-none", props.containerClassName)}
+ />
+ );
+ }
);
RichTextReadOnlyEditor.displayName = "RichTextReadOnlyEditor";
diff --git a/space/google.d.ts b/space/google.d.ts
deleted file mode 100644
index c37c83c94..000000000
--- a/space/google.d.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-// google.d.ts
-interface GsiButtonConfiguration {
- type: "standard" | "icon";
- theme?: "outline" | "filled_blue" | "filled_black";
- size?: "large" | "medium" | "small";
- text?: "signin_with" | "signup_with" | "continue_with" | "signup_with";
- shape?: "rectangular" | "pill" | "circle" | "square";
- logo_alignment?: "left" | "center";
- width?: number;
- local?: string;
-}
diff --git a/space/instrumentation.ts b/space/instrumentation.ts
deleted file mode 100644
index 7b89a972e..000000000
--- a/space/instrumentation.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-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 58b6cfa0b..2d3e4e788 100644
--- a/space/next.config.js
+++ b/space/next.config.js
@@ -1,8 +1,4 @@
-// eslint-disable-next-line @typescript-eslint/no-var-requires
-/* eslint-disable @typescript-eslint/no-var-requires */
/** @type {import('next').NextConfig} */
-require("dotenv").config({ path: ".env" });
-const { withSentryConfig } = require("@sentry/nextjs");
const nextConfig = {
trailingSlash: true,
@@ -27,45 +23,19 @@ const nextConfig = {
],
unoptimized: true,
},
+ transpilePackages: [
+ "@plane/constants",
+ "@plane/editor",
+ "@plane/hooks",
+ "@plane/i18n",
+ "@plane/logger",
+ "@plane/propel",
+ "@plane/services",
+ "@plane/shared-state",
+ "@plane/types",
+ "@plane/ui",
+ "@plane/utils",
+ ],
};
-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;
-}
+module.exports = nextConfig;
diff --git a/space/package.json b/space/package.json
index 8be06a5a8..c5a302664 100644
--- a/space/package.json
+++ b/space/package.json
@@ -1,6 +1,6 @@
{
"name": "space",
- "version": "0.25.3",
+ "version": "0.26.0",
"private": true,
"license": "AGPL-3.0",
"scripts": {
@@ -25,7 +25,6 @@
"@plane/types": "*",
"@plane/ui": "*",
"@plane/services": "*",
- "@sentry/nextjs": "^8.54.0",
"axios": "^1.8.3",
"clsx": "^2.0.0",
"date-fns": "^4.1.0",
@@ -37,7 +36,7 @@
"mobx": "^6.10.0",
"mobx-react": "^9.1.1",
"mobx-utils": "^6.0.8",
- "next": "^14.2.20",
+ "next": "^14.2.26",
"next-themes": "^0.2.1",
"nprogress": "^0.2.0",
"react": "^18.3.1",
diff --git a/space/sentry.client.config.ts b/space/sentry.client.config.ts
deleted file mode 100644
index c81030622..000000000
--- a/space/sentry.client.config.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-// 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.ts b/space/sentry.edge.config.ts
deleted file mode 100644
index 2dbc6e93a..000000000
--- a/space/sentry.edge.config.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-// 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.properties b/space/sentry.properties
deleted file mode 100644
index 1741152a3..000000000
--- a/space/sentry.properties
+++ /dev/null
@@ -1,3 +0,0 @@
-defaults.url=https://sentry.io/
-defaults.org=plane
-defaults.project=plane-space
diff --git a/space/sentry.server.config.ts b/space/sentry.server.config.ts
deleted file mode 100644
index e578f1530..000000000
--- a/space/sentry.server.config.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-// This file configures the initialization of Sentry on the server.
-// The config you add here will be used whenever the server handles a request.
-// 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,
-
- // Uncomment the line below to enable Spotlight (https://spotlightjs.com)
- // spotlight: process.env.NODE_ENV === 'development',
-});
diff --git a/turbo.json b/turbo.json
index 1113926ce..65f289b9c 100644
--- a/turbo.json
+++ b/turbo.json
@@ -18,13 +18,7 @@
"NEXT_PUBLIC_POSTHOG_KEY",
"NEXT_PUBLIC_POSTHOG_HOST",
"NEXT_PUBLIC_POSTHOG_DEBUG",
- "NEXT_PUBLIC_SUPPORT_EMAIL",
- "SENTRY_AUTH_TOKEN",
- "SENTRY_ORG_ID",
- "SENTRY_PROJECT_ID",
- "NEXT_PUBLIC_SENTRY_ENVIRONMENT",
- "NEXT_PUBLIC_SENTRY_DSN",
- "SENTRY_MONITORING_ENABLED"
+ "NEXT_PUBLIC_SUPPORT_EMAIL"
],
"tasks": {
"build": {
diff --git a/web/app/[workspaceSlug]/(projects)/extended-project-sidebar.tsx b/web/app/[workspaceSlug]/(projects)/extended-project-sidebar.tsx
index c79a63237..b96f008ab 100644
--- a/web/app/[workspaceSlug]/(projects)/extended-project-sidebar.tsx
+++ b/web/app/[workspaceSlug]/(projects)/extended-project-sidebar.tsx
@@ -8,13 +8,12 @@ import { Plus, Search } from "lucide-react";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui";
-import { cn } from "@plane/utils";
+import { cn, copyUrlToClipboard } from "@plane/utils";
// components
import { CreateProjectModal } from "@/components/project";
import { SidebarProjectsListItem } from "@/components/workspace";
// hooks
import { orderJoinedProjects } from "@/helpers/project.helper";
-import { copyUrlToClipboard } from "@/helpers/string.helper";
import { useAppTheme, useProject, useUserPermissions } from "@/hooks/store";
import useExtendedSidebarOutsideClickDetector from "@/hooks/use-extended-sidebar-overview-outside-click";
import { TProject } from "@/plane-web/types";
diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx
index 381b567df..fbe8ed85d 100644
--- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx
+++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx
@@ -26,7 +26,7 @@ const CycleDetailPage = observer(() => {
const { getProjectById } = useProject();
// const { issuesFilter } = useIssues(EIssuesStoreType.CYCLE);
// hooks
- const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false");
+ const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", false);
useCyclesDetails({
workspaceSlug: workspaceSlug?.toString(),
@@ -34,7 +34,7 @@ const CycleDetailPage = observer(() => {
cycleId: cycleId.toString(),
});
// derived values
- const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
+ const isSidebarCollapsed = storedValue ? (storedValue === true ? true : false) : false;
const cycle = cycleId ? getCycleById(cycleId.toString()) : undefined;
const project = projectId ? getProjectById(projectId.toString()) : undefined;
const pageTitle = project?.name && cycle?.name ? `${project?.name} - ${cycle?.name}` : undefined;
@@ -42,7 +42,7 @@ const CycleDetailPage = observer(() => {
/**
* Toggles the sidebar
*/
- const toggleSidebar = () => setValue(`${!isSidebarCollapsed}`);
+ const toggleSidebar = () => setValue(!isSidebarCollapsed);
// const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx
index 508da58a2..ebe492584 100644
--- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx
+++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx
@@ -1,11 +1,11 @@
"use client";
-import { useCallback, useState } from "react";
+import { useCallback, useRef, useState } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams } from "next/navigation";
// icons
-import { ArrowRight, PanelRight } from "lucide-react";
+import { PanelRight } from "lucide-react";
// plane constants
import {
EIssueLayoutTypes,
@@ -18,17 +18,22 @@ import {
// i18n
import { useTranslation } from "@plane/i18n";
// types
-import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
+import {
+ ICustomSearchSelectOption,
+ IIssueDisplayFilterOptions,
+ IIssueDisplayProperties,
+ IIssueFilterOptions,
+} from "@plane/types";
// ui
-import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip, Header } from "@plane/ui";
+import { Breadcrumbs, Button, ContrastIcon, Tooltip, Header, CustomSearchSelect } from "@plane/ui";
// components
import { ProjectAnalyticsModal } from "@/components/analytics";
-import { BreadcrumbLink } from "@/components/common";
+import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
+import { CycleQuickActions } from "@/components/cycles";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
// helpers
import { cn } from "@/helpers/common.helper";
import { isIssueFilterActive } from "@/helpers/filter.helper";
-import { truncateText } from "@/helpers/string.helper";
// hooks
import {
useEventTracker,
@@ -47,28 +52,9 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
-const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => {
- // router
- const { workspaceSlug, projectId } = useParams();
- // store hooks
- const { getCycleById } = useCycle();
- // derived values
- const cycle = getCycleById(cycleId);
- //
-
- if (!cycle) return null;
-
- return (
-
-
-
- {truncateText(cycle.name, 40)}
-
-
- );
-};
-
export const CycleIssuesHeader: React.FC = observer(() => {
+ // refs
+ const parentRef = useRef(null);
// states
const [analyticsModal, setAnalyticsModal] = useState(false);
// router
@@ -99,11 +85,11 @@ export const CycleIssuesHeader: React.FC = observer(() => {
const activeLayout = issueFilters?.displayFilters?.layout;
- const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false");
+ const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", false);
- const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
+ const isSidebarCollapsed = storedValue ? (storedValue === true ? true : false) : false;
const toggleSidebar = () => {
- setValue(`${!isSidebarCollapsed}`);
+ setValue(!isSidebarCollapsed);
};
const handleLayoutChange = useCallback(
@@ -159,7 +145,19 @@ export const CycleIssuesHeader: React.FC = observer(() => {
EUserPermissionsLevel.PROJECT
);
- const issuesCount = getGroupIssueCount(undefined, undefined, false);
+ const switcherOptions = currentProjectCycleIds
+ ?.map((id) => {
+ const _cycle = id === cycleId ? cycleDetails : getCycleById(id);
+ if (!_cycle) return;
+ return {
+ value: _cycle.id,
+ query: _cycle.name,
+ content: ,
+ };
+ })
+ .filter((option) => option !== undefined) as ICustomSearchSelectOption[];
+
+ const workItemsCount = getGroupIssueCount(undefined, undefined, false);
return (
<>
@@ -201,39 +199,37 @@ export const CycleIssuesHeader: React.FC = observer(() => {
{
+ router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${value}`);
+ }}
label={
- <>
-
-
-
{cycleDetails?.name && cycleDetails.name}
- {issuesCount && issuesCount > 0 ? (
-
1 ? "work items" : "work item"
- } in this cycle`}
- position="bottom"
- >
-
- {issuesCount}
-
-
- ) : null}
-
- >
+
+
+ {workItemsCount && workItemsCount > 0 ? (
+ 1 ? "work items" : "work item"
+ } in this cycle`}
+ position="bottom"
+ >
+
+ {workItemsCount}
+
+
+ ) : null}
+
}
- className="ml-1.5 flex-shrink-0 truncate"
- placement="bottom-start"
- >
- {currentProjectCycleIds?.map((cycleId) => )}
-
+ />
}
/>