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:
parent
9ad8b43408
commit
54f828cbfa
82 changed files with 1044 additions and 320 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
2
packages/types/src/common.d.ts
vendored
2
packages/types/src/common.d.ts
vendored
|
|
@ -22,3 +22,5 @@ export type TLogoProps = {
|
|||
background_color?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TNameDescriptionLoader = "submitting" | "submitted" | "saved";
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
|
|
|
|||
3
packages/types/src/issues.d.ts
vendored
3
packages/types/src/issues.d.ts
vendored
|
|
@ -211,7 +211,8 @@ export type GroupByColumnTypes =
|
|||
| "priority"
|
||||
| "labels"
|
||||
| "assignees"
|
||||
| "created_by";
|
||||
| "created_by"
|
||||
| "team_project";
|
||||
|
||||
export interface IGroupByColumn {
|
||||
id: string;
|
||||
|
|
|
|||
3
packages/types/src/view-props.d.ts
vendored
3
packages/types/src/view-props.d.ts
vendored
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
export * from "./breadcrumbs";
|
||||
export * from "./navigation-dropdown";
|
||||
|
|
|
|||
96
packages/ui/src/breadcrumbs/navigation-dropdown.tsx
Normal file
96
packages/ui/src/breadcrumbs/navigation-dropdown.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
26
packages/ui/src/icons/lead-icon.tsx
Normal file
26
packages/ui/src/icons/lead-icon.tsx
Normal 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>
|
||||
);
|
||||
19
packages/ui/src/icons/teams.tsx
Normal file
19
packages/ui/src/icons/teams.tsx
Normal 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>
|
||||
);
|
||||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./applied-filters";
|
||||
export * from "./issue-types";
|
||||
export * from "./team-project";
|
||||
|
|
|
|||
12
web/ce/components/issues/filters/team-project.tsx
Normal file
12
web/ce/components/issues/filters/team-project.tsx
Normal 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);
|
||||
4
web/ce/components/issues/issue-layouts/utils.tsx
Normal file
4
web/ce/components/issues/issue-layouts/utils.tsx
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// types
|
||||
import { IGroupByColumn } from "@plane/types";
|
||||
|
||||
export const getTeamProjectColumns = (): IGroupByColumn[] | undefined => undefined;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export const SidebarTeamsList = () => null;
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
} = {};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
15
web/ce/helpers/issue-action-helper.ts
Normal file
15
web/ce/helpers/issue-action-helper.ts
Normal 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),
|
||||
});
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
12
web/ce/store/issue/team-views/filter.store.ts
Normal file
12
web/ce/store/issue/team-views/filter.store.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
2
web/ce/store/issue/team-views/index.ts
Normal file
2
web/ce/store/issue/team-views/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./filter.store";
|
||||
export * from "./issue.store";
|
||||
13
web/ce/store/issue/team-views/issue.store.ts
Normal file
13
web/ce/store/issue/team-views/issue.store.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
12
web/ce/store/issue/team/filter.store.ts
Normal file
12
web/ce/store/issue/team/filter.store.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
2
web/ce/store/issue/team/index.ts
Normal file
2
web/ce/store/issue/team/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./filter.store";
|
||||
export * from "./issue.store";
|
||||
13
web/ce/store/issue/team/issue.store.ts
Normal file
13
web/ce/store/issue/team/issue.store.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1,3 @@
|
|||
export type TSidebarUserMenuItemKeys = "home" | "your-work" | "notifications" | "drafts";
|
||||
|
||||
export type TSidebarWorkspaceMenuItemKeys = "projects" | "all-issues" | "active-cycles" | "analytics";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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")}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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")}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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")}
|
||||
|
|
|
|||
|
|
@ -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 />;
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
175
web/core/components/project/multi-select-modal.tsx
Normal file
175
web/core/components/project/multi-select-modal.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
78
web/core/components/project/search-projects.tsx
Normal file
78
web/core/components/project/search-projects.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./applied-filters";
|
||||
export * from "./issue-types";
|
||||
export * from "./team-project";
|
||||
|
|
|
|||
1
web/ee/components/issues/filters/team-project.tsx
Normal file
1
web/ee/components/issues/filters/team-project.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "ce/components/issues/filters/team-project";
|
||||
1
web/ee/components/issues/issue-layouts/utils.tsx
Normal file
1
web/ee/components/issues/issue-layouts/utils.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "ce/components/issues/issue-layouts/utils";
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from "ce/components/workspace/sidebar/teams-sidebar-list";
|
||||
1
web/ee/constants/dashboard.ts
Normal file
1
web/ee/constants/dashboard.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "ce/constants/dashboard";
|
||||
1
web/ee/helpers/issue-action-helper.ts
Normal file
1
web/ee/helpers/issue-action-helper.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "ce/helpers/issue-action-helper";
|
||||
1
web/ee/store/issue/team-views/index.ts
Normal file
1
web/ee/store/issue/team-views/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "ce/store/issue/team-views";
|
||||
1
web/ee/store/issue/team/index.ts
Normal file
1
web/ee/store/issue/team/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "ce/store/issue/team";
|
||||
|
|
@ -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[] = [];
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue