refactor: enhance components modularity and introduce new UI componenets (#6192)

* feat: add navigation dropdown component

* chore: enhance title/ description loader and componenet modularity

* chore: issue store filter update

* chore: added few icons to ui package

* chore: improvements for tabs componenet

* chore: enhance sidebar modularity

* chore: update issue and router store to add support for additional issue layouts

* chore: enhanced cycle componenets modularity

* feat: added project grouping header for cycles list

* chore: enhanced project dropdown componenet by adding multiple selection functionality

* chore: enhanced rich text editor modularity by taking members ids as props for mentions

* chore: added functionality to filter disabled layouts in issue-layout dropdown

* chore: added support to pass project ids as props in project card list

* feat: multi select project modal

* chore: seperate out project componenet for reusability

* chore: command pallete store improvements

* fix: build errors
This commit is contained in:
Prateek Shourya 2024-12-12 21:40:57 +05:30 committed by GitHub
parent 9ad8b43408
commit 54f828cbfa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
82 changed files with 1044 additions and 320 deletions

View file

@ -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 {

View file

@ -22,3 +22,5 @@ export type TLogoProps = {
background_color?: string;
};
};
export type TNameDescriptionLoader = "submitting" | "submitted" | "saved";

View file

@ -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",
}

View file

@ -211,7 +211,8 @@ export type GroupByColumnTypes =
| "priority"
| "labels"
| "assignees"
| "created_by";
| "created_by"
| "team_project";
export interface IGroupByColumn {
id: string;

View file

@ -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;

View file

@ -1 +1,2 @@
export * from "./breadcrumbs";
export * from "./navigation-dropdown";

View file

@ -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 ? (
<selectedItem.icon className={cn("size-3.5", selectedItem.iconClassName)} />
) : undefined;
// if no selected item, return null
if (!selectedItem) return null;
const NavigationButton = ({ className }: { className?: string }) => (
<li
className={cn(
"flex items-center justify-center cursor-default text-sm font-medium text-custom-text-200 group-hover:text-custom-text-100 outline-none",
className
)}
tabIndex={-1}
>
{selectedItemIcon && (
<div className="flex h-5 w-5 items-center justify-start overflow-hidden">{selectedItemIcon}</div>
)}
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{selectedItem.title}</div>
</li>
);
if (navigationDisabled) {
return <NavigationButton />;
}
return (
<CustomMenu
customButton={
<div className="group flex items-center gap-1.5">
<NavigationButton className="cursor-pointer" />
<ChevronDownIcon className="size-4 text-custom-text-200 group-hover:text-custom-text-100" />
</div>
}
placement="bottom-start"
closeOnSelect
>
{navigationItems.map((item) => {
if (item.shouldRender === false) return null;
return (
<CustomMenu.MenuItem
key={item.key}
onClick={(e) => {
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.icon className={cn("size-3.5", item.iconClassName)} />}
<div className="w-full">
<h5>{item.title}</h5>
{item.description && (
<p
className={cn("text-custom-text-300 whitespace-pre-line", {
"text-custom-text-400": item.disabled,
})}
>
{item.description}
</p>
)}
</div>
{item.key === selectedItemKey && <CheckIcon className="flex-shrink-0 size-3.5" />}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
);
};

View file

@ -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";

View file

@ -0,0 +1,26 @@
import * as React from "react";
import { ISvgIcons } from "./type";
export const LeadIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
<svg className={className} viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg" {...rest}>
<path
d="M0.571533 9C0.571533 4.02944 4.60097 0 9.57153 0C14.5421 0 18.5715 4.02944 18.5715 9C18.5715 13.9706 14.5421 18 9.57153 18C4.60097 18 0.571533 13.9706 0.571533 9Z"
fill="#3372FF"
/>
<g clip-path="url(#clip0_8992_2377)">
<circle cx="9.57153" cy="6.5" r="2.5" fill="#F5F5FF" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8.94653 9.625C6.53029 9.625 4.57153 11.5838 4.57153 14H9.57153H14.5715C14.5715 11.5838 12.6128 9.625 10.1965 9.625H9.82153L10.8215 13.0278L9.57153 14L8.32153 13.0278L9.32153 9.625H8.94653Z"
fill="#F5F5FF"
/>
</g>
<defs>
<clipPath id="clip0_8992_2377">
<rect width="10" height="10" fill="white" transform="translate(4.57153 4)" />
</clipPath>
</defs>
</svg>
);

View file

@ -0,0 +1,19 @@
import * as React from "react";
import { ISvgIcons } from "./type";
export const TeamsIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.25 6.75C8.25 5.75544 8.64509 4.80161 9.34835 4.09835C10.0516 3.39509 11.0054 3 12 3C12.9946 3 13.9484 3.39509 14.6517 4.09835C15.3549 4.80161 15.75 5.75544 15.75 6.75C15.75 7.74456 15.3549 8.69839 14.6517 9.40165C13.9484 10.1049 12.9946 10.5 12 10.5C11.0054 10.5 10.0516 10.1049 9.34835 9.40165C8.64509 8.69839 8.25 7.74456 8.25 6.75ZM15.75 9.75C15.75 8.95435 16.0661 8.19129 16.6287 7.62868C17.1913 7.06607 17.9544 6.75 18.75 6.75C19.5456 6.75 20.3087 7.06607 20.8713 7.62868C21.4339 8.19129 21.75 8.95435 21.75 9.75C21.75 10.5456 21.4339 11.3087 20.8713 11.8713C20.3087 12.4339 19.5456 12.75 18.75 12.75C17.9544 12.75 17.1913 12.4339 16.6287 11.8713C16.0661 11.3087 15.75 10.5456 15.75 9.75ZM2.25 9.75C2.25 8.95435 2.56607 8.19129 3.12868 7.62868C3.69129 7.06607 4.45435 6.75 5.25 6.75C6.04565 6.75 6.80871 7.06607 7.37132 7.62868C7.93393 8.19129 8.25 8.95435 8.25 9.75C8.25 10.5456 7.93393 11.3087 7.37132 11.8713C6.80871 12.4339 6.04565 12.75 5.25 12.75C4.45435 12.75 3.69129 12.4339 3.12868 11.8713C2.56607 11.3087 2.25 10.5456 2.25 9.75ZM6.31 15.117C6.91995 14.161 7.76108 13.3743 8.75562 12.8294C9.75016 12.2846 10.866 11.9994 12 12C12.9498 11.9991 13.8891 12.1989 14.7564 12.5862C15.6237 12.9734 16.3994 13.5395 17.0327 14.2474C17.6661 14.9552 18.1428 15.7888 18.4317 16.6936C18.7205 17.5985 18.815 18.5541 18.709 19.498C18.696 19.6153 18.6556 19.7278 18.591 19.8265C18.5263 19.9252 18.4393 20.0073 18.337 20.066C16.4086 21.1725 14.2233 21.7532 12 21.75C9.695 21.75 7.53 21.138 5.663 20.066C5.56069 20.0073 5.47368 19.9252 5.40904 19.8265C5.34441 19.7278 5.30396 19.6153 5.291 19.498C5.12305 17.9646 5.48246 16.4198 6.31 15.118V15.117Z"
fill="currentColor"
{...rest}
/>
<path
d="M5.08208 14.2539C4.09584 15.7763 3.63633 17.5802 3.77408 19.3889C3.17359 19.2979 2.58299 19.1505 2.01008 18.9489L1.89508 18.9089C1.79248 18.8725 1.70263 18.8071 1.63643 18.7207C1.57023 18.6342 1.53051 18.5305 1.52208 18.4219L1.51208 18.3009C1.47169 17.7989 1.53284 17.2938 1.69188 16.816C1.85093 16.3381 2.10462 15.8971 2.4378 15.5194C2.77099 15.1417 3.17685 14.835 3.63116 14.6176C4.08547 14.4001 4.57893 14.2765 5.08208 14.2539ZM20.2261 19.3889C20.3638 17.5802 19.9043 15.7763 18.9181 14.2539C19.4212 14.2765 19.9147 14.4001 20.369 14.6176C20.8233 14.835 21.2292 15.1417 21.5624 15.5194C21.8955 15.8971 22.1492 16.3381 22.3083 16.816C22.4673 17.2938 22.5285 17.7989 22.4881 18.3009L22.4781 18.4219C22.4695 18.5303 22.4297 18.6338 22.3635 18.7201C22.2973 18.8063 22.2075 18.8716 22.1051 18.9079L21.9901 18.9479C21.4231 19.1479 20.8341 19.2969 20.2261 19.3889Z"
fill="currentColor"
/>
</svg>
);

View file

@ -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<TTabsProps> = (props: TTabsProps) => {
@ -36,15 +39,28 @@ export const Tabs: FC<TTabsProps> = (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 (
<div className="flex flex-col w-full h-full">
<Tab.Group defaultIndex={currentTabIndex(storedValue ?? defaultTab)}>
<Tab.Group defaultIndex={currentTabIndex(selectedTab)}>
<div className={cn("flex flex-col w-full h-full gap-2", containerClassName)}>
<div className={cn("flex w-full items-center gap-4", tabListContainerClassName)}>
<Tab.List
@ -64,12 +80,18 @@ export const Tabs: FC<TTabsProps> = (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}
>

View file

@ -26,7 +26,7 @@ import { CoreRootStore } from "../root.store";
// constants
// helpers
export type TIssueDisplayFilterOptions = Exclude<TIssueGroupByOptions, null> | "target_date";
export type TIssueDisplayFilterOptions = Exclude<TIssueGroupByOptions, null | "team_project"> | "target_date";
export enum EIssueGroupedAction {
ADD = "ADD",

View file

@ -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)",
}}
>
<CycleDetailsSidebar handleClose={toggleSidebar} />
<CycleDetailsSidebar
handleClose={toggleSidebar}
cycleId={cycleId.toString()}
projectId={projectId.toString()}
workspaceSlug={workspaceSlug.toString()}
/>
</div>
)}
</div>

View file

@ -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 */}
<SidebarDropdown />
<div className="flex-shrink-0 h-4" />
<SidebarAppSwitcher />
{/* App switcher */}
{canPerformWorkspaceMemberActions && <SidebarAppSwitcher />}
{/* Quick actions */}
<SidebarQuickActions />
</div>
<hr
@ -88,18 +92,23 @@ export const AppSidebar: FC = observer(() => {
"vertical-scrollbar px-4": !sidebarCollapsed,
})}
>
{/* User Menu */}
<SidebarUserMenu />
{/* Workspace Menu */}
<SidebarWorkspaceMenu />
<hr
className={cn("flex-shrink-0 border-custom-sidebar-border-300 h-[0.5px] w-3/5 mx-auto my-1", {
"opacity-0": !sidebarCollapsed,
})}
/>
{/* Favorites Menu */}
{canPerformWorkspaceMemberActions && !isFavoriteEmpty && <SidebarFavoritesMenu />}
{/* Teams List */}
<SidebarTeamsList />
{/* Projects List */}
<SidebarProjectsList />
</div>
{/* Help Section */}
<SidebarHelpSection />
</div>
</div>

View file

@ -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<IActiveCycleDetails> = 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 ? (
<EmptyState type={EmptyStateType.PROJECT_CYCLE_ACTIVE} size="sm" />
) : (
<div className="flex flex-col border-b border-custom-border-200">
{cycleId && (
<CyclesListItem
key={cycleId}
cycleId={cycleId}
workspaceSlug={workspaceSlug}
projectId={projectId}
className="!border-b-transparent"
/>
)}
<Row className="bg-custom-background-100 pt-3 pb-6">
<div className="grid grid-cols-1 bg-custom-background-100 gap-3 lg:grid-cols-2 xl:grid-cols-3">
<ActiveCycleProgress
handleFiltersUpdate={handleFiltersUpdate}
projectId={projectId}
workspaceSlug={workspaceSlug}
cycle={activeCycle}
/>
<ActiveCycleProductivity workspaceSlug={workspaceSlug} projectId={projectId} cycle={activeCycle} />
<ActiveCycleStats
workspaceSlug={workspaceSlug}
projectId={projectId}
cycle={activeCycle}
cycleId={cycleId}
handleFiltersUpdate={handleFiltersUpdate}
cycleIssueDetails={cycleIssueDetails as ActiveCycleIssueDetails}
/>
</div>
</Row>
</div>
)}
</>
),
[cycleId, activeCycle, workspaceSlug, projectId, handleFiltersUpdate, cycleIssueDetails]
);
return (
<>
<Disclosure as="div" className="flex flex-shrink-0 flex-col" defaultOpen>
{({ open }) => (
<>
<Disclosure.Button className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 cursor-pointer">
<CycleListGroupHeader title="Active cycle" type="current" isExpanded={open} />
</Disclosure.Button>
<Disclosure.Panel>
{!currentProjectActiveCycle ? (
<EmptyState type={EmptyStateType.PROJECT_CYCLE_ACTIVE} size="sm" />
) : (
<div className="flex flex-col border-b border-custom-border-200">
{currentProjectActiveCycleId && (
<CyclesListItem
key={currentProjectActiveCycleId}
cycleId={currentProjectActiveCycleId}
workspaceSlug={workspaceSlug}
projectId={projectId}
className="!border-b-transparent"
/>
)}
<Row className="bg-custom-background-100 pt-3 pb-6">
<div className="grid grid-cols-1 bg-custom-background-100 gap-3 lg:grid-cols-2 xl:grid-cols-3">
<ActiveCycleProgress
handleFiltersUpdate={handleFiltersUpdate}
projectId={projectId}
workspaceSlug={workspaceSlug}
cycle={activeCycle}
/>
<ActiveCycleProductivity
workspaceSlug={workspaceSlug}
projectId={projectId}
cycle={activeCycle}
/>
<ActiveCycleStats
workspaceSlug={workspaceSlug}
projectId={projectId}
cycle={activeCycle}
cycleId={currentProjectActiveCycleId}
handleFiltersUpdate={handleFiltersUpdate}
cycleIssueDetails={cycleIssueDetails as ActiveCycleIssueDetails}
/>
</div>
</Row>
</div>
)}
</Disclosure.Panel>
</>
)}
</Disclosure>
{showHeader ? (
<Disclosure as="div" className="flex flex-shrink-0 flex-col" defaultOpen>
{({ open }) => (
<>
<Disclosure.Button className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 cursor-pointer">
<CycleListGroupHeader title="Active cycle" type="current" isExpanded={open} />
</Disclosure.Button>
<Disclosure.Panel>{ActiveCyclesComponent}</Disclosure.Panel>
</>
)}
</Disclosure>
) : (
<>{ActiveCyclesComponent}</>
)}
</>
);
});

View file

@ -1,2 +1,3 @@
export * from "./applied-filters";
export * from "./issue-types";
export * from "./team-project";

View file

@ -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<Props> = observer(() => null);

View file

@ -0,0 +1,4 @@
// types
import { IGroupByColumn } from "@plane/types";
export const getTeamProjectColumns = (): IGroupByColumn[] | undefined => undefined;

View file

@ -0,0 +1 @@
export const SidebarTeamsList = () => null;

View file

@ -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<T extends TSidebarUserMenuItemKeys | TSidebarWorkspaceMenuItemKeys> = {
key: T;
label: string;
href: string;
access: EUserPermissions[];
@ -19,6 +21,8 @@ export type TSidebarUserMenuItems = {
Icon: React.FC<Props>;
};
export type TSidebarUserMenuItems = TSidebarMenuItems<TSidebarUserMenuItemKeys>;
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<TSidebarWorkspaceMenuItemKeys>;
export const SIDEBAR_WORKSPACE_MENU: Partial<Record<TSidebarWorkspaceMenuItemKeys, TSidebarWorkspaceMenuItems>> = {
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);

View file

@ -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<EActivityFilterType, { label:
export const defaultActivityFilters: TActivityFilters[] = [EActivityFilterType.ACTIVITY, EActivityFilterType.COMMENT];
export type TActivityFilterOption = {
key: EActivityFilterType;
key: TActivityFilters;
label: string;
isSelected: boolean;
onClick: () => 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 };
} = {};

View file

@ -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;

View file

@ -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),
});

View file

@ -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());
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,2 @@
export * from "./filter.store";
export * from "./issue.store";

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,2 @@
export * from "./filter.store";
export * from "./issue.store";

View file

@ -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);
}
}

View file

@ -1 +1,3 @@
export type TSidebarUserMenuItemKeys = "home" | "your-work" | "notifications" | "drafts";
export type TSidebarWorkspaceMenuItemKeys = "projects" | "all-issues" | "active-cycles" | "analytics";

View file

@ -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<Props> = 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<Props> = observer((props) => {
<div className="relative pb-2">
<div className="flex flex-col gap-5 w-full">
<CycleSidebarHeader
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
workspaceSlug={workspaceSlug}
projectId={projectId}
cycleDetails={cycleDetails}
isArchived={isArchived}
handleClose={handleClose}
/>
<CycleSidebarDetails projectId={projectId.toString()} cycleDetails={cycleDetails} />
<CycleSidebarDetails projectId={projectId} cycleDetails={cycleDetails} />
</div>
{workspaceSlug && projectId && cycleDetails?.id && (
<CycleAnalyticsProgress
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
cycleId={cycleDetails?.id}
/>
<CycleAnalyticsProgress workspaceSlug={workspaceSlug} projectId={projectId} cycleId={cycleDetails?.id} />
)}
</div>
);

View file

@ -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<Props> = observer(({ projectId, workspaceSlug, isArchived = false }) => {
export const CyclePeekOverview: React.FC<Props> = observer((props) => {
const { projectId: propsProjectId, workspaceSlug, isArchived } = props;
// router
const router = useAppRouter();
const pathname = usePathname();
@ -23,22 +24,25 @@ export const CyclePeekOverview: React.FC<Props> = 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 && (
<div
ref={ref}
className="flex h-full w-full max-w-[21.5rem] flex-shrink-0 flex-col gap-3.5 overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 px-4 duration-300 fixed md:relative right-0 z-[9]"
@ -47,7 +51,13 @@ export const CyclePeekOverview: React.FC<Props> = 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)",
}}
>
<CycleDetailsSidebar handleClose={handleClose} isArchived={isArchived} cycleId={peekCycle} />
<CycleDetailsSidebar
handleClose={handleClose}
isArchived={isArchived}
projectId={projectId}
workspaceSlug={workspaceSlug}
cycleId={peekCycle}
/>
</div>
)}
</>

View file

@ -75,6 +75,7 @@ export const CycleForm: React.FC<Props> = (props) => {
onChange(val);
setActiveProject(val);
}}
multiple={false}
buttonVariant="border-with-text"
renderCondition={(project) => shouldRenderProject(project)}
tabIndex={getIndex("cover_image")}

View file

@ -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<Props> = 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<HTMLButtonElement>) => {
@ -201,9 +200,9 @@ export const CycleListItemAction: FC<Props> = 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 });
}
};

View file

@ -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<Props> = 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 (
<Row className="flex items-center gap-2 flex-shrink-0 py-2.5">
<ChevronRight
className={cn("h-4 w-4 text-custom-sidebar-text-300 duration-300 ", {
"rotate-90": isExpanded,
})}
strokeWidth={2}
/>
<div className="flex size-4 flex-shrink-0 items-center justify-center overflow-hidden">
<Logo logo={project.logo_props} size={16} />
</div>
<div className="relative flex w-full flex-row items-center gap-1 overflow-hidden">
<div className="inline-block line-clamp-1 truncate font-medium text-custom-text-100">{project.name}</div>
{showCount && <div className="pl-2 text-sm font-medium text-custom-text-300">{`${count ?? "0"}`}</div>}
</div>
</Row>
);
});

View file

@ -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<TCyclesListItem> = 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 });
}
};

View file

@ -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";

View file

@ -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) => {

View file

@ -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<Props> = observer((props) => {
const {
@ -43,6 +52,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
dropdownArrow = false,
dropdownArrowClassName = "",
hideIcon = false,
multiple,
onChange,
onClose,
placeholder = "Project",
@ -99,8 +109,6 @@ export const ProjectDropdown: React.FC<Props> = 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<Props> = 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) => (
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
<Logo logo={projectDetails.logo_props} size={14} />
</span>
);
if (Array.isArray(value)) {
return (
<div className="flex items-center gap-0.5">
{value.map((projectId) => {
const projectDetails = getProjectById(projectId);
return projectDetails ? renderIcon(projectDetails) : null;
})}
</div>
);
} else {
const projectDetails = getProjectById(value);
return projectDetails ? renderIcon(projectDetails) : null;
}
};
const comboButton = (
@ -147,18 +186,14 @@ export const ProjectDropdown: React.FC<Props> = 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 && (
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
<Logo logo={selectedProject.logo_props} size={12} />
</span>
)}
{!hideIcon && getProjectIcon(value)}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate max-w-40">{selectedProject?.name ?? placeholder}</span>
<span className="flex-grow truncate max-w-40">{getDisplayName(value, placeholder)}</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
@ -181,6 +216,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
onKeyDown={handleKeyDown}
button={comboButton}
renderByDefault={renderByDefault}
multiple={multiple}
>
{isOpen && (
<Combobox.Options className="fixed z-10" static>

View file

@ -16,28 +16,25 @@ interface RichTextEditorWrapperProps
extends Omit<IRichTextEditor, "disabledExtensions" | "fileHandler" | "mentionHandler"> {
workspaceSlug: string;
workspaceId: string;
projectId: string;
memberIds: string[];
projectId?: string;
uploadFile: (file: File) => Promise<string>;
}
export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProps>((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

View file

@ -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<TInboxIssueActionsHeader> = observer((p
)}
<InboxIssueStatus inboxIssue={inboxIssue} iconSize={12} />
<div className="flex items-center justify-end w-full">
<IssueUpdateStatus isSubmitting={isSubmitting} />
<NameDescriptionUpdateStatus isSubmitting={isSubmitting} />
</div>
</div>

View file

@ -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<Props> = observer((props) =
<div className="flex items-center gap-4">
<InboxIssueStatus inboxIssue={inboxIssue} iconSize={12} />
<div className="flex items-center justify-end w-full">
<IssueUpdateStatus isSubmitting={isSubmitting} />
<NameDescriptionUpdateStatus isSubmitting={isSubmitting} />
</div>
</div>
<div className="ml-auto">

View file

@ -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<SetStateAction<"submitting" | "submitted" | "saved">>;
isSubmitting: TNameDescriptionLoader;
setIsSubmitting: Dispatch<SetStateAction<TNameDescriptionLoader>>;
};
export const InboxIssueMainContent: React.FC<Props> = observer((props) => {

View file

@ -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<TInboxContentRoot> = observer((props) => {
/// router
const router = useAppRouter();
// states
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
const [isSubmitting, setIsSubmitting] = useState<TNameDescriptionLoader>("saved");
// hooks
const { data: currentUser } = useUser();
const { currentTab, fetchInboxIssueById, getIssueInboxByIssueId, getIsIssueAvailable } = useProjectInbox();

View file

@ -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<TInboxIssueDescription> = 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<TInboxIssueDescription> = 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)}

View file

@ -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<IssueDescriptionInputProps> = observer((p
setIsSubmitting,
placeholder,
} = props;
// store hooks
const {
project: { getProjectMemberIds },
} = useMember();
// derived values
const memberIds = getProjectMemberIds(projectId) ?? [];
const { handleSubmit, reset, control } = useForm<TIssue>({
defaultValues: {
@ -108,6 +114,7 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
value={swrIssueDescription ?? null}
workspaceSlug={workspaceSlug}
workspaceId={workspaceId}
memberIds={memberIds}
projectId={projectId}
dragDropEnabled
onChange={(_description: object, description_html: string) => {

View file

@ -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<Props> = observer((props) => {
const { workspaceSlug, projectId, issueId, issueOperations, isEditable, isArchived } = props;
// states
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
const [isSubmitting, setIsSubmitting] = useState<TNameDescriptionLoader>("saved");
// hooks
const windowSize = useSize();
const { data: currentUser } = useUser();
@ -87,7 +88,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
<div className="mb-2.5 flex items-center justify-between gap-4">
<IssueTypeSwitcher issueId={issueId} disabled={isArchived || !isEditable} />
<div className="flex items-center gap-3">
<IssueUpdateStatus isSubmitting={isSubmitting} />
<NameDescriptionUpdateStatus isSubmitting={isSubmitting} />
{duplicateIssues?.length > 0 && (
<DeDupeIssuePopoverRoot
workspaceSlug={workspaceSlug}

View file

@ -37,6 +37,9 @@ import { renderFormattedDate } from "@/helpers/date-time.helper";
import { getFileURL } from "@/helpers/file.helper";
// store
import { store } from "@/lib/store-context";
// plane web store
import { getTeamProjectColumns } from "@/plane-web/components/issues/issue-layouts/utils";
// store
import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/base-issues.store";
export const HIGHLIGHT_CLASS = "highlight";
@ -100,6 +103,7 @@ export const getGroupByColumns = ({
labels: () => 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,
};
};
};

View file

@ -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<TIssueDescriptionEditorProps> = 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<TIssueDescriptionEditorProps> = ob
value={descriptionHtmlData}
workspaceSlug={workspaceSlug?.toString() as string}
workspaceId={workspaceId}
memberIds={memberIds}
projectId={projectId}
onChange={(_description: object, description_html: string) => {
onChange(description_html);

View file

@ -46,6 +46,7 @@ export const IssueProjectSelect: React.FC<TIssueProjectSelectProps> = observer((
onChange(projectId);
handleFormChange();
}}
multiple={false}
buttonVariant="border-with-text"
renderCondition={(project) => shouldRenderProject(project)}
tabIndex={getIndex("project_id")}

View file

@ -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<Props> = observer((props) => {
export const NameDescriptionUpdateStatus: React.FC<Props> = observer((props) => {
const { isSubmitting } = props;
return (
@ -17,7 +19,7 @@ export const IssueUpdateStatus: React.FC<Props> = observer((props) => {
}`}
>
{isSubmitting !== "submitted" && isSubmitting !== "saved" && (
<RefreshCw className="h-4 w-4 stroke-custom-text-300" />
<RefreshCw className="animate-spin size-3.5 stroke-custom-text-300" />
)}
<span className="text-sm text-custom-text-300">{isSubmitting === "submitting" ? "Saving..." : "Saved"}</span>
</div>

View file

@ -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<PeekOverviewHeaderProps> = observer((props) => {
@ -157,7 +159,7 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
)}
</div>
<div className="flex items-center gap-x-4">
<IssueUpdateStatus isSubmitting={isSubmitting} />
<NameDescriptionUpdateStatus isSubmitting={isSubmitting} />
<div className="flex items-center gap-4">
{currentUser && !isArchived && (
<IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />

View file

@ -86,6 +86,7 @@ export const ModuleForm: React.FC<Props> = (props) => {
onChange(val);
setActiveProject(val);
}}
multiple={false}
buttonVariant="border-with-text"
renderCondition={(project) => shouldRenderProject(project)}
tabIndex={getIndex("cover_image")}

View file

@ -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 <ProjectsLoader />;

View file

@ -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<HTMLInputElement>(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<HTMLInputElement>) => {
if (e.key === "Escape") {
if (searchQuery && searchQuery.trim() !== "") updateSearchQuery("");
else setIsSearchOpen(false);
}
};
useEffect(() => {
if (searchQuery.trim() !== "") setIsSearchOpen(true);
}, [searchQuery]);
return (
<Header>
<Header.LeftItem>
@ -65,51 +41,7 @@ export const ProjectsBaseHeader = observer(() => {
</Breadcrumbs>
</Header.LeftItem>
<Header.RightItem>
<div className="flex items-center">
{!isSearchOpen && (
<button
type="button"
className="-mr-1 p-2 hover:bg-custom-background-80 rounded text-custom-text-400 grid place-items-center"
onClick={() => {
setIsSearchOpen(true);
inputRef.current?.focus();
}}
>
<Search className="h-3.5 w-3.5" />
</button>
)}
<div
className={cn(
"ml-auto flex items-center justify-start gap-1 rounded-md border border-transparent bg-custom-background-100 text-custom-text-400 w-0 transition-[width] ease-linear overflow-hidden opacity-0",
{
"w-30 md:w-64 px-2.5 py-1.5 border-custom-border-200 opacity-100": isSearchOpen,
}
)}
>
<Search className="h-3.5 w-3.5" />
<input
ref={inputRef}
className="w-full max-w-[234px] border-none bg-transparent text-sm text-custom-text-100 placeholder:text-custom-text-400 focus:outline-none"
placeholder="Search"
value={searchQuery}
onChange={(e) => updateSearchQuery(e.target.value)}
onKeyDown={handleInputKeyDown}
/>
{isSearchOpen && (
<button
type="button"
className="grid place-items-center"
onClick={() => {
updateSearchQuery("");
setIsSearchOpen(false);
}}
>
<X className="h-3 w-3" />
</button>
)}
</div>
</div>
<ProjectSearch />
<div className="hidden md:flex">
<HeaderFilters />
</div>

View file

@ -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";

View file

@ -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<void>;
};
export const ProjectMultiSelectModal: React.FC<Props> = observer((props) => {
const { isOpen, onClose, selectedProjectIds: selectedProjectIdsProp, projectIds, onSubmit } = props;
// states
const [searchTerm, setSearchTerm] = useState("");
const [selectedProjectIds, setSelectedProjectIds] = useState<string[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
// refs
const moveButtonRef = useRef<HTMLButtonElement>(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 (
<ModalCore isOpen={isOpen} width={EModalWidth.LG} position={EModalPosition.TOP} handleClose={handleClose}>
<Combobox as="div" multiple value={selectedProjectIds} onChange={handleSelectedProjectChange}>
<div className="flex items-center gap-2 px-4 border-b border-custom-border-100">
<Search className="flex-shrink-0 size-4 text-custom-text-400" aria-hidden="true" />
<Combobox.Input
className="h-12 w-full border-0 bg-transparent text-sm text-custom-text-100 outline-none placeholder:text-custom-text-400 focus:ring-0"
placeholder="Search for projects"
displayValue={() => ""}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
{selectedProjectIds.length > 0 && (
<div className="flex flex-wrap gap-2 pt-2 px-4">
{selectedProjectIds.map((projectId) => {
const projectDetails = projectDetailsMap.get(projectId);
if (!projectDetails) return null;
return (
<div
key={projectDetails.id}
className="group flex items-center gap-1.5 bg-custom-background-90 px-2 py-1 rounded cursor-pointer"
onClick={() => {
handleSelectedProjectChange(selectedProjectIds.filter((id) => id !== projectDetails.id));
}}
>
<Logo logo={projectDetails.logo_props} size={14} />
<p className="text-xs truncate text-custom-text-300 group-hover:text-custom-text-200 transition-colors">
{projectDetails.identifier}
</p>
<X className="size-3 flex-shrink-0 text-custom-text-400 group-hover:text-custom-text-200 transition-colors" />
</div>
);
})}
</div>
)}
<Combobox.Options
static
className="py-2 vertical-scrollbar scrollbar-md max-h-80 scroll-py-2 overflow-y-auto transition-[height] duration-200 ease-in-out"
>
{filteredProjectIds.length === 0 ? (
<div className="flex flex-col items-center justify-center px-3 py-8 text-center">
<EmptyState type={EmptyStateType.PROJECTS_EMPTY_SEARCH} layout="screen-simple" />
</div>
) : (
<ul
className={cn("text-custom-text-100", {
"px-2": filteredProjectIds.length > 0,
})}
>
{filteredProjectIds.map((projectId) => {
const projectDetails = projectDetailsMap.get(projectId);
if (!projectDetails) return null;
const isProjectSelected = selectedProjectIds.includes(projectDetails.id);
return (
<Combobox.Option
key={projectDetails.id}
value={projectDetails.id}
className={({ active }) =>
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,
}
)
}
>
<div className="flex items-center gap-2 truncate">
<span className="flex-shrink-0 flex items-center gap-2.5">
<Checkbox checked={isProjectSelected} />
<Logo logo={projectDetails.logo_props} size={16} />
</span>
<span className="flex-shrink-0 text-[10px]">{projectDetails.identifier}</span>
<p className="text-sm truncate">{projectDetails.name}</p>
</div>
</Combobox.Option>
);
})}
</ul>
)}
</Combobox.Options>
</Combobox>
<div className="flex items-center justify-end gap-2 p-3 border-t border-custom-border-100">
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
Cancel
</Button>
<Button
ref={moveButtonRef}
variant="primary"
size="sm"
onClick={handleSubmit}
loading={isSubmitting}
disabled={!areSelectedProjectsChanged}
>
{isSubmitting ? "Confirming" : "Confirm"}
</Button>
</div>
</ModalCore>
);
});

View file

@ -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<HTMLInputElement>(null);
// states
const [isSearchOpen, setIsSearchOpen] = useState(false);
// outside click detector hook
useOutsideClickDetector(inputRef, () => {
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
});
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Escape") {
if (searchQuery && searchQuery.trim() !== "") updateSearchQuery("");
else setIsSearchOpen(false);
}
};
return (
<div className="flex items-center">
{!isSearchOpen && (
<button
type="button"
className="-mr-1 p-2 hover:bg-custom-background-80 rounded text-custom-text-400 grid place-items-center"
onClick={() => {
setIsSearchOpen(true);
inputRef.current?.focus();
}}
>
<Search className="h-3.5 w-3.5" />
</button>
)}
<div
className={cn(
"ml-auto flex items-center justify-start gap-1 rounded-md border border-transparent bg-custom-background-100 text-custom-text-400 w-0 transition-[width] ease-linear overflow-hidden opacity-0",
{
"w-30 md:w-64 px-2.5 py-1.5 border-custom-border-200 opacity-100": isSearchOpen,
}
)}
>
<Search className="h-3.5 w-3.5" />
<input
ref={inputRef}
className="w-full max-w-[234px] border-none bg-transparent text-sm text-custom-text-100 placeholder:text-custom-text-400 focus:outline-none"
placeholder="Search"
value={searchQuery}
onChange={(e) => updateSearchQuery(e.target.value)}
onKeyDown={handleInputKeyDown}
/>
{isSearchOpen && (
<button
type="button"
className="grid place-items-center"
onClick={() => {
updateSearchQuery("");
setIsSearchOpen(false);
}}
>
<X className="h-3 w-3" />
</button>
)}
</div>
</div>
);
});

View file

@ -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<HTMLDivElement | null>(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<boolean>("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()) && (
<Tooltip
key={link.key}
@ -188,7 +192,8 @@ export const SidebarWorkspaceMenu = observer(() => {
</Link>
</Tooltip>
)
)}
);
})}
</Disclosure.Panel>
)}
</Transition>

View file

@ -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<Props>;
}[] = [
{
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;
};

View file

@ -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 {

View file

@ -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 = <T extends EIssuesStoreType>(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 = <T extends EIssuesStoreType>(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,

View file

@ -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,

View file

@ -8,7 +8,7 @@ export const IssuesStoreContext = createContext<EIssuesStoreType | undefined>(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;
};

View file

@ -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:

View file

@ -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 = <T,>(key: string, initialValue: T) => {
const [storedValue, setStoredValue] = useState<T | null>(() => getValueFromLocalStorage(key, initialValue));

View file

@ -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 ||

View file

@ -129,6 +129,7 @@ const ISSUE_GROUP_BY_KEY: Record<TIssueDisplayFilterOptions, keyof TIssue> = {
target_date: "target_date",
cycle: "cycle_id",
module: "module_ids",
team_project: "project_id",
};
export const ISSUE_FILTER_DEFAULT_DATA: Record<TIssueDisplayFilterOptions, keyof TIssue> = {
@ -142,6 +143,7 @@ export const ISSUE_FILTER_DEFAULT_DATA: Record<TIssueDisplayFilterOptions, keyof
created_by: "created_by",
assignees: "assignee_ids",
target_date: "target_date",
team_project: "project_id",
};
// This constant maps the order by keys to the respective issue property that the key relies on

View file

@ -93,6 +93,7 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
start_date: filters?.start_date || undefined,
target_date: filters?.target_date || undefined,
project: filters?.project || undefined,
team_project: filters?.team_project || undefined,
subscriber: filters?.subscriber || undefined,
issue_type: filters?.issue_type || undefined,
// display filters
@ -142,6 +143,7 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
start_date: filters?.start_date || null,
target_date: filters?.target_date || null,
project: filters?.project || null,
team_project: filters?.team_project || null,
subscriber: filters?.subscriber || null,
issue_type: filters?.issue_type || null,
});

View file

@ -1,9 +1,17 @@
import isEmpty from "lodash/isEmpty";
import { autorun, makeObservable, observable } from "mobx";
import { ICycle, IIssueLabel, IModule, IProject, IState, IUserLite } from "@plane/types";
// plane web store
import { ITeamIssuesFilter, ITeamIssues, TeamIssues, TeamIssuesFilter } from "@/plane-web/store/issue/team";
import {
ITeamViewIssues,
ITeamViewIssuesFilter,
TeamViewIssues,
TeamViewIssuesFilter,
} from "@/plane-web/store/issue/team-views";
// root store
import { RootStore } from "@/plane-web/store/root.store";
import { IWorkspaceMembership } from "@/store/member/workspace-member.store";
import { CoreRootStore } from "../root.store";
import { IStateStore, StateStore } from "../state.store";
// issues data store
import { IArchivedIssuesFilter, ArchivedIssuesFilter, IArchivedIssues, ArchivedIssues } from "./archived";
@ -33,6 +41,7 @@ import {
export interface IIssueRootStore {
currentUserId: string | undefined;
workspaceSlug: string | undefined;
teamId: string | undefined;
projectId: string | undefined;
cycleId: string | undefined;
moduleId: string | undefined;
@ -49,7 +58,7 @@ export interface IIssueRootStore {
moduleMap: Record<string, IModule> | undefined;
cycleMap: Record<string, ICycle> | 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<string, IModule> | undefined = undefined;
cycleMap: Record<string, ICycle> | 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);

View file

@ -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<TPage>) => Promise<TPage | undefined>;
@ -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;
});

View file

@ -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

View file

@ -1,2 +1,3 @@
export * from "./applied-filters";
export * from "./issue-types";
export * from "./team-project";

View file

@ -0,0 +1 @@
export * from "ce/components/issues/filters/team-project";

View file

@ -0,0 +1 @@
export * from "ce/components/issues/issue-layouts/utils";

View file

@ -0,0 +1 @@
export * from "ce/components/workspace/sidebar/teams-sidebar-list";

View file

@ -0,0 +1 @@
export * from "ce/constants/dashboard";

View file

@ -0,0 +1 @@
export * from "ce/helpers/issue-action-helper";

View file

@ -0,0 +1 @@
export * from "ce/store/issue/team-views";

View file

@ -0,0 +1 @@
export * from "ce/store/issue/team";

View file

@ -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[] = [];