diff --git a/packages/constants/src/issue.ts b/packages/constants/src/issue.ts
index 5db398c7634..9f6a1a2e2e9 100644
--- a/packages/constants/src/issue.ts
+++ b/packages/constants/src/issue.ts
@@ -11,6 +11,7 @@ export enum EIssueGroupByToServerOptions {
"target_date" = "target_date",
"project" = "project_id",
"created_by" = "created_by",
+ "team_project" = "project_id",
}
export enum EIssueGroupBYServerToProperty {
diff --git a/packages/types/src/common.d.ts b/packages/types/src/common.d.ts
index 5fe31ad0006..7e755fcc28e 100644
--- a/packages/types/src/common.d.ts
+++ b/packages/types/src/common.d.ts
@@ -22,3 +22,5 @@ export type TLogoProps = {
background_color?: string;
};
};
+
+export type TNameDescriptionLoader = "submitting" | "submitted" | "saved";
diff --git a/packages/types/src/enums.ts b/packages/types/src/enums.ts
index df6a462b02e..e37e2f4a5d5 100644
--- a/packages/types/src/enums.ts
+++ b/packages/types/src/enums.ts
@@ -59,4 +59,5 @@ export enum EFileAssetType {
USER_AVATAR = "USER_AVATAR",
USER_COVER = "USER_COVER",
WORKSPACE_LOGO = "WORKSPACE_LOGO",
+ TEAM_SPACE_DESCRIPTION = "TEAM_SPACE_DESCRIPTION",
}
diff --git a/packages/types/src/issues.d.ts b/packages/types/src/issues.d.ts
index 9bbfa36b1f0..b6d32bdf816 100644
--- a/packages/types/src/issues.d.ts
+++ b/packages/types/src/issues.d.ts
@@ -211,7 +211,8 @@ export type GroupByColumnTypes =
| "priority"
| "labels"
| "assignees"
- | "created_by";
+ | "created_by"
+ | "team_project";
export interface IGroupByColumn {
id: string;
diff --git a/packages/types/src/view-props.d.ts b/packages/types/src/view-props.d.ts
index 57baa4cfdd1..aa1c75cdbdc 100644
--- a/packages/types/src/view-props.d.ts
+++ b/packages/types/src/view-props.d.ts
@@ -18,6 +18,7 @@ export type TIssueGroupByOptions =
| "cycle"
| "module"
| "target_date"
+ | "team_project"
| null;
export type TIssueOrderByOptions =
@@ -69,6 +70,7 @@ export type TIssueParams =
| "start_date"
| "target_date"
| "project"
+ | "team_project"
| "group_by"
| "sub_group_by"
| "order_by"
@@ -92,6 +94,7 @@ export interface IIssueFilterOptions {
cycle?: string[] | null;
module?: string[] | null;
project?: string[] | null;
+ team_project?: string[] | null;
start_date?: string[] | null;
state?: string[] | null;
state_group?: string[] | null;
diff --git a/packages/ui/src/breadcrumbs/index.ts b/packages/ui/src/breadcrumbs/index.ts
index 669f5575772..05a8bdbf1b6 100644
--- a/packages/ui/src/breadcrumbs/index.ts
+++ b/packages/ui/src/breadcrumbs/index.ts
@@ -1 +1,2 @@
export * from "./breadcrumbs";
+export * from "./navigation-dropdown";
diff --git a/packages/ui/src/breadcrumbs/navigation-dropdown.tsx b/packages/ui/src/breadcrumbs/navigation-dropdown.tsx
new file mode 100644
index 00000000000..a716ca65e19
--- /dev/null
+++ b/packages/ui/src/breadcrumbs/navigation-dropdown.tsx
@@ -0,0 +1,96 @@
+"use client";
+
+import * as React from "react";
+import { CheckIcon, ChevronDownIcon } from "lucide-react";
+// ui
+import { CustomMenu, TContextMenuItem } from "../dropdowns";
+// helpers
+import { cn } from "../../helpers";
+
+type TBreadcrumbNavigationDropdownProps = {
+ selectedItemKey: string;
+ navigationItems: TContextMenuItem[];
+ navigationDisabled?: boolean;
+};
+
+export const BreadcrumbNavigationDropdown = (props: TBreadcrumbNavigationDropdownProps) => {
+ const { selectedItemKey, navigationItems, navigationDisabled = false } = props;
+ // derived values
+ const selectedItem = navigationItems.find((item) => item.key === selectedItemKey);
+ const selectedItemIcon = selectedItem?.icon ? (
+
+ ) : undefined;
+
+ // if no selected item, return null
+ if (!selectedItem) return null;
+
+ const NavigationButton = ({ className }: { className?: string }) => (
+
+ {selectedItemIcon && (
+ {selectedItemIcon}
+ )}
+ {selectedItem.title}
+
+ );
+
+ if (navigationDisabled) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ }
+ placement="bottom-start"
+ closeOnSelect
+ >
+ {navigationItems.map((item) => {
+ if (item.shouldRender === false) return null;
+ return (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ if (item.key === selectedItemKey) return;
+ item.action();
+ }}
+ className={cn(
+ "flex items-center gap-2",
+ {
+ "text-custom-text-400": item.disabled,
+ },
+ item.className
+ )}
+ disabled={item.disabled}
+ >
+ {item.icon && }
+
+
{item.title}
+ {item.description && (
+
+ {item.description}
+
+ )}
+
+ {item.key === selectedItemKey && }
+
+ );
+ })}
+
+ );
+};
diff --git a/packages/ui/src/icons/index.ts b/packages/ui/src/icons/index.ts
index f8a2b1c849b..573efd99fb7 100644
--- a/packages/ui/src/icons/index.ts
+++ b/packages/ui/src/icons/index.ts
@@ -16,6 +16,7 @@ export * from "./epic-icon";
export * from "./full-screen-panel-icon";
export * from "./github-icon";
export * from "./gitlab-icon";
+export * from "./info-fill-icon";
export * from "./info-icon";
export * from "./layer-stack";
export * from "./layers-icon";
@@ -38,3 +39,5 @@ export * from "./done-icon";
export * from "./pending-icon";
export * from "./pi-chat";
export * from "./workspace-icon";
+export * from "./teams";
+export * from "./lead-icon";
diff --git a/packages/ui/src/icons/lead-icon.tsx b/packages/ui/src/icons/lead-icon.tsx
new file mode 100644
index 00000000000..75575d35ec5
--- /dev/null
+++ b/packages/ui/src/icons/lead-icon.tsx
@@ -0,0 +1,26 @@
+import * as React from "react";
+
+import { ISvgIcons } from "./type";
+
+export const LeadIcon: React.FC = ({ className = "text-current", ...rest }) => (
+
+);
diff --git a/packages/ui/src/icons/teams.tsx b/packages/ui/src/icons/teams.tsx
new file mode 100644
index 00000000000..b730555989e
--- /dev/null
+++ b/packages/ui/src/icons/teams.tsx
@@ -0,0 +1,19 @@
+import * as React from "react";
+
+import { ISvgIcons } from "./type";
+
+export const TeamsIcon: React.FC = ({ className = "text-current", ...rest }) => (
+
+);
diff --git a/packages/ui/src/tabs/tabs.tsx b/packages/ui/src/tabs/tabs.tsx
index a323d9721fd..92bc3ad7276 100644
--- a/packages/ui/src/tabs/tabs.tsx
+++ b/packages/ui/src/tabs/tabs.tsx
@@ -1,4 +1,4 @@
-import React, { FC, Fragment } from "react";
+import React, { FC, Fragment, useEffect, useState } from "react";
import { Tab } from "@headlessui/react";
import { LucideProps } from "lucide-react";
// helpers
@@ -11,11 +11,12 @@ type TabItem = {
label?: React.ReactNode;
content: React.ReactNode;
disabled?: boolean;
+ onClick?: () => void;
};
type TTabsProps = {
tabs: TabItem[];
- storageKey: string;
+ storageKey?: string;
actions?: React.ReactNode;
defaultTab?: string;
containerClassName?: string;
@@ -23,6 +24,8 @@ type TTabsProps = {
tabListClassName?: string;
tabClassName?: string;
tabPanelClassName?: string;
+ size?: "sm" | "md" | "lg";
+ storeInLocalStorage?: boolean;
};
export const Tabs: FC = (props: TTabsProps) => {
@@ -36,15 +39,28 @@ export const Tabs: FC = (props: TTabsProps) => {
tabListClassName = "",
tabClassName = "",
tabPanelClassName = "",
+ size = "md",
+ storeInLocalStorage = true,
} = props;
// local storage
- const { storedValue, setValue } = useLocalStorage(`tab-${storageKey}`, defaultTab);
+ const { storedValue, setValue } = useLocalStorage(
+ storeInLocalStorage && storageKey ? `tab-${storageKey}` : `tab-${tabs[0]?.key}`,
+ defaultTab
+ );
+ // state
+ const [selectedTab, setSelectedTab] = useState(storedValue ?? defaultTab);
+
+ useEffect(() => {
+ if (storeInLocalStorage) {
+ setValue(selectedTab);
+ }
+ }, [selectedTab, setValue, storeInLocalStorage, storageKey]);
const currentTabIndex = (tabKey: string): number => tabs.findIndex((tab) => tab.key === tabKey);
return (
-
+
= (props: TTabsProps) => {
: tab.disabled
? "text-custom-text-400 cursor-not-allowed"
: "text-custom-text-400 hover:text-custom-text-300 hover:bg-custom-background-80/60",
+ {
+ "text-xs": size === "sm",
+ "text-sm": size === "md",
+ "text-base": size === "lg",
+ },
tabClassName
)
}
key={tab.key}
onClick={() => {
- if (!tab.disabled) setValue(tab.key);
+ if (!tab.disabled) setSelectedTab(tab.key);
+ tab.onClick?.();
}}
disabled={tab.disabled}
>
diff --git a/space/core/store/helpers/base-issues.store.ts b/space/core/store/helpers/base-issues.store.ts
index 004aa06c630..7abfa324a8d 100644
--- a/space/core/store/helpers/base-issues.store.ts
+++ b/space/core/store/helpers/base-issues.store.ts
@@ -26,7 +26,7 @@ import { CoreRootStore } from "../root.store";
// constants
// helpers
-export type TIssueDisplayFilterOptions = Exclude | "target_date";
+export type TIssueDisplayFilterOptions = Exclude | "target_date";
export enum EIssueGroupedAction {
ADD = "ADD",
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 a1f7071a449..7a29f055306 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
@@ -77,7 +77,12 @@ const CycleDetailPage = observer(() => {
"0px 1px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 4px 0px rgba(16, 24, 40, 0.06), 0px 1px 8px -1px rgba(16, 24, 40, 0.06)",
}}
>
-
+
)}
diff --git a/web/app/[workspaceSlug]/(projects)/sidebar.tsx b/web/app/[workspaceSlug]/(projects)/sidebar.tsx
index ac55fdec81e..11d23e35f05 100644
--- a/web/app/[workspaceSlug]/(projects)/sidebar.tsx
+++ b/web/app/[workspaceSlug]/(projects)/sidebar.tsx
@@ -21,6 +21,7 @@ import { useFavorite } from "@/hooks/store/use-favorite";
import useSize from "@/hooks/use-window-size";
// plane web components
import { SidebarAppSwitcher } from "@/plane-web/components/sidebar";
+import { SidebarTeamsList } from "@/plane-web/components/workspace/sidebar/teams-sidebar-list";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export const AppSidebar: FC = observer(() => {
@@ -47,7 +48,7 @@ export const AppSidebar: FC = observer(() => {
});
useEffect(() => {
- if (windowSize[0] < 768) !sidebarCollapsed && toggleSidebar();
+ if (windowSize[0] < 768 && !sidebarCollapsed) toggleSidebar();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [windowSize]);
@@ -73,9 +74,12 @@ export const AppSidebar: FC = observer(() => {
"px-4": !sidebarCollapsed,
})}
>
+ {/* Workspace switcher and settings */}
-
+ {/* App switcher */}
+ {canPerformWorkspaceMemberActions && }
+ {/* Quick actions */}
{
"vertical-scrollbar px-4": !sidebarCollapsed,
})}
>
+ {/* User Menu */}
-
+ {/* Workspace Menu */}
+ {/* Favorites Menu */}
{canPerformWorkspaceMemberActions && !isFavoriteEmpty && }
-
+ {/* Teams List */}
+
+ {/* Projects List */}
+ {/* Help Section */}
diff --git a/web/ce/components/cycles/active-cycle/root.tsx b/web/ce/components/cycles/active-cycle/root.tsx
index a173cfda03a..5ebddc63f23 100644
--- a/web/ce/components/cycles/active-cycle/root.tsx
+++ b/web/ce/components/cycles/active-cycle/root.tsx
@@ -1,5 +1,6 @@
"use client";
+import { useMemo } from "react";
import { observer } from "mobx-react";
import { Disclosure } from "@headlessui/react";
// ui
@@ -22,68 +23,80 @@ import { ActiveCycleIssueDetails } from "@/store/issue/cycle";
interface IActiveCycleDetails {
workspaceSlug: string;
projectId: string;
+ cycleId?: string;
+ showHeader?: boolean;
}
export const ActiveCycleRoot: React.FC = observer((props) => {
- const { workspaceSlug, projectId } = props;
- const { currentProjectActiveCycle, currentProjectActiveCycleId } = useCycle();
+ const { workspaceSlug, projectId, cycleId: propsCycleId, showHeader = true } = props;
+ const { currentProjectActiveCycleId } = useCycle();
+ // derived values
+ const cycleId = propsCycleId ?? currentProjectActiveCycleId;
+ // fetch cycle details
const {
handleFiltersUpdate,
cycle: activeCycle,
cycleIssueDetails,
- } = useCyclesDetails({ workspaceSlug, projectId, cycleId: currentProjectActiveCycleId });
+ } = useCyclesDetails({ workspaceSlug, projectId, cycleId });
+
+ const ActiveCyclesComponent = useMemo(
+ () => (
+ <>
+ {!cycleId || !activeCycle ? (
+
+ ) : (
+
+ {cycleId && (
+
+ )}
+
+
+
+
+ )}
+ >
+ ),
+ [cycleId, activeCycle, workspaceSlug, projectId, handleFiltersUpdate, cycleIssueDetails]
+ );
return (
<>
-
- {({ open }) => (
- <>
-
-
-
-
- {!currentProjectActiveCycle ? (
-
- ) : (
-
- {currentProjectActiveCycleId && (
-
- )}
-
-
-
-
- )}
-
- >
- )}
-
+ {showHeader ? (
+
+ {({ open }) => (
+ <>
+
+
+
+ {ActiveCyclesComponent}
+ >
+ )}
+
+ ) : (
+ <>{ActiveCyclesComponent}>
+ )}
>
);
});
diff --git a/web/ce/components/issues/filters/index.ts b/web/ce/components/issues/filters/index.ts
index 2cd80e3a7e5..f0f36b6c97e 100644
--- a/web/ce/components/issues/filters/index.ts
+++ b/web/ce/components/issues/filters/index.ts
@@ -1,2 +1,3 @@
export * from "./applied-filters";
export * from "./issue-types";
+export * from "./team-project";
diff --git a/web/ce/components/issues/filters/team-project.tsx b/web/ce/components/issues/filters/team-project.tsx
new file mode 100644
index 00000000000..4f4787fef8b
--- /dev/null
+++ b/web/ce/components/issues/filters/team-project.tsx
@@ -0,0 +1,12 @@
+"use client";
+
+import React from "react";
+import { observer } from "mobx-react";
+
+type Props = {
+ appliedFilters: string[] | null;
+ handleUpdate: (val: string) => void;
+ searchQuery: string;
+};
+
+export const FilterTeamProjects: React.FC = observer(() => null);
diff --git a/web/ce/components/issues/issue-layouts/utils.tsx b/web/ce/components/issues/issue-layouts/utils.tsx
new file mode 100644
index 00000000000..48dca43bd92
--- /dev/null
+++ b/web/ce/components/issues/issue-layouts/utils.tsx
@@ -0,0 +1,4 @@
+// types
+import { IGroupByColumn } from "@plane/types";
+
+export const getTeamProjectColumns = (): IGroupByColumn[] | undefined => undefined;
diff --git a/web/ce/components/workspace/sidebar/teams-sidebar-list.tsx b/web/ce/components/workspace/sidebar/teams-sidebar-list.tsx
new file mode 100644
index 00000000000..92cbdfc5f5f
--- /dev/null
+++ b/web/ce/components/workspace/sidebar/teams-sidebar-list.tsx
@@ -0,0 +1 @@
+export const SidebarTeamsList = () => null;
diff --git a/web/ce/constants/dashboard.ts b/web/ce/constants/dashboard.ts
index 8872982fc5b..0df2719a772 100644
--- a/web/ce/constants/dashboard.ts
+++ b/web/ce/constants/dashboard.ts
@@ -1,17 +1,19 @@
"use client";
// icons
-import { Home, Inbox, PenSquare } from "lucide-react";
+import { Briefcase, Home, Inbox, Layers, PenSquare, BarChart2 } from "lucide-react";
// ui
-import { UserActivityIcon } from "@plane/ui";
+import { UserActivityIcon, ContrastIcon } from "@plane/ui";
import { Props } from "@/components/icons/types";
+// constants
import { TLinkOptions } from "@/constants/dashboard";
+// plane web constants
import { EUserPermissions } from "@/plane-web/constants/user-permissions";
// plane web types
-import { TSidebarUserMenuItemKeys } from "@/plane-web/types/dashboard";
+import { TSidebarUserMenuItemKeys, TSidebarWorkspaceMenuItemKeys } from "@/plane-web/types/dashboard";
-export type TSidebarUserMenuItems = {
- key: TSidebarUserMenuItemKeys;
+export type TSidebarMenuItems = {
+ key: T;
label: string;
href: string;
access: EUserPermissions[];
@@ -19,6 +21,8 @@ export type TSidebarUserMenuItems = {
Icon: React.FC;
};
+export type TSidebarUserMenuItems = TSidebarMenuItems;
+
export const SIDEBAR_USER_MENU_ITEMS: TSidebarUserMenuItems[] = [
{
key: "home",
@@ -54,3 +58,47 @@ export const SIDEBAR_USER_MENU_ITEMS: TSidebarUserMenuItems[] = [
Icon: PenSquare,
},
];
+
+export type TSidebarWorkspaceMenuItems = TSidebarMenuItems;
+
+export const SIDEBAR_WORKSPACE_MENU: Partial> = {
+ projects: {
+ key: "projects",
+ label: "Projects",
+ href: `/projects`,
+ access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
+ highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/projects/`,
+ Icon: Briefcase,
+ },
+ "all-issues": {
+ key: "all-issues",
+ label: "Views",
+ href: `/workspace-views/all-issues`,
+ access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
+ highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/workspace-views/`),
+ Icon: Layers,
+ },
+ "active-cycles": {
+ key: "active-cycles",
+ label: "Cycles",
+ href: `/active-cycles`,
+ access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
+ highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/active-cycles/`,
+ Icon: ContrastIcon,
+ },
+ analytics: {
+ key: "analytics",
+ label: "Analytics",
+ href: `/analytics`,
+ access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
+ highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/analytics/`),
+ Icon: BarChart2,
+ },
+};
+
+export const SIDEBAR_WORKSPACE_MENU_ITEMS: TSidebarWorkspaceMenuItems[] = [
+ SIDEBAR_WORKSPACE_MENU?.projects,
+ SIDEBAR_WORKSPACE_MENU?.["all-issues"],
+ SIDEBAR_WORKSPACE_MENU?.["active-cycles"],
+ SIDEBAR_WORKSPACE_MENU?.analytics,
+].filter((item): item is TSidebarWorkspaceMenuItems => item !== undefined);
diff --git a/web/ce/constants/issues.ts b/web/ce/constants/issues.ts
index dc6ffbcb8c2..99b8ef90de1 100644
--- a/web/ce/constants/issues.ts
+++ b/web/ce/constants/issues.ts
@@ -1,4 +1,6 @@
import { TIssueActivityComment } from "@plane/types";
+// constants
+import { ILayoutDisplayFiltersOptions } from "@/constants/issue";
export enum EActivityFilterType {
ACTIVITY = "ACTIVITY",
@@ -19,7 +21,7 @@ export const ACTIVITY_FILTER_TYPE_OPTIONS: Record void;
@@ -32,3 +34,7 @@ export const filterActivityOnSelectedFilters = (
activity.filter((activity) => filter.includes(activity.activity_type as TActivityFilters));
export const ENABLE_ISSUE_DEPENDENCIES = false;
+
+export const ADDITIONAL_ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
+ [pageType: string]: { [layoutType: string]: ILayoutDisplayFiltersOptions };
+} = {};
diff --git a/web/ce/helpers/dashboard.helper.ts b/web/ce/helpers/dashboard.helper.ts
index b2fba63adb0..c96c818a1f0 100644
--- a/web/ce/helpers/dashboard.helper.ts
+++ b/web/ce/helpers/dashboard.helper.ts
@@ -1,5 +1,8 @@
// plane web types
-import { TSidebarUserMenuItemKeys } from "@/plane-web/types/dashboard";
+import { TSidebarUserMenuItemKeys, TSidebarWorkspaceMenuItemKeys } from "@/plane-web/types/dashboard";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const isUserFeatureEnabled = (featureKey: TSidebarUserMenuItemKeys) => true;
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export const isWorkspaceFeatureEnabled = (featureKey: TSidebarWorkspaceMenuItemKeys, workspaceSlug: string) => true;
diff --git a/web/ce/helpers/issue-action-helper.ts b/web/ce/helpers/issue-action-helper.ts
new file mode 100644
index 00000000000..b1644e2aa26
--- /dev/null
+++ b/web/ce/helpers/issue-action-helper.ts
@@ -0,0 +1,15 @@
+import { IssueActions } from "@/hooks/use-issues-actions";
+
+export const useTeamIssueActions: () => IssueActions = () => ({
+ fetchIssues: () => Promise.resolve(undefined),
+ fetchNextIssues: () => Promise.resolve(undefined),
+ removeIssue: () => Promise.resolve(undefined),
+ updateFilters: () => Promise.resolve(undefined),
+});
+
+export const useTeamViewIssueActions: () => IssueActions = () => ({
+ fetchIssues: () => Promise.resolve(undefined),
+ fetchNextIssues: () => Promise.resolve(undefined),
+ removeIssue: () => Promise.resolve(undefined),
+ updateFilters: () => Promise.resolve(undefined),
+});
diff --git a/web/ce/store/command-palette.store.ts b/web/ce/store/command-palette.store.ts
index 47c9280cd41..1b6fabf1875 100644
--- a/web/ce/store/command-palette.store.ts
+++ b/web/ce/store/command-palette.store.ts
@@ -1,12 +1,26 @@
-import { makeObservable } from "mobx";
+import { computed, makeObservable } from "mobx";
// types / constants
import { BaseCommandPaletteStore, IBaseCommandPaletteStore } from "@/store/base-command-palette.store";
-export type ICommandPaletteStore = IBaseCommandPaletteStore;
+export interface ICommandPaletteStore extends IBaseCommandPaletteStore {
+ // computed
+ isAnyModalOpen: boolean;
+}
export class CommandPaletteStore extends BaseCommandPaletteStore implements ICommandPaletteStore {
constructor() {
super();
- makeObservable(this, {});
+ makeObservable(this, {
+ // computed
+ isAnyModalOpen: computed,
+ });
+ }
+
+ /**
+ * Checks whether any modal is open or not in the base command palette.
+ * @returns boolean
+ */
+ get isAnyModalOpen(): boolean {
+ return Boolean(super.getCoreModalsState());
}
}
diff --git a/web/ce/store/issue/team-views/filter.store.ts b/web/ce/store/issue/team-views/filter.store.ts
new file mode 100644
index 00000000000..9c33f94051c
--- /dev/null
+++ b/web/ce/store/issue/team-views/filter.store.ts
@@ -0,0 +1,12 @@
+import { IProjectViewIssuesFilter, ProjectViewIssuesFilter } from "@/store/issue/project-views";
+import { IIssueRootStore } from "@/store/issue/root.store";
+
+// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
+export type ITeamViewIssuesFilter = IProjectViewIssuesFilter;
+
+// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
+export class TeamViewIssuesFilter extends ProjectViewIssuesFilter implements IProjectViewIssuesFilter {
+ constructor(_rootStore: IIssueRootStore) {
+ super(_rootStore);
+ }
+}
diff --git a/web/ce/store/issue/team-views/index.ts b/web/ce/store/issue/team-views/index.ts
new file mode 100644
index 00000000000..0fe6c946b0c
--- /dev/null
+++ b/web/ce/store/issue/team-views/index.ts
@@ -0,0 +1,2 @@
+export * from "./filter.store";
+export * from "./issue.store";
diff --git a/web/ce/store/issue/team-views/issue.store.ts b/web/ce/store/issue/team-views/issue.store.ts
new file mode 100644
index 00000000000..328370f853d
--- /dev/null
+++ b/web/ce/store/issue/team-views/issue.store.ts
@@ -0,0 +1,13 @@
+import { IProjectViewIssues, ProjectViewIssues } from "@/store/issue/project-views";
+import { IIssueRootStore } from "@/store/issue/root.store";
+import { ITeamViewIssuesFilter } from "./filter.store";
+
+// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
+export type ITeamViewIssues = IProjectViewIssues;
+
+// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
+export class TeamViewIssues extends ProjectViewIssues implements IProjectViewIssues {
+ constructor(_rootStore: IIssueRootStore, teamViewFilterStore: ITeamViewIssuesFilter) {
+ super(_rootStore, teamViewFilterStore);
+ }
+}
diff --git a/web/ce/store/issue/team/filter.store.ts b/web/ce/store/issue/team/filter.store.ts
new file mode 100644
index 00000000000..42b2d5dd248
--- /dev/null
+++ b/web/ce/store/issue/team/filter.store.ts
@@ -0,0 +1,12 @@
+import { IProjectIssuesFilter, ProjectIssuesFilter } from "@/store/issue/project";
+import { IIssueRootStore } from "@/store/issue/root.store";
+
+// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
+export type ITeamIssuesFilter = IProjectIssuesFilter;
+
+// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
+export class TeamIssuesFilter extends ProjectIssuesFilter implements IProjectIssuesFilter {
+ constructor(_rootStore: IIssueRootStore) {
+ super(_rootStore);
+ }
+}
diff --git a/web/ce/store/issue/team/index.ts b/web/ce/store/issue/team/index.ts
new file mode 100644
index 00000000000..0fe6c946b0c
--- /dev/null
+++ b/web/ce/store/issue/team/index.ts
@@ -0,0 +1,2 @@
+export * from "./filter.store";
+export * from "./issue.store";
diff --git a/web/ce/store/issue/team/issue.store.ts b/web/ce/store/issue/team/issue.store.ts
new file mode 100644
index 00000000000..2e397943640
--- /dev/null
+++ b/web/ce/store/issue/team/issue.store.ts
@@ -0,0 +1,13 @@
+import { IProjectIssues, ProjectIssues } from "@/store/issue/project";
+import { IIssueRootStore } from "@/store/issue/root.store";
+import { ITeamIssuesFilter } from "./filter.store";
+
+// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
+export type ITeamIssues = IProjectIssues;
+
+// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
+export class TeamIssues extends ProjectIssues implements IProjectIssues {
+ constructor(_rootStore: IIssueRootStore, teamIssueFilterStore: ITeamIssuesFilter) {
+ super(_rootStore, teamIssueFilterStore);
+ }
+}
diff --git a/web/ce/types/dashboard.ts b/web/ce/types/dashboard.ts
index d615ac4afce..de35f60c6a7 100644
--- a/web/ce/types/dashboard.ts
+++ b/web/ce/types/dashboard.ts
@@ -1 +1,3 @@
export type TSidebarUserMenuItemKeys = "home" | "your-work" | "notifications" | "drafts";
+
+export type TSidebarWorkspaceMenuItemKeys = "projects" | "all-issues" | "active-cycles" | "analytics";
diff --git a/web/core/components/cycles/analytics-sidebar/root.tsx b/web/core/components/cycles/analytics-sidebar/root.tsx
index fd8c984a690..b709c0e6317 100644
--- a/web/core/components/cycles/analytics-sidebar/root.tsx
+++ b/web/core/components/cycles/analytics-sidebar/root.tsx
@@ -2,7 +2,6 @@
import React from "react";
import { observer } from "mobx-react";
-import { useParams } from "next/navigation";
// ui
import { Loader } from "@plane/ui";
// components
@@ -13,19 +12,19 @@ import useCyclesDetails from "../active-cycle/use-cycles-details";
type Props = {
handleClose: () => void;
isArchived?: boolean;
- cycleId?: string;
+ cycleId: string;
+ projectId: string;
+ workspaceSlug: string;
};
export const CycleDetailsSidebar: React.FC = observer((props) => {
- const { handleClose, isArchived } = props;
- // router
- const { workspaceSlug, projectId, cycleId } = useParams();
+ const { handleClose, isArchived, projectId, workspaceSlug, cycleId } = props;
// store hooks
const { cycle: cycleDetails } = useCyclesDetails({
- workspaceSlug: workspaceSlug.toString(),
- projectId: projectId.toString(),
- cycleId: cycleId?.toString() || props.cycleId,
+ workspaceSlug,
+ projectId,
+ cycleId,
});
if (!cycleDetails)
@@ -47,21 +46,17 @@ export const CycleDetailsSidebar: React.FC = observer((props) => {
-
+
{workspaceSlug && projectId && cycleDetails?.id && (
-
+
)}
);
diff --git a/web/core/components/cycles/cycle-peek-overview.tsx b/web/core/components/cycles/cycle-peek-overview.tsx
index 759569cfa9f..187425b8d72 100644
--- a/web/core/components/cycles/cycle-peek-overview.tsx
+++ b/web/core/components/cycles/cycle-peek-overview.tsx
@@ -9,12 +9,13 @@ import { useAppRouter } from "@/hooks/use-app-router";
import { CycleDetailsSidebar } from "./";
type Props = {
- projectId: string;
+ projectId?: string;
workspaceSlug: string;
isArchived?: boolean;
};
-export const CyclePeekOverview: React.FC = observer(({ projectId, workspaceSlug, isArchived = false }) => {
+export const CyclePeekOverview: React.FC = observer((props) => {
+ const { projectId: propsProjectId, workspaceSlug, isArchived } = props;
// router
const router = useAppRouter();
const pathname = usePathname();
@@ -23,22 +24,25 @@ export const CyclePeekOverview: React.FC = observer(({ projectId, workspa
// refs
const ref = React.useRef(null);
// store hooks
- const { fetchCycleDetails, fetchArchivedCycleDetails } = useCycle();
+ const { getCycleById, fetchCycleDetails, fetchArchivedCycleDetails } = useCycle();
+ // derived values
+ const cycleDetails = peekCycle ? getCycleById(peekCycle.toString()) : undefined;
+ const projectId = propsProjectId || cycleDetails?.project_id;
const handleClose = () => {
const query = generateQueryParams(searchParams, ["peekCycle"]);
- router.push(`${pathname}?${query}`);
+ router.push(`${pathname}?${query}`, {}, { showProgressBar: false });
};
useEffect(() => {
- if (!peekCycle) return;
+ if (!peekCycle || !projectId) return;
if (isArchived) fetchArchivedCycleDetails(workspaceSlug, projectId, peekCycle.toString());
else fetchCycleDetails(workspaceSlug, projectId, peekCycle.toString());
}, [fetchArchivedCycleDetails, fetchCycleDetails, isArchived, peekCycle, projectId, workspaceSlug]);
return (
<>
- {peekCycle && (
+ {peekCycle && projectId && (
= observer(({ projectId, workspa
"0px 1px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 4px 0px rgba(16, 24, 40, 0.06), 0px 1px 8px -1px rgba(16, 24, 40, 0.06)",
}}
>
-
+
)}
>
diff --git a/web/core/components/cycles/form.tsx b/web/core/components/cycles/form.tsx
index 660d33cdb80..7651c5d4433 100644
--- a/web/core/components/cycles/form.tsx
+++ b/web/core/components/cycles/form.tsx
@@ -75,6 +75,7 @@ export const CycleForm: React.FC = (props) => {
onChange(val);
setActiveProject(val);
}}
+ multiple={false}
buttonVariant="border-with-text"
renderCondition={(project) => shouldRenderProject(project)}
tabIndex={getIndex("cover_image")}
diff --git a/web/core/components/cycles/list/cycle-list-item-action.tsx b/web/core/components/cycles/list/cycle-list-item-action.tsx
index 989e0436e36..73dca345d7c 100644
--- a/web/core/components/cycles/list/cycle-list-item-action.tsx
+++ b/web/core/components/cycles/list/cycle-list-item-action.tsx
@@ -14,10 +14,9 @@ import { CycleQuickActions } from "@/components/cycles";
import { DateRangeDropdown } from "@/components/dropdowns";
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
// constants
-import { CYCLE_STATUS } from "@/constants/cycle";
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "@/constants/event-tracker";
// helpers
-import { findHowManyDaysLeft, getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
+import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { getFileURL } from "@/helpers/file.helper";
// hooks
import { generateQueryParams } from "@/helpers/router.helper";
@@ -69,11 +68,11 @@ export const CycleListItemAction: FC = observer((props) => {
const cycleStatus = cycleDetails.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft";
const isEditingAllowed = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
- EUserPermissionsLevel.PROJECT
+ EUserPermissionsLevel.PROJECT,
+ workspaceSlug,
+ projectId
);
const renderIcon = Boolean(cycleDetails.start_date) || Boolean(cycleDetails.end_date);
- const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
- const daysLeft = findHowManyDaysLeft(cycleDetails.end_date) ?? 0;
// handlers
const handleAddToFavorites = (e: MouseEvent) => {
@@ -201,9 +200,9 @@ export const CycleListItemAction: FC = observer((props) => {
const query = generateQueryParams(searchParams, ["peekCycle"]);
if (searchParams.has("peekCycle") && searchParams.get("peekCycle") === cycleId) {
- router.push(`${pathname}?${query}`);
+ router.push(`${pathname}?${query}`, {}, { showProgressBar: false });
} else {
- router.push(`${pathname}?${query && `${query}&`}peekCycle=${cycleId}`);
+ router.push(`${pathname}?${query && `${query}&`}peekCycle=${cycleId}`, {}, { showProgressBar: false });
}
};
diff --git a/web/core/components/cycles/list/cycle-list-project-group-header.tsx b/web/core/components/cycles/list/cycle-list-project-group-header.tsx
new file mode 100644
index 00000000000..d663eca0d86
--- /dev/null
+++ b/web/core/components/cycles/list/cycle-list-project-group-header.tsx
@@ -0,0 +1,44 @@
+"use client";
+
+import React, { FC } from "react";
+import { observer } from "mobx-react";
+import { ChevronRight } from "lucide-react";
+// icons
+import { Row, Logo } from "@plane/ui";
+// helpers
+import { cn } from "@/helpers/common.helper";
+import { useProject } from "@/hooks/store/use-project";
+
+type Props = {
+ projectId: string;
+ count?: number;
+ showCount?: boolean;
+ isExpanded?: boolean;
+};
+
+export const CycleListProjectGroupHeader: FC = observer((props) => {
+ const { projectId, count, showCount = false, isExpanded = false } = props;
+ // store hooks
+ const { getProjectById } = useProject();
+ // derived values
+ const project = getProjectById(projectId);
+
+ if (!project) return null;
+ return (
+
+
+
+
+
+
+
{project.name}
+ {showCount &&
{`${count ?? "0"}`}
}
+
+
+ );
+});
diff --git a/web/core/components/cycles/list/cycles-list-item.tsx b/web/core/components/cycles/list/cycles-list-item.tsx
index 8d531216a4b..5954a0a7f33 100644
--- a/web/core/components/cycles/list/cycles-list-item.tsx
+++ b/web/core/components/cycles/list/cycles-list-item.tsx
@@ -4,7 +4,7 @@ import { FC, MouseEvent, useRef } from "react";
import { observer } from "mobx-react";
import { usePathname, useSearchParams } from "next/navigation";
// icons
-import { Check, Info } from "lucide-react";
+import { Check } from "lucide-react";
// types
import type { TCycleGroups } from "@plane/types";
// ui
@@ -72,9 +72,9 @@ export const CyclesListItem: FC = observer((props) => {
const query = generateQueryParams(searchParams, ["peekCycle"]);
if (searchParams.has("peekCycle") && searchParams.get("peekCycle") === cycleId) {
- router.push(`${pathname}?${query}`);
+ router.push(`${pathname}?${query}`, {}, { showProgressBar: false });
} else {
- router.push(`${pathname}?${query && `${query}&`}peekCycle=${cycleId}`);
+ router.push(`${pathname}?${query && `${query}&`}peekCycle=${cycleId}`, {}, { showProgressBar: false });
}
};
diff --git a/web/core/components/cycles/list/index.ts b/web/core/components/cycles/list/index.ts
index 4eebc577943..25419a0560a 100644
--- a/web/core/components/cycles/list/index.ts
+++ b/web/core/components/cycles/list/index.ts
@@ -3,3 +3,4 @@ export * from "./cycles-list-map";
export * from "./root";
export * from "./cycle-list-item-action";
export * from "./cycle-list-group-header";
+export * from "./cycle-list-project-group-header";
diff --git a/web/core/components/dropdowns/layout.tsx b/web/core/components/dropdowns/layout.tsx
index 2557e57a2a2..7864d1849df 100644
--- a/web/core/components/dropdowns/layout.tsx
+++ b/web/core/components/dropdowns/layout.tsx
@@ -10,18 +10,24 @@ import { EIssueLayoutTypes, ISSUE_LAYOUT_MAP } from "@/constants/issue";
type TLayoutDropDown = {
onChange: (value: EIssueLayoutTypes) => void;
value: EIssueLayoutTypes;
+ disabledLayouts?: EIssueLayoutTypes[];
};
export const LayoutDropDown = observer((props: TLayoutDropDown) => {
- const { onChange, value = EIssueLayoutTypes.LIST } = props;
+ const { onChange, value = EIssueLayoutTypes.LIST, disabledLayouts = [] } = props;
+ // derived values
+ const availableLayouts = useMemo(
+ () => Object.values(ISSUE_LAYOUT_MAP).filter((layout) => !disabledLayouts.includes(layout.key)),
+ [disabledLayouts]
+ );
const options = useMemo(
() =>
- Object.values(ISSUE_LAYOUT_MAP).map((issueLayout) => ({
+ availableLayouts.map((issueLayout) => ({
data: issueLayout.key,
value: issueLayout.key,
})),
- []
+ [availableLayouts]
);
const buttonContent = useCallback((isOpen: boolean, buttonValue: string | string[] | undefined) => {
diff --git a/web/core/components/dropdowns/project.tsx b/web/core/components/dropdowns/project.tsx
index 3f973cd1618..f94014eb8b8 100644
--- a/web/core/components/dropdowns/project.tsx
+++ b/web/core/components/dropdowns/project.tsx
@@ -1,4 +1,4 @@
-import { Fragment, ReactNode, useRef, useState } from "react";
+import { ReactNode, useRef, useState } from "react";
import { observer } from "mobx-react";
import { usePopper } from "react-popper";
import { Check, ChevronDown, Search } from "lucide-react";
@@ -25,12 +25,21 @@ type Props = TDropdownProps & {
button?: ReactNode;
dropdownArrow?: boolean;
dropdownArrowClassName?: string;
- onChange: (val: string) => void;
onClose?: () => void;
renderCondition?: (project: TProject) => boolean;
- value: string | null;
renderByDefault?: boolean;
-};
+} & (
+ | {
+ multiple: false;
+ onChange: (val: string) => void;
+ value: string | null;
+ }
+ | {
+ multiple: true;
+ onChange: (val: string[]) => void;
+ value: string[];
+ }
+ );
export const ProjectDropdown: React.FC = observer((props) => {
const {
@@ -43,6 +52,7 @@ export const ProjectDropdown: React.FC = observer((props) => {
dropdownArrow = false,
dropdownArrowClassName = "",
hideIcon = false,
+ multiple,
onChange,
onClose,
placeholder = "Project",
@@ -99,8 +109,6 @@ export const ProjectDropdown: React.FC = observer((props) => {
const filteredOptions =
query === "" ? options : options?.filter((o) => o?.query.toLowerCase().includes(query.toLowerCase()));
- const selectedProject = value ? getProjectById(value) : null;
-
const { handleClose, handleKeyDown, handleOnClick, searchInputKeyDown } = useDropdown({
dropdownRef,
inputRef,
@@ -111,9 +119,40 @@ export const ProjectDropdown: React.FC = observer((props) => {
setQuery,
});
- const dropdownOnChange = (val: string) => {
+ const dropdownOnChange = (val: string & string[]) => {
onChange(val);
- handleClose();
+ if (!multiple) handleClose();
+ };
+
+ const getDisplayName = (value: string | string[] | null, placeholder: string = "") => {
+ if (Array.isArray(value)) {
+ const firstProject = getProjectById(value[0]);
+ return value.length ? (value.length === 1 ? firstProject?.name : `${value.length} projects`) : placeholder;
+ } else {
+ return value ? (getProjectById(value)?.name ?? placeholder) : placeholder;
+ }
+ };
+
+ const getProjectIcon = (value: string | string[] | null) => {
+ const renderIcon = (projectDetails: TProject) => (
+
+
+
+ );
+
+ if (Array.isArray(value)) {
+ return (
+
+ {value.map((projectId) => {
+ const projectDetails = getProjectById(projectId);
+ return projectDetails ? renderIcon(projectDetails) : null;
+ })}
+
+ );
+ } else {
+ const projectDetails = getProjectById(value);
+ return projectDetails ? renderIcon(projectDetails) : null;
+ }
};
const comboButton = (
@@ -147,18 +186,14 @@ export const ProjectDropdown: React.FC = observer((props) => {
className={buttonClassName}
isActive={isOpen}
tooltipHeading="Project"
- tooltipContent={selectedProject?.name ?? placeholder}
+ tooltipContent={value?.length ? `${value.length} project${value.length !== 1 ? "s" : ""}` : placeholder}
showTooltip={showTooltip}
variant={buttonVariant}
renderToolTipByDefault={renderByDefault}
>
- {!hideIcon && selectedProject && (
-
-
-
- )}
+ {!hideIcon && getProjectIcon(value)}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
- {selectedProject?.name ?? placeholder}
+ {getDisplayName(value, placeholder)}
)}
{dropdownArrow && (
@@ -181,6 +216,7 @@ export const ProjectDropdown: React.FC = observer((props) => {
onKeyDown={handleKeyDown}
button={comboButton}
renderByDefault={renderByDefault}
+ multiple={multiple}
>
{isOpen && (
diff --git a/web/core/components/editor/rich-text-editor/rich-text-editor.tsx b/web/core/components/editor/rich-text-editor/rich-text-editor.tsx
index 908ac502256..23750b397d9 100644
--- a/web/core/components/editor/rich-text-editor/rich-text-editor.tsx
+++ b/web/core/components/editor/rich-text-editor/rich-text-editor.tsx
@@ -16,28 +16,25 @@ interface RichTextEditorWrapperProps
extends Omit {
workspaceSlug: string;
workspaceId: string;
- projectId: string;
+ memberIds: string[];
+ projectId?: string;
uploadFile: (file: File) => Promise;
}
export const RichTextEditor = forwardRef((props, ref) => {
- const { containerClassName, workspaceSlug, workspaceId, projectId, uploadFile, ...rest } = props;
+ const { containerClassName, workspaceSlug, workspaceId, projectId, memberIds, uploadFile, ...rest } = props;
// store hooks
const { data: currentUser } = useUser();
- const {
- getUserDetails,
- project: { getProjectMemberIds },
- } = useMember();
+ const { getUserDetails } = useMember();
// editor flaggings
const { richTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
// derived values
- const projectMemberIds = getProjectMemberIds(projectId);
- const projectMemberDetails = projectMemberIds?.map((id) => getUserDetails(id) as IUserLite);
+ const memberDetails = memberIds?.map((id) => getUserDetails(id) as IUserLite);
// use-mention
const { mentionHighlights, mentionSuggestions } = useMention({
workspaceSlug,
projectId,
- members: projectMemberDetails,
+ members: memberDetails,
user: currentUser ?? undefined,
});
// file size
diff --git a/web/core/components/inbox/content/inbox-issue-header.tsx b/web/core/components/inbox/content/inbox-issue-header.tsx
index ad7d63ff0f4..cb0dbcfc5f1 100644
--- a/web/core/components/inbox/content/inbox-issue-header.tsx
+++ b/web/core/components/inbox/content/inbox-issue-header.tsx
@@ -15,6 +15,7 @@ import {
MoveRight,
Copy,
} from "lucide-react";
+import { TNameDescriptionLoader } from "@plane/types";
import { Button, ControlLink, CustomMenu, Row, TOAST_TYPE, setToast } from "@plane/ui";
// components
import {
@@ -25,7 +26,7 @@ import {
InboxIssueStatus,
SelectDuplicateInboxIssueModal,
} from "@/components/inbox";
-import { CreateUpdateIssueModal, IssueUpdateStatus } from "@/components/issues";
+import { CreateUpdateIssueModal, NameDescriptionUpdateStatus } from "@/components/issues";
// helpers
import { findHowManyDaysLeft } from "@/helpers/date-time.helper";
import { EInboxIssueStatus } from "@/helpers/inbox.helper";
@@ -41,7 +42,7 @@ type TInboxIssueActionsHeader = {
workspaceSlug: string;
projectId: string;
inboxIssue: IInboxIssueStore | undefined;
- isSubmitting: "submitting" | "submitted" | "saved";
+ isSubmitting: TNameDescriptionLoader;
isMobileSidebar: boolean;
setIsMobileSidebar: (value: boolean) => void;
isNotificationEmbed: boolean;
@@ -282,7 +283,7 @@ export const InboxIssueActionsHeader: FC = observer((p
)}
-
+
diff --git a/web/core/components/inbox/content/inbox-issue-mobile-header.tsx b/web/core/components/inbox/content/inbox-issue-mobile-header.tsx
index 7a66d0976f8..987a663a5ea 100644
--- a/web/core/components/inbox/content/inbox-issue-mobile-header.tsx
+++ b/web/core/components/inbox/content/inbox-issue-mobile-header.tsx
@@ -15,10 +15,11 @@ import {
PanelLeft,
MoveRight,
} from "lucide-react";
+import { TNameDescriptionLoader } from "@plane/types";
import { Header, CustomMenu, EHeaderVariant } from "@plane/ui";
// components
import { InboxIssueStatus } from "@/components/inbox";
-import { IssueUpdateStatus } from "@/components/issues";
+import { NameDescriptionUpdateStatus } from "@/components/issues";
// helpers
import { cn } from "@/helpers/common.helper";
import { findHowManyDaysLeft } from "@/helpers/date-time.helper";
@@ -30,7 +31,7 @@ import type { IInboxIssueStore } from "@/store/inbox/inbox-issue.store";
type Props = {
workspaceSlug: string;
inboxIssue: IInboxIssueStore | undefined;
- isSubmitting: "submitting" | "submitted" | "saved";
+ isSubmitting: TNameDescriptionLoader;
handleInboxIssueNavigation: (direction: "next" | "prev") => void;
canMarkAsAccepted: boolean;
canMarkAsDeclined: boolean;
@@ -117,7 +118,7 @@ export const InboxIssueActionsMobileHeader: React.FC = observer((props) =
diff --git a/web/core/components/inbox/content/issue-root.tsx b/web/core/components/inbox/content/issue-root.tsx
index 2673245b0f1..b154bd2056a 100644
--- a/web/core/components/inbox/content/issue-root.tsx
+++ b/web/core/components/inbox/content/issue-root.tsx
@@ -4,7 +4,7 @@ import { Dispatch, SetStateAction, useEffect, useMemo } from "react";
import { observer } from "mobx-react";
import { usePathname } from "next/navigation";
// plane types
-import { TIssue } from "@plane/types";
+import { TIssue, TNameDescriptionLoader } from "@plane/types";
// plane ui
import { Loader, TOAST_TYPE, setToast } from "@plane/ui";
// components
@@ -34,8 +34,8 @@ type Props = {
projectId: string;
inboxIssue: IInboxIssueStore;
isEditable: boolean;
- isSubmitting: "submitting" | "submitted" | "saved";
- setIsSubmitting: Dispatch
>;
+ isSubmitting: TNameDescriptionLoader;
+ setIsSubmitting: Dispatch>;
};
export const InboxIssueMainContent: React.FC = observer((props) => {
diff --git a/web/core/components/inbox/content/root.tsx b/web/core/components/inbox/content/root.tsx
index 64d1257e91a..e8d86a91f15 100644
--- a/web/core/components/inbox/content/root.tsx
+++ b/web/core/components/inbox/content/root.tsx
@@ -1,6 +1,7 @@
import { FC, useEffect, useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
+import { TNameDescriptionLoader } from "@plane/types";
// components
import { ContentWrapper } from "@plane/ui";
import { InboxIssueActionsHeader, InboxIssueMainContent } from "@/components/inbox";
@@ -32,7 +33,7 @@ export const InboxContentRoot: FC = observer((props) => {
/// router
const router = useAppRouter();
// states
- const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
+ const [isSubmitting, setIsSubmitting] = useState("saved");
// hooks
const { data: currentUser } = useUser();
const { currentTab, fetchInboxIssueById, getIssueInboxByIssueId, getIsIssueAvailable } = useProjectInbox();
diff --git a/web/core/components/inbox/modals/create-modal/issue-description.tsx b/web/core/components/inbox/modals/create-modal/issue-description.tsx
index b9bad6c11ac..3835c006d6f 100644
--- a/web/core/components/inbox/modals/create-modal/issue-description.tsx
+++ b/web/core/components/inbox/modals/create-modal/issue-description.tsx
@@ -17,7 +17,7 @@ import { ETabIndices } from "@/constants/tab-indices";
import { getDescriptionPlaceholder } from "@/helpers/issue.helper";
import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks
-import { useProjectInbox } from "@/hooks/store";
+import { useMember, useProjectInbox } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// services
import { FileService } from "@/services/file.service";
@@ -51,6 +51,11 @@ export const InboxIssueDescription: FC = observer((props
// hooks
const { loader } = useProjectInbox();
const { isMobile } = usePlatformOS();
+ const {
+ project: { getProjectMemberIds },
+ } = useMember();
+ // derived values
+ const memberIds = getProjectMemberIds(projectId) ?? [];
const { getIndex } = getTabIndex(ETabIndices.INTAKE_ISSUE_FORM, isMobile);
@@ -68,6 +73,7 @@ export const InboxIssueDescription: FC = observer((props
ref={editorRef}
workspaceSlug={workspaceSlug}
workspaceId={workspaceId}
+ memberIds={memberIds}
projectId={projectId}
dragDropEnabled={false}
onChange={(_description: object, description_html: string) => handleData("description_html", description_html)}
diff --git a/web/core/components/issues/description-input.tsx b/web/core/components/issues/description-input.tsx
index 8c18618c506..fd629f9ed0d 100644
--- a/web/core/components/issues/description-input.tsx
+++ b/web/core/components/issues/description-input.tsx
@@ -5,7 +5,7 @@ import debounce from "lodash/debounce";
import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form";
// types
-import { TIssue } from "@plane/types";
+import { TIssue, TNameDescriptionLoader } from "@plane/types";
import { EFileAssetType } from "@plane/types/src/enums";
// ui
import { Loader } from "@plane/ui";
@@ -15,7 +15,7 @@ import { TIssueOperations } from "@/components/issues/issue-detail";
// helpers
import { getDescriptionPlaceholder } from "@/helpers/issue.helper";
// hooks
-import { useWorkspace } from "@/hooks/store";
+import { useMember, useWorkspace } from "@/hooks/store";
// services
import { FileService } from "@/services/file.service";
const fileService = new FileService();
@@ -29,7 +29,7 @@ export type IssueDescriptionInputProps = {
disabled?: boolean;
issueOperations: TIssueOperations;
placeholder?: string | ((isFocused: boolean, value: string) => string);
- setIsSubmitting: (initialValue: "submitting" | "submitted" | "saved") => void;
+ setIsSubmitting: (initialValue: TNameDescriptionLoader) => void;
swrIssueDescription?: string | null | undefined;
};
@@ -46,6 +46,12 @@ export const IssueDescriptionInput: FC = observer((p
setIsSubmitting,
placeholder,
} = props;
+ // store hooks
+ const {
+ project: { getProjectMemberIds },
+ } = useMember();
+ // derived values
+ const memberIds = getProjectMemberIds(projectId) ?? [];
const { handleSubmit, reset, control } = useForm({
defaultValues: {
@@ -108,6 +114,7 @@ export const IssueDescriptionInput: FC = observer((p
value={swrIssueDescription ?? null}
workspaceSlug={workspaceSlug}
workspaceId={workspaceId}
+ memberIds={memberIds}
projectId={projectId}
dragDropEnabled
onChange={(_description: object, description_html: string) => {
diff --git a/web/core/components/issues/issue-detail/main-content.tsx b/web/core/components/issues/issue-detail/main-content.tsx
index fb4dbc1fce9..08d5c06630c 100644
--- a/web/core/components/issues/issue-detail/main-content.tsx
+++ b/web/core/components/issues/issue-detail/main-content.tsx
@@ -2,10 +2,11 @@
import { useEffect, useState } from "react";
import { observer } from "mobx-react";
+import { TNameDescriptionLoader } from "@plane/types";
// components
import {
IssueActivity,
- IssueUpdateStatus,
+ NameDescriptionUpdateStatus,
IssueReaction,
IssueParentDetail,
IssueTitleInput,
@@ -38,7 +39,7 @@ type Props = {
export const IssueMainContent: React.FC = observer((props) => {
const { workspaceSlug, projectId, issueId, issueOperations, isEditable, isArchived } = props;
// states
- const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
+ const [isSubmitting, setIsSubmitting] = useState("saved");
// hooks
const windowSize = useSize();
const { data: currentUser } = useUser();
@@ -87,7 +88,7 @@ export const IssueMainContent: React.FC = observer((props) => {
-
+
{duplicateIssues?.length > 0 && (
getLabelsColumns(isWorkspaceLevel),
assignees: getAssigneeColumns,
created_by: getCreatedByColumns,
+ team_project: getTeamProjectColumns,
};
// Get and return the columns for the specified group by option
@@ -703,4 +707,4 @@ export const getBlockViewDetails = (
message,
blockStyle,
};
-};
+};
\ No newline at end of file
diff --git a/web/core/components/issues/issue-modal/components/description-editor.tsx b/web/core/components/issues/issue-modal/components/description-editor.tsx
index 316668d0107..bb7fa8d32ac 100644
--- a/web/core/components/issues/issue-modal/components/description-editor.tsx
+++ b/web/core/components/issues/issue-modal/components/description-editor.tsx
@@ -20,7 +20,7 @@ import { ETabIndices } from "@/constants/tab-indices";
import { getDescriptionPlaceholder } from "@/helpers/issue.helper";
import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks
-import { useInstance, useWorkspace } from "@/hooks/store";
+import { useInstance, useMember, useWorkspace } from "@/hooks/store";
import useKeypress from "@/hooks/use-keypress";
import { usePlatformOS } from "@/hooks/use-platform-os";
// services
@@ -76,6 +76,11 @@ export const IssueDescriptionEditor: React.FC = ob
const workspaceId = getWorkspaceBySlug(workspaceSlug?.toString())?.id as string;
const { config } = useInstance();
const { isMobile } = usePlatformOS();
+ const {
+ project: { getProjectMemberIds },
+ } = useMember();
+ // derived values
+ const memberIds = projectId ? (getProjectMemberIds(projectId) ?? []) : [];
const { getIndex } = getTabIndex(ETabIndices.ISSUE_FORM, isMobile);
@@ -179,6 +184,7 @@ export const IssueDescriptionEditor: React.FC = ob
value={descriptionHtmlData}
workspaceSlug={workspaceSlug?.toString() as string}
workspaceId={workspaceId}
+ memberIds={memberIds}
projectId={projectId}
onChange={(_description: object, description_html: string) => {
onChange(description_html);
diff --git a/web/core/components/issues/issue-modal/components/project-select.tsx b/web/core/components/issues/issue-modal/components/project-select.tsx
index d2e4ca74576..b63b32b769a 100644
--- a/web/core/components/issues/issue-modal/components/project-select.tsx
+++ b/web/core/components/issues/issue-modal/components/project-select.tsx
@@ -46,6 +46,7 @@ export const IssueProjectSelect: React.FC = observer((
onChange(projectId);
handleFormChange();
}}
+ multiple={false}
buttonVariant="border-with-text"
renderCondition={(project) => shouldRenderProject(project)}
tabIndex={getIndex("project_id")}
diff --git a/web/core/components/issues/issue-update-status.tsx b/web/core/components/issues/issue-update-status.tsx
index 6eb064b529d..37c4bf19a2e 100644
--- a/web/core/components/issues/issue-update-status.tsx
+++ b/web/core/components/issues/issue-update-status.tsx
@@ -1,12 +1,14 @@
import React from "react";
import { observer } from "mobx-react";
import { RefreshCw } from "lucide-react";
+// types
+import { TNameDescriptionLoader } from "@plane/types";
type Props = {
- isSubmitting: "submitting" | "submitted" | "saved";
+ isSubmitting: TNameDescriptionLoader;
};
-export const IssueUpdateStatus: React.FC = observer((props) => {
+export const NameDescriptionUpdateStatus: React.FC = observer((props) => {
const { isSubmitting } = props;
return (
@@ -17,7 +19,7 @@ export const IssueUpdateStatus: React.FC = observer((props) => {
}`}
>
{isSubmitting !== "submitted" && isSubmitting !== "saved" && (
-
+
)}
{isSubmitting === "submitting" ? "Saving..." : "Saved"}
diff --git a/web/core/components/issues/peek-overview/header.tsx b/web/core/components/issues/peek-overview/header.tsx
index 9967ad067d0..8484d9bc3da 100644
--- a/web/core/components/issues/peek-overview/header.tsx
+++ b/web/core/components/issues/peek-overview/header.tsx
@@ -4,6 +4,8 @@ import { FC } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { ArchiveRestoreIcon, Link2, MoveDiagonal, MoveRight, Trash2 } from "lucide-react";
+// types
+import { TNameDescriptionLoader } from "@plane/types";
// ui
import {
ArchiveIcon,
@@ -16,7 +18,7 @@ import {
setToast,
} from "@plane/ui";
// components
-import { IssueSubscription, IssueUpdateStatus } from "@/components/issues";
+import { IssueSubscription, NameDescriptionUpdateStatus } from "@/components/issues";
import { ARCHIVABLE_STATE_GROUPS } from "@/constants/state";
// helpers
import { cn } from "@/helpers/common.helper";
@@ -58,7 +60,7 @@ export type PeekOverviewHeaderProps = {
toggleDeleteIssueModal: (issueId: string | null) => void;
toggleArchiveIssueModal: (issueId: string | null) => void;
handleRestoreIssue: () => void;
- isSubmitting: "submitting" | "submitted" | "saved";
+ isSubmitting: TNameDescriptionLoader;
};
export const IssuePeekOverviewHeader: FC
= observer((props) => {
@@ -157,7 +159,7 @@ export const IssuePeekOverviewHeader: FC = observer((pr
)}
-
+
{currentUser && !isArchived && (
diff --git a/web/core/components/modules/form.tsx b/web/core/components/modules/form.tsx
index 0acf587033a..a13d7fbac62 100644
--- a/web/core/components/modules/form.tsx
+++ b/web/core/components/modules/form.tsx
@@ -86,6 +86,7 @@ export const ModuleForm: React.FC
= (props) => {
onChange(val);
setActiveProject(val);
}}
+ multiple={false}
buttonVariant="border-with-text"
renderCondition={(project) => shouldRenderProject(project)}
tabIndex={getIndex("cover_image")}
diff --git a/web/core/components/project/card-list.tsx b/web/core/components/project/card-list.tsx
index 2578be7276b..ff98c188c55 100644
--- a/web/core/components/project/card-list.tsx
+++ b/web/core/components/project/card-list.tsx
@@ -13,12 +13,26 @@ import { useCommandPalette, useEventTracker, useProject, useProjectFilter } from
import AllFiltersImage from "@/public/empty-state/project/all-filters.svg";
import NameFilterImage from "@/public/empty-state/project/name-filter.svg";
-export const ProjectCardList = observer(() => {
+type TProjectCardListProps = {
+ totalProjectIds?: string[];
+ filteredProjectIds?: string[];
+};
+
+export const ProjectCardList = observer((props: TProjectCardListProps) => {
+ const { totalProjectIds: totalProjectIdsProps, filteredProjectIds: filteredProjectIdsProps } = props;
// store hooks
const { toggleCreateProjectModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
- const { workspaceProjectIds, filteredProjectIds, getProjectById, loader } = useProject();
+ const {
+ workspaceProjectIds: storeWorkspaceProjectIds,
+ filteredProjectIds: storeFilteredProjectIds,
+ getProjectById,
+ loader,
+ } = useProject();
const { searchQuery, currentWorkspaceDisplayFilters } = useProjectFilter();
+ // derived values
+ const workspaceProjectIds = totalProjectIdsProps ?? storeWorkspaceProjectIds;
+ const filteredProjectIds = filteredProjectIdsProps ?? storeFilteredProjectIds;
if (!filteredProjectIds || !workspaceProjectIds || loader) return ;
diff --git a/web/core/components/project/header.tsx b/web/core/components/project/header.tsx
index edb6d58157d..b79f68d68fc 100644
--- a/web/core/components/project/header.tsx
+++ b/web/core/components/project/header.tsx
@@ -1,40 +1,27 @@
"use client";
-import { useEffect, useRef, useState } from "react";
import { observer } from "mobx-react";
import { usePathname } from "next/navigation";
-import { Search, Briefcase, X } from "lucide-react";
-// plane helpers
-import { useOutsideClickDetector } from "@plane/hooks";
+import { Briefcase } from "lucide-react";
// ui
import { Breadcrumbs, Button, Header } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
-// helpers
-import { cn } from "@/helpers/common.helper";
// hooks
-import { useCommandPalette, useEventTracker, useProjectFilter, useUserPermissions } from "@/hooks/store";
+import { useCommandPalette, useEventTracker, useUserPermissions } from "@/hooks/store";
+// plane web constants
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
+// components
import HeaderFilters from "./filters";
+import { ProjectSearch } from "./search-projects";
export const ProjectsBaseHeader = observer(() => {
- // states
- const [isSearchOpen, setIsSearchOpen] = useState(false);
- // refs
- const inputRef = useRef(null);
// store hooks
const { toggleCreateProjectModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
const pathname = usePathname();
-
- const { searchQuery, updateSearchQuery } = useProjectFilter();
-
- // outside click detector hook
- useOutsideClickDetector(inputRef, () => {
- if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
- });
// auth
const isAuthorizedUser = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
@@ -42,17 +29,6 @@ export const ProjectsBaseHeader = observer(() => {
);
const isArchived = pathname.includes("/archives");
- const handleInputKeyDown = (e: React.KeyboardEvent) => {
- if (e.key === "Escape") {
- if (searchQuery && searchQuery.trim() !== "") updateSearchQuery("");
- else setIsSearchOpen(false);
- }
- };
-
- useEffect(() => {
- if (searchQuery.trim() !== "") setIsSearchOpen(true);
- }, [searchQuery]);
-
return (
@@ -65,51 +41,7 @@ export const ProjectsBaseHeader = observer(() => {
-
- {!isSearchOpen && (
-
- )}
-
-
- updateSearchQuery(e.target.value)}
- onKeyDown={handleInputKeyDown}
- />
- {isSearchOpen && (
-
- )}
-
-
-
+
diff --git a/web/core/components/project/index.ts b/web/core/components/project/index.ts
index 5f47350a8f7..83f4b814559 100644
--- a/web/core/components/project/index.ts
+++ b/web/core/components/project/index.ts
@@ -18,4 +18,6 @@ export * from "./member-list-item";
export * from "./project-settings-member-defaults";
export * from "./send-project-invitation-modal";
export * from "./confirm-project-member-remove";
+export * from "./multi-select-modal";
+export * from "./search-projects";
export * from "@/plane-web/components/projects/create/root";
diff --git a/web/core/components/project/multi-select-modal.tsx b/web/core/components/project/multi-select-modal.tsx
new file mode 100644
index 00000000000..e866f2595e6
--- /dev/null
+++ b/web/core/components/project/multi-select-modal.tsx
@@ -0,0 +1,175 @@
+import React, { useEffect, useMemo, useRef, useState } from "react";
+import xor from "lodash/xor";
+import { observer } from "mobx-react";
+import { Search, X } from "lucide-react";
+import { Combobox } from "@headlessui/react";
+// plane ui
+import { Button, Checkbox, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
+// components
+import { Logo } from "@/components/common";
+import { EmptyState } from "@/components/empty-state";
+// constants
+import { EmptyStateType } from "@/constants/empty-state";
+// helpers
+import { cn } from "@/helpers/common.helper";
+// hooks
+import { useProject } from "@/hooks/store";
+
+type Props = {
+ isOpen: boolean;
+ onClose: () => void;
+ selectedProjectIds: string[];
+ projectIds: string[];
+ onSubmit: (projectIds: string[]) => Promise;
+};
+
+export const ProjectMultiSelectModal: React.FC = observer((props) => {
+ const { isOpen, onClose, selectedProjectIds: selectedProjectIdsProp, projectIds, onSubmit } = props;
+ // states
+ const [searchTerm, setSearchTerm] = useState("");
+ const [selectedProjectIds, setSelectedProjectIds] = useState([]);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ // refs
+ const moveButtonRef = useRef(null);
+ // store hooks
+ const { getProjectById } = useProject();
+ // derived values
+ const projectDetailsMap = useMemo(
+ () => new Map(projectIds.map((id) => [id, getProjectById(id)])),
+ [projectIds, getProjectById]
+ );
+ const areSelectedProjectsChanged = xor(selectedProjectIds, selectedProjectIdsProp).length > 0;
+ const filteredProjectIds = projectIds.filter((id) => {
+ const project = projectDetailsMap.get(id);
+ const projectQuery = `${project?.identifier} ${project?.name}`.toLowerCase();
+ return projectQuery.includes(searchTerm.toLowerCase());
+ });
+
+ useEffect(() => {
+ if (isOpen) setSelectedProjectIds(selectedProjectIdsProp);
+ }, [isOpen, selectedProjectIdsProp]);
+
+ const handleClose = () => {
+ onClose();
+ setTimeout(() => {
+ setSearchTerm("");
+ setSelectedProjectIds([]);
+ }, 300);
+ };
+
+ const handleSubmit = async () => {
+ setIsSubmitting(true);
+ await onSubmit(selectedProjectIds);
+ setIsSubmitting(false);
+ handleClose();
+ };
+
+ const handleSelectedProjectChange = (val: string[]) => {
+ setSelectedProjectIds(val);
+ setSearchTerm("");
+ moveButtonRef.current?.focus();
+ };
+
+ if (!isOpen) return null;
+
+ return (
+
+
+
+
+ ""}
+ value={searchTerm}
+ onChange={(e) => setSearchTerm(e.target.value)}
+ />
+
+ {selectedProjectIds.length > 0 && (
+
+ {selectedProjectIds.map((projectId) => {
+ const projectDetails = projectDetailsMap.get(projectId);
+ if (!projectDetails) return null;
+ return (
+
{
+ handleSelectedProjectChange(selectedProjectIds.filter((id) => id !== projectDetails.id));
+ }}
+ >
+
+
+ {projectDetails.identifier}
+
+
+
+ );
+ })}
+
+ )}
+
+ {filteredProjectIds.length === 0 ? (
+
+
+
+ ) : (
+ 0,
+ })}
+ >
+ {filteredProjectIds.map((projectId) => {
+ const projectDetails = projectDetailsMap.get(projectId);
+ if (!projectDetails) return null;
+ const isProjectSelected = selectedProjectIds.includes(projectDetails.id);
+ return (
+
+ cn(
+ "flex items-center justify-between gap-2 truncate w-full cursor-pointer select-none rounded-md p-2 text-custom-text-200 transition-colors",
+ {
+ "bg-custom-background-80": active,
+ "text-custom-text-100": isProjectSelected,
+ }
+ )
+ }
+ >
+
+
+
+
+
+
{projectDetails.identifier}
+
{projectDetails.name}
+
+
+ );
+ })}
+
+ )}
+
+
+
+
+
+
+
+ );
+});
diff --git a/web/core/components/project/search-projects.tsx b/web/core/components/project/search-projects.tsx
new file mode 100644
index 00000000000..0434f83470b
--- /dev/null
+++ b/web/core/components/project/search-projects.tsx
@@ -0,0 +1,78 @@
+"use client";
+
+import { FC, useRef, useState } from "react";
+import { observer } from "mobx-react";
+import { Search, X } from "lucide-react";
+// plane hooks
+import { useOutsideClickDetector } from "@plane/hooks";
+// helpers
+import { cn } from "@/helpers/common.helper";
+// hooks
+import { useProjectFilter } from "@/hooks/store";
+
+export const ProjectSearch: FC = observer(() => {
+ // hooks
+ const { searchQuery, updateSearchQuery } = useProjectFilter();
+ // refs
+ const inputRef = useRef(null);
+ // states
+ const [isSearchOpen, setIsSearchOpen] = useState(false);
+ // outside click detector hook
+ useOutsideClickDetector(inputRef, () => {
+ if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
+ });
+
+ const handleInputKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Escape") {
+ if (searchQuery && searchQuery.trim() !== "") updateSearchQuery("");
+ else setIsSearchOpen(false);
+ }
+ };
+
+ return (
+
+ {!isSearchOpen && (
+
+ )}
+
+
+ updateSearchQuery(e.target.value)}
+ onKeyDown={handleInputKeyDown}
+ />
+ {isSearchOpen && (
+
+ )}
+
+
+ );
+});
diff --git a/web/core/components/workspace/sidebar/workspace-menu.tsx b/web/core/components/workspace/sidebar/workspace-menu.tsx
index cf786f40592..42cb9cced07 100644
--- a/web/core/components/workspace/sidebar/workspace-menu.tsx
+++ b/web/core/components/workspace/sidebar/workspace-menu.tsx
@@ -13,7 +13,6 @@ import { CustomMenu, Tooltip } from "@plane/ui";
// components
import { SidebarNavItem } from "@/components/sidebar";
// constants
-import { SIDEBAR_WORKSPACE_MENU_ITEMS } from "@/constants/dashboard";
import { SIDEBAR_CLICKED } from "@/constants/event-tracker";
// helpers
import { cn } from "@/helpers/common.helper";
@@ -23,22 +22,26 @@ import useLocalStorage from "@/hooks/use-local-storage";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web components
import { UpgradeBadge } from "@/plane-web/components/workspace";
+// plane web constants
+import { SIDEBAR_WORKSPACE_MENU_ITEMS } from "@/plane-web/constants/dashboard";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
+// plane web hooks
+import { isWorkspaceFeatureEnabled } from "@/plane-web/helpers/dashboard.helper";
export const SidebarWorkspaceMenu = observer(() => {
// state
const [isMenuActive, setIsMenuActive] = useState(false);
// refs
const actionSectionRef = useRef(null);
+ // router params
+ const { workspaceSlug } = useParams();
+ // pathname
+ const pathname = usePathname();
// store hooks
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
const { captureEvent } = useEventTracker();
const { isMobile } = usePlatformOS();
const { allowPermissions } = useUserPermissions();
- // router params
- const { workspaceSlug } = useParams();
- // pathname
- const pathname = usePathname();
// local storage
const { setValue: toggleWorkspaceMenu, storedValue } = useLocalStorage("is_workspace_menu_open", true);
// derived values
@@ -158,8 +161,9 @@ export const SidebarWorkspaceMenu = observer(() => {
})}
static
>
- {SIDEBAR_WORKSPACE_MENU_ITEMS.map(
- (link) =>
+ {SIDEBAR_WORKSPACE_MENU_ITEMS.map((link) => {
+ if (!isWorkspaceFeatureEnabled(link.key, workspaceSlug.toString())) return null;
+ return (
allowPermissions(link.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) && (
{
)
- )}
+ );
+ })}
)}
diff --git a/web/core/constants/dashboard.ts b/web/core/constants/dashboard.ts
index 8d9848a486f..c70664eee47 100644
--- a/web/core/constants/dashboard.ts
+++ b/web/core/constants/dashboard.ts
@@ -1,14 +1,8 @@
"use client";
import { linearGradientDef } from "@nivo/core";
-// icons
-import { BarChart2, Briefcase, Layers } from "lucide-react";
// types
import { TIssuesListTypes, TStateGroups } from "@plane/types";
-// ui
-import { ContrastIcon } from "@plane/ui";
-import { Props } from "@/components/icons/types";
-import { EUserPermissions } from "@/plane-web/constants/user-permissions";
// assets
import CompletedIssuesDark from "@/public/empty-state/dashboard/dark/completed-issues.svg";
import OverdueIssuesDark from "@/public/empty-state/dashboard/dark/overdue-issues.svg";
@@ -250,48 +244,6 @@ export const CREATED_ISSUES_EMPTY_STATES = {
},
};
-export const SIDEBAR_WORKSPACE_MENU_ITEMS: {
- key: string;
- label: string;
- href: string;
- access: EUserPermissions[];
- highlight: (pathname: string, baseUrl: string) => boolean;
- Icon: React.FC;
-}[] = [
- {
- key: "projects",
- label: "Projects",
- href: `/projects`,
- access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
- highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/projects/`,
- Icon: Briefcase,
- },
- {
- key: "all-issues",
- label: "Views",
- href: `/workspace-views/all-issues`,
- access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
- highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/workspace-views/`),
- Icon: Layers,
- },
- {
- key: "active-cycles",
- label: "Cycles",
- href: `/active-cycles`,
- access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
- highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/active-cycles/`,
- Icon: ContrastIcon,
- },
- {
- key: "analytics",
- label: "Analytics",
- href: `/analytics`,
- access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
- highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/analytics/`),
- Icon: BarChart2,
- },
-];
-
export type TLinkOptions = {
userId: string | undefined;
};
diff --git a/web/core/constants/issue.ts b/web/core/constants/issue.ts
index 8629c05a253..3780567b35d 100644
--- a/web/core/constants/issue.ts
+++ b/web/core/constants/issue.ts
@@ -10,6 +10,7 @@ import {
TIssuePriorities,
TIssueGroupingFilters,
} from "@plane/types";
+import { ADDITIONAL_ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/plane-web/constants";
export const DRAG_ALLOWED_GROUPS: TIssueGroupByOptions[] = [
"state",
@@ -23,9 +24,11 @@ export const DRAG_ALLOWED_GROUPS: TIssueGroupByOptions[] = [
export enum EIssuesStoreType {
GLOBAL = "GLOBAL",
PROFILE = "PROFILE",
+ TEAM = "TEAM",
PROJECT = "PROJECT",
CYCLE = "CYCLE",
MODULE = "MODULE",
+ TEAM_VIEW = "TEAM_VIEW",
PROJECT_VIEW = "PROJECT_VIEW",
ARCHIVED = "ARCHIVED",
DRAFT = "DRAFT",
@@ -42,7 +45,9 @@ export enum EIssueLayoutTypes {
}
export type TCreateModalStoreTypes =
+ | EIssuesStoreType.TEAM
| EIssuesStoreType.PROJECT
+ | EIssuesStoreType.TEAM_VIEW
| EIssuesStoreType.PROJECT_VIEW
| EIssuesStoreType.PROFILE
| EIssuesStoreType.CYCLE
@@ -78,6 +83,7 @@ export const ISSUE_GROUP_BY_OPTIONS: {
{ key: "state", title: "States" },
{ key: "state_detail.group", title: "State Groups" },
{ key: "priority", title: "Priority" },
+ { key: "team_project", title: "Team Project" }, // required this on team issues
{ key: "project", title: "Project" }, // required this on my issues
{ key: "cycle", title: "Cycle" }, // required this on my issues
{ key: "module", title: "Module" }, // required this on my issues
@@ -463,6 +469,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
},
},
},
+ ...ADDITIONAL_ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
};
export enum EIssueListRow {
diff --git a/web/core/hooks/store/use-issues.ts b/web/core/hooks/store/use-issues.ts
index ca40dce9ddf..8c9bc905af4 100644
--- a/web/core/hooks/store/use-issues.ts
+++ b/web/core/hooks/store/use-issues.ts
@@ -1,10 +1,12 @@
import { useContext } from "react";
import merge from "lodash/merge";
-// mobx store
import { TIssueMap } from "@plane/types";
+// mobx store
import { EIssuesStoreType } from "@/constants/issue";
import { StoreContext } from "@/lib/store-context";
// types
+import { ITeamIssues, ITeamIssuesFilter } from "@/plane-web/store/issue/team";
+import { ITeamViewIssues, ITeamViewIssuesFilter } from "@/plane-web/store/issue/team-views";
import { IArchivedIssues, IArchivedIssuesFilter } from "@/store/issue/archived";
import { ICycleIssues, ICycleIssuesFilter } from "@/store/issue/cycle";
import { IDraftIssues, IDraftIssuesFilter } from "@/store/issue/draft";
@@ -33,6 +35,10 @@ export type TStoreIssues = {
issues: IProfileIssues;
issuesFilter: IProfileIssuesFilter;
};
+ [EIssuesStoreType.TEAM]: defaultIssueStore & {
+ issues: ITeamIssues;
+ issuesFilter: ITeamIssuesFilter;
+ };
[EIssuesStoreType.PROJECT]: defaultIssueStore & {
issues: IProjectIssues;
issuesFilter: IProjectIssuesFilter;
@@ -45,6 +51,10 @@ export type TStoreIssues = {
issues: IModuleIssues;
issuesFilter: IModuleIssuesFilter;
};
+ [EIssuesStoreType.TEAM_VIEW]: defaultIssueStore & {
+ issues: ITeamViewIssues;
+ issuesFilter: ITeamViewIssuesFilter;
+ };
[EIssuesStoreType.PROJECT_VIEW]: defaultIssueStore & {
issues: IProjectViewIssues;
issuesFilter: IProjectViewIssuesFilter;
@@ -82,16 +92,16 @@ export const useIssues = (storeType?: T): TStoreIssu
issues: context.issue.workspaceDraftIssues,
issuesFilter: context.issue.workspaceDraftIssuesFilter,
}) as TStoreIssues[T];
- case EIssuesStoreType.WORKSPACE_DRAFT:
- return merge(defaultStore, {
- issues: context.issue.workspaceDraftIssues,
- issuesFilter: context.issue.workspaceDraftIssuesFilter,
- }) as TStoreIssues[T];
case EIssuesStoreType.PROFILE:
return merge(defaultStore, {
issues: context.issue.profileIssues,
issuesFilter: context.issue.profileIssuesFilter,
}) as TStoreIssues[T];
+ case EIssuesStoreType.TEAM:
+ return merge(defaultStore, {
+ issues: context.issue.teamIssues,
+ issuesFilter: context.issue.teamIssuesFilter,
+ }) as TStoreIssues[T];
case EIssuesStoreType.PROJECT:
return merge(defaultStore, {
issues: context.issue.projectIssues,
@@ -107,6 +117,11 @@ export const useIssues = (storeType?: T): TStoreIssu
issues: context.issue.moduleIssues,
issuesFilter: context.issue.moduleIssuesFilter,
}) as TStoreIssues[T];
+ case EIssuesStoreType.TEAM_VIEW:
+ return merge(defaultStore, {
+ issues: context.issue.teamViewIssues,
+ issuesFilter: context.issue.teamViewIssuesFilter,
+ }) as TStoreIssues[T];
case EIssuesStoreType.PROJECT_VIEW:
return merge(defaultStore, {
issues: context.issue.projectViewIssues,
diff --git a/web/core/hooks/use-group-dragndrop.ts b/web/core/hooks/use-group-dragndrop.ts
index 08695d341d2..bef3bd1f115 100644
--- a/web/core/hooks/use-group-dragndrop.ts
+++ b/web/core/hooks/use-group-dragndrop.ts
@@ -17,7 +17,9 @@ type DNDStoreType =
| EIssuesStoreType.DRAFT
| EIssuesStoreType.PROFILE
| EIssuesStoreType.ARCHIVED
- | EIssuesStoreType.WORKSPACE_DRAFT;
+ | EIssuesStoreType.WORKSPACE_DRAFT
+ | EIssuesStoreType.TEAM
+ | EIssuesStoreType.TEAM_VIEW;
export const useGroupIssuesDragNDrop = (
storeType: DNDStoreType,
diff --git a/web/core/hooks/use-issue-layout-store.ts b/web/core/hooks/use-issue-layout-store.ts
index 17187d88377..122e97fe53c 100644
--- a/web/core/hooks/use-issue-layout-store.ts
+++ b/web/core/hooks/use-issue-layout-store.ts
@@ -8,7 +8,7 @@ export const IssuesStoreContext = createContext(un
export const useIssueStoreType = () => {
const storeType = useContext(IssuesStoreContext);
- const { globalViewId, viewId, projectId, cycleId, moduleId, userId } = useParams();
+ const { globalViewId, viewId, projectId, cycleId, moduleId, userId, teamId } = useParams();
// If store type exists in context, use that store type
if (storeType) return storeType;
@@ -26,6 +26,10 @@ export const useIssueStoreType = () => {
if (projectId) return EIssuesStoreType.PROJECT;
+ if (teamId) return EIssuesStoreType.TEAM;
+
+ if (teamId && viewId) return EIssuesStoreType.TEAM_VIEW;
+
return EIssuesStoreType.PROJECT;
};
diff --git a/web/core/hooks/use-issues-actions.tsx b/web/core/hooks/use-issues-actions.tsx
index 8fb2ed228ab..a0778a47d19 100644
--- a/web/core/hooks/use-issues-actions.tsx
+++ b/web/core/hooks/use-issues-actions.tsx
@@ -14,9 +14,10 @@ import {
} from "@plane/types";
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
import { EDraftIssuePaginationType } from "@/constants/workspace-drafts";
+import { useTeamIssueActions, useTeamViewIssueActions } from "@/plane-web/helpers/issue-action-helper";
import { useIssues } from "./store";
-interface IssueActions {
+export interface IssueActions {
fetchIssues: (
loadType: TLoader,
options: IssuePaginationOptions,
@@ -38,9 +39,11 @@ interface IssueActions {
}
export const useIssuesActions = (storeType: EIssuesStoreType): IssueActions => {
+ const teamIssueActions = useTeamIssueActions();
const projectIssueActions = useProjectIssueActions();
const cycleIssueActions = useCycleIssueActions();
const moduleIssueActions = useModuleIssueActions();
+ const teamViewIssueActions = useTeamViewIssueActions();
const projectViewIssueActions = useProjectViewIssueActions();
const globalIssueActions = useGlobalIssueActions();
const profileIssueActions = useProfileIssueActions();
@@ -49,10 +52,14 @@ export const useIssuesActions = (storeType: EIssuesStoreType): IssueActions => {
const workspaceDraftIssueActions = useWorkspaceDraftIssueActions();
switch (storeType) {
+ case EIssuesStoreType.TEAM_VIEW:
+ return teamViewIssueActions;
case EIssuesStoreType.PROJECT_VIEW:
return projectViewIssueActions;
case EIssuesStoreType.PROFILE:
return profileIssueActions;
+ case EIssuesStoreType.TEAM:
+ return teamIssueActions;
case EIssuesStoreType.ARCHIVED:
return archivedIssueActions;
case EIssuesStoreType.DRAFT:
diff --git a/web/core/hooks/use-local-storage.tsx b/web/core/hooks/use-local-storage.tsx
index 538b8a93b77..6ba44662a9f 100644
--- a/web/core/hooks/use-local-storage.tsx
+++ b/web/core/hooks/use-local-storage.tsx
@@ -21,6 +21,7 @@ export const setValueIntoLocalStorage = (key: string, value: any) => {
}
};
+// TODO: Remove this once we migrate to the new hooks from plane/helpers
const useLocalStorage = (key: string, initialValue: T) => {
const [storedValue, setStoredValue] = useState(() => getValueFromLocalStorage(key, initialValue));
diff --git a/web/core/store/base-command-palette.store.ts b/web/core/store/base-command-palette.store.ts
index 9b0ff52b572..436129dd0d4 100644
--- a/web/core/store/base-command-palette.store.ts
+++ b/web/core/store/base-command-palette.store.ts
@@ -1,4 +1,4 @@
-import { observable, action, makeObservable, computed } from "mobx";
+import { observable, action, makeObservable } from "mobx";
// services
import { EIssuesStoreType, TCreateModalStoreTypes } from "@/constants/issue";
// types / constants
@@ -22,8 +22,6 @@ export interface IBaseCommandPaletteStore {
isDeleteIssueModalOpen: boolean;
isBulkDeleteIssueModalOpen: boolean;
createIssueStoreType: TCreateModalStoreTypes;
- // computed
- isAnyModalOpen: boolean;
// toggle actions
toggleCommandPaletteModal: (value?: boolean) => void;
toggleShortcutModal: (value?: boolean) => void;
@@ -65,8 +63,6 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor
isBulkDeleteIssueModalOpen: observable.ref,
createPageModal: observable,
createIssueStoreType: observable,
- // computed
- isAnyModalOpen: computed,
// projectPages: computed,
// toggle actions
toggleCommandPaletteModal: action,
@@ -83,10 +79,10 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor
}
/**
- * Checks whether any modal is open or not in the base command palette.
- * @returns boolean
+ * Returns whether any base modal is open
+ * @protected - allows access from child classes
*/
- get isAnyModalOpen() {
+ protected getCoreModalsState(): boolean {
return Boolean(
this.isCreateIssueModalOpen ||
this.isCreateCycleModalOpen ||
diff --git a/web/core/store/issue/helpers/base-issues.store.ts b/web/core/store/issue/helpers/base-issues.store.ts
index d5545db4425..17e264d4240 100644
--- a/web/core/store/issue/helpers/base-issues.store.ts
+++ b/web/core/store/issue/helpers/base-issues.store.ts
@@ -129,6 +129,7 @@ const ISSUE_GROUP_BY_KEY: Record = {
target_date: "target_date",
cycle: "cycle_id",
module: "module_ids",
+ team_project: "project_id",
};
export const ISSUE_FILTER_DEFAULT_DATA: Record = {
@@ -142,6 +143,7 @@ export const ISSUE_FILTER_DEFAULT_DATA: Record | undefined;
cycleMap: Record | undefined;
- rootStore: CoreRootStore;
+ rootStore: RootStore;
issues: IIssueStore;
@@ -64,6 +73,9 @@ export interface IIssueRootStore {
profileIssuesFilter: IProfileIssuesFilter;
profileIssues: IProfileIssues;
+ teamIssuesFilter: ITeamIssuesFilter;
+ teamIssues: ITeamIssues;
+
projectIssuesFilter: IProjectIssuesFilter;
projectIssues: IProjectIssues;
@@ -73,6 +85,9 @@ export interface IIssueRootStore {
moduleIssuesFilter: IModuleIssuesFilter;
moduleIssues: IModuleIssues;
+ teamViewIssuesFilter: ITeamViewIssuesFilter;
+ teamViewIssues: ITeamViewIssues;
+
projectViewIssuesFilter: IProjectViewIssuesFilter;
projectViewIssues: IProjectViewIssues;
@@ -89,6 +104,7 @@ export interface IIssueRootStore {
export class IssueRootStore implements IIssueRootStore {
currentUserId: string | undefined = undefined;
workspaceSlug: string | undefined = undefined;
+ teamId: string | undefined = undefined;
projectId: string | undefined = undefined;
cycleId: string | undefined = undefined;
moduleId: string | undefined = undefined;
@@ -105,7 +121,7 @@ export class IssueRootStore implements IIssueRootStore {
moduleMap: Record | undefined = undefined;
cycleMap: Record | undefined = undefined;
- rootStore: CoreRootStore;
+ rootStore: RootStore;
issues: IIssueStore;
@@ -120,6 +136,9 @@ export class IssueRootStore implements IIssueRootStore {
profileIssuesFilter: IProfileIssuesFilter;
profileIssues: IProfileIssues;
+ teamIssuesFilter: ITeamIssuesFilter;
+ teamIssues: ITeamIssues;
+
projectIssuesFilter: IProjectIssuesFilter;
projectIssues: IProjectIssues;
@@ -129,6 +148,9 @@ export class IssueRootStore implements IIssueRootStore {
moduleIssuesFilter: IModuleIssuesFilter;
moduleIssues: IModuleIssues;
+ teamViewIssuesFilter: ITeamViewIssuesFilter;
+ teamViewIssues: ITeamViewIssues;
+
projectViewIssuesFilter: IProjectViewIssuesFilter;
projectViewIssues: IProjectViewIssues;
@@ -141,9 +163,10 @@ export class IssueRootStore implements IIssueRootStore {
issueKanBanView: IIssueKanBanViewStore;
issueCalendarView: ICalendarStore;
- constructor(rootStore: CoreRootStore) {
+ constructor(rootStore: RootStore) {
makeObservable(this, {
workspaceSlug: observable.ref,
+ teamId: observable.ref,
projectId: observable.ref,
cycleId: observable.ref,
moduleId: observable.ref,
@@ -166,6 +189,7 @@ export class IssueRootStore implements IIssueRootStore {
autorun(() => {
if (rootStore?.user?.data?.id) this.currentUserId = rootStore?.user?.data?.id;
if (this.workspaceSlug !== rootStore.router.workspaceSlug) this.workspaceSlug = rootStore.router.workspaceSlug;
+ if (this.teamId !== rootStore.router.teamId) this.teamId = rootStore.router.teamId;
if (this.projectId !== rootStore.router.projectId) this.projectId = rootStore.router.projectId;
if (this.cycleId !== rootStore.router.cycleId) this.cycleId = rootStore.router.cycleId;
if (this.moduleId !== rootStore.router.moduleId) this.moduleId = rootStore.router.moduleId;
@@ -201,12 +225,18 @@ export class IssueRootStore implements IIssueRootStore {
this.projectIssuesFilter = new ProjectIssuesFilter(this);
this.projectIssues = new ProjectIssues(this, this.projectIssuesFilter);
+ this.teamIssuesFilter = new TeamIssuesFilter(this);
+ this.teamIssues = new TeamIssues(this, this.teamIssuesFilter);
+
this.cycleIssuesFilter = new CycleIssuesFilter(this);
this.cycleIssues = new CycleIssues(this, this.cycleIssuesFilter);
this.moduleIssuesFilter = new ModuleIssuesFilter(this);
this.moduleIssues = new ModuleIssues(this, this.moduleIssuesFilter);
+ this.teamViewIssuesFilter = new TeamViewIssuesFilter(this);
+ this.teamViewIssues = new TeamViewIssues(this, this.teamViewIssuesFilter);
+
this.projectViewIssuesFilter = new ProjectViewIssuesFilter(this);
this.projectViewIssues = new ProjectViewIssues(this, this.projectViewIssuesFilter);
diff --git a/web/core/store/pages/page.ts b/web/core/store/pages/page.ts
index edf136455b2..d609cab6498 100644
--- a/web/core/store/pages/page.ts
+++ b/web/core/store/pages/page.ts
@@ -1,7 +1,7 @@
import set from "lodash/set";
import { action, computed, makeObservable, observable, reaction, runInAction } from "mobx";
// types
-import { TDocumentPayload, TLogoProps, TPage } from "@plane/types";
+import { TDocumentPayload, TLogoProps, TNameDescriptionLoader, TPage } from "@plane/types";
// constants
import { EPageAccess } from "@/constants/page";
import { EUserPermissions } from "@/plane-web/constants/user-permissions";
@@ -10,11 +10,9 @@ import { ProjectPageService } from "@/services/page";
// store
import { CoreRootStore } from "../root.store";
-export type TLoader = "submitting" | "submitted" | "saved";
-
export interface IPage extends TPage {
// observables
- isSubmitting: TLoader;
+ isSubmitting: TNameDescriptionLoader;
// computed
asJSON: TPage | undefined;
isCurrentUserOwner: boolean; // it will give the user is the owner of the page or not
@@ -28,7 +26,7 @@ export interface IPage extends TPage {
isContentEditable: boolean;
// helpers
oldName: string;
- setIsSubmitting: (value: TLoader) => void;
+ setIsSubmitting: (value: TNameDescriptionLoader) => void;
cleanup: () => void;
// actions
update: (pageData: Partial) => Promise;
@@ -47,7 +45,7 @@ export interface IPage extends TPage {
export class Page implements IPage {
// loaders
- isSubmitting: TLoader = "saved";
+ isSubmitting: TNameDescriptionLoader = "saved";
// page properties
id: string | undefined;
name: string | undefined;
@@ -324,7 +322,7 @@ export class Page implements IPage {
* @description update the submitting state
* @param value
*/
- setIsSubmitting = (value: TLoader) => {
+ setIsSubmitting = (value: TNameDescriptionLoader) => {
runInAction(() => {
this.isSubmitting = value;
});
diff --git a/web/core/store/router.store.ts b/web/core/store/router.store.ts
index ae5013836c7..051ed49b4a0 100644
--- a/web/core/store/router.store.ts
+++ b/web/core/store/router.store.ts
@@ -9,6 +9,7 @@ export interface IRouterStore {
setQuery: (query: ParsedUrlQuery) => void;
// computed
workspaceSlug: string | undefined;
+ teamId: string | undefined;
projectId: string | undefined;
cycleId: string | undefined;
moduleId: string | undefined;
@@ -34,6 +35,7 @@ export class RouterStore implements IRouterStore {
setQuery: action.bound,
//computed
workspaceSlug: computed,
+ teamId: computed,
projectId: computed,
cycleId: computed,
moduleId: computed,
@@ -66,6 +68,14 @@ export class RouterStore implements IRouterStore {
return this.query?.workspaceSlug?.toString();
}
+ /**
+ * Returns the team id from the query
+ * @returns string|undefined
+ */
+ get teamId() {
+ return this.query?.teamId?.toString();
+ }
+
/**
* Returns the project id from the query
* @returns string|undefined
diff --git a/web/ee/components/issues/filters/index.ts b/web/ee/components/issues/filters/index.ts
index 2cd80e3a7e5..f0f36b6c97e 100644
--- a/web/ee/components/issues/filters/index.ts
+++ b/web/ee/components/issues/filters/index.ts
@@ -1,2 +1,3 @@
export * from "./applied-filters";
export * from "./issue-types";
+export * from "./team-project";
diff --git a/web/ee/components/issues/filters/team-project.tsx b/web/ee/components/issues/filters/team-project.tsx
new file mode 100644
index 00000000000..0f5683af114
--- /dev/null
+++ b/web/ee/components/issues/filters/team-project.tsx
@@ -0,0 +1 @@
+export * from "ce/components/issues/filters/team-project";
diff --git a/web/ee/components/issues/issue-layouts/utils.tsx b/web/ee/components/issues/issue-layouts/utils.tsx
new file mode 100644
index 00000000000..1716a8950ec
--- /dev/null
+++ b/web/ee/components/issues/issue-layouts/utils.tsx
@@ -0,0 +1 @@
+export * from "ce/components/issues/issue-layouts/utils";
diff --git a/web/ee/components/workspace/sidebar/teams-sidebar-list.tsx b/web/ee/components/workspace/sidebar/teams-sidebar-list.tsx
new file mode 100644
index 00000000000..2cd5f20dc17
--- /dev/null
+++ b/web/ee/components/workspace/sidebar/teams-sidebar-list.tsx
@@ -0,0 +1 @@
+export * from "ce/components/workspace/sidebar/teams-sidebar-list";
diff --git a/web/ee/constants/dashboard.ts b/web/ee/constants/dashboard.ts
new file mode 100644
index 00000000000..8612fbe013f
--- /dev/null
+++ b/web/ee/constants/dashboard.ts
@@ -0,0 +1 @@
+export * from "ce/constants/dashboard";
diff --git a/web/ee/helpers/issue-action-helper.ts b/web/ee/helpers/issue-action-helper.ts
new file mode 100644
index 00000000000..1e1ed08f01c
--- /dev/null
+++ b/web/ee/helpers/issue-action-helper.ts
@@ -0,0 +1 @@
+export * from "ce/helpers/issue-action-helper";
diff --git a/web/ee/store/issue/team-views/index.ts b/web/ee/store/issue/team-views/index.ts
new file mode 100644
index 00000000000..c02c38b4fa1
--- /dev/null
+++ b/web/ee/store/issue/team-views/index.ts
@@ -0,0 +1 @@
+export * from "ce/store/issue/team-views";
diff --git a/web/ee/store/issue/team/index.ts b/web/ee/store/issue/team/index.ts
new file mode 100644
index 00000000000..2e02634a967
--- /dev/null
+++ b/web/ee/store/issue/team/index.ts
@@ -0,0 +1 @@
+export * from "ce/store/issue/team";
diff --git a/web/helpers/issue.helper.ts b/web/helpers/issue.helper.ts
index 695d124d049..171c469b0d7 100644
--- a/web/helpers/issue.helper.ts
+++ b/web/helpers/issue.helper.ts
@@ -97,7 +97,7 @@ export const handleIssuesMutation: THandleIssuesMutation = (
export const handleIssueQueryParamsByLayout = (
layout: EIssueLayoutTypes | undefined,
- viewType: "my_issues" | "issues" | "profile_issues" | "archived_issues" | "draft_issues"
+ viewType: "my_issues" | "issues" | "profile_issues" | "archived_issues" | "draft_issues" | "team_issues"
): TIssueParams[] | null => {
const queryParams: TIssueParams[] = [];