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",
|
"target_date" = "target_date",
|
||||||
"project" = "project_id",
|
"project" = "project_id",
|
||||||
"created_by" = "created_by",
|
"created_by" = "created_by",
|
||||||
|
"team_project" = "project_id",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum EIssueGroupBYServerToProperty {
|
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;
|
background_color?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TNameDescriptionLoader = "submitting" | "submitted" | "saved";
|
||||||
|
|
|
||||||
|
|
@ -59,4 +59,5 @@ export enum EFileAssetType {
|
||||||
USER_AVATAR = "USER_AVATAR",
|
USER_AVATAR = "USER_AVATAR",
|
||||||
USER_COVER = "USER_COVER",
|
USER_COVER = "USER_COVER",
|
||||||
WORKSPACE_LOGO = "WORKSPACE_LOGO",
|
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"
|
| "priority"
|
||||||
| "labels"
|
| "labels"
|
||||||
| "assignees"
|
| "assignees"
|
||||||
| "created_by";
|
| "created_by"
|
||||||
|
| "team_project";
|
||||||
|
|
||||||
export interface IGroupByColumn {
|
export interface IGroupByColumn {
|
||||||
id: string;
|
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"
|
| "cycle"
|
||||||
| "module"
|
| "module"
|
||||||
| "target_date"
|
| "target_date"
|
||||||
|
| "team_project"
|
||||||
| null;
|
| null;
|
||||||
|
|
||||||
export type TIssueOrderByOptions =
|
export type TIssueOrderByOptions =
|
||||||
|
|
@ -69,6 +70,7 @@ export type TIssueParams =
|
||||||
| "start_date"
|
| "start_date"
|
||||||
| "target_date"
|
| "target_date"
|
||||||
| "project"
|
| "project"
|
||||||
|
| "team_project"
|
||||||
| "group_by"
|
| "group_by"
|
||||||
| "sub_group_by"
|
| "sub_group_by"
|
||||||
| "order_by"
|
| "order_by"
|
||||||
|
|
@ -92,6 +94,7 @@ export interface IIssueFilterOptions {
|
||||||
cycle?: string[] | null;
|
cycle?: string[] | null;
|
||||||
module?: string[] | null;
|
module?: string[] | null;
|
||||||
project?: string[] | null;
|
project?: string[] | null;
|
||||||
|
team_project?: string[] | null;
|
||||||
start_date?: string[] | null;
|
start_date?: string[] | null;
|
||||||
state?: string[] | null;
|
state?: string[] | null;
|
||||||
state_group?: string[] | null;
|
state_group?: string[] | null;
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
export * from "./breadcrumbs";
|
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 "./full-screen-panel-icon";
|
||||||
export * from "./github-icon";
|
export * from "./github-icon";
|
||||||
export * from "./gitlab-icon";
|
export * from "./gitlab-icon";
|
||||||
|
export * from "./info-fill-icon";
|
||||||
export * from "./info-icon";
|
export * from "./info-icon";
|
||||||
export * from "./layer-stack";
|
export * from "./layer-stack";
|
||||||
export * from "./layers-icon";
|
export * from "./layers-icon";
|
||||||
|
|
@ -38,3 +39,5 @@ export * from "./done-icon";
|
||||||
export * from "./pending-icon";
|
export * from "./pending-icon";
|
||||||
export * from "./pi-chat";
|
export * from "./pi-chat";
|
||||||
export * from "./workspace-icon";
|
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 { Tab } from "@headlessui/react";
|
||||||
import { LucideProps } from "lucide-react";
|
import { LucideProps } from "lucide-react";
|
||||||
// helpers
|
// helpers
|
||||||
|
|
@ -11,11 +11,12 @@ type TabItem = {
|
||||||
label?: React.ReactNode;
|
label?: React.ReactNode;
|
||||||
content: React.ReactNode;
|
content: React.ReactNode;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TTabsProps = {
|
type TTabsProps = {
|
||||||
tabs: TabItem[];
|
tabs: TabItem[];
|
||||||
storageKey: string;
|
storageKey?: string;
|
||||||
actions?: React.ReactNode;
|
actions?: React.ReactNode;
|
||||||
defaultTab?: string;
|
defaultTab?: string;
|
||||||
containerClassName?: string;
|
containerClassName?: string;
|
||||||
|
|
@ -23,6 +24,8 @@ type TTabsProps = {
|
||||||
tabListClassName?: string;
|
tabListClassName?: string;
|
||||||
tabClassName?: string;
|
tabClassName?: string;
|
||||||
tabPanelClassName?: string;
|
tabPanelClassName?: string;
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
storeInLocalStorage?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Tabs: FC<TTabsProps> = (props: TTabsProps) => {
|
export const Tabs: FC<TTabsProps> = (props: TTabsProps) => {
|
||||||
|
|
@ -36,15 +39,28 @@ export const Tabs: FC<TTabsProps> = (props: TTabsProps) => {
|
||||||
tabListClassName = "",
|
tabListClassName = "",
|
||||||
tabClassName = "",
|
tabClassName = "",
|
||||||
tabPanelClassName = "",
|
tabPanelClassName = "",
|
||||||
|
size = "md",
|
||||||
|
storeInLocalStorage = true,
|
||||||
} = props;
|
} = props;
|
||||||
// local storage
|
// 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);
|
const currentTabIndex = (tabKey: string): number => tabs.findIndex((tab) => tab.key === tabKey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full h-full">
|
<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 flex-col w-full h-full gap-2", containerClassName)}>
|
||||||
<div className={cn("flex w-full items-center gap-4", tabListContainerClassName)}>
|
<div className={cn("flex w-full items-center gap-4", tabListContainerClassName)}>
|
||||||
<Tab.List
|
<Tab.List
|
||||||
|
|
@ -64,12 +80,18 @@ export const Tabs: FC<TTabsProps> = (props: TTabsProps) => {
|
||||||
: tab.disabled
|
: tab.disabled
|
||||||
? "text-custom-text-400 cursor-not-allowed"
|
? "text-custom-text-400 cursor-not-allowed"
|
||||||
: "text-custom-text-400 hover:text-custom-text-300 hover:bg-custom-background-80/60",
|
: "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
|
tabClassName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
key={tab.key}
|
key={tab.key}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!tab.disabled) setValue(tab.key);
|
if (!tab.disabled) setSelectedTab(tab.key);
|
||||||
|
tab.onClick?.();
|
||||||
}}
|
}}
|
||||||
disabled={tab.disabled}
|
disabled={tab.disabled}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import { CoreRootStore } from "../root.store";
|
||||||
// constants
|
// constants
|
||||||
// helpers
|
// helpers
|
||||||
|
|
||||||
export type TIssueDisplayFilterOptions = Exclude<TIssueGroupByOptions, null> | "target_date";
|
export type TIssueDisplayFilterOptions = Exclude<TIssueGroupByOptions, null | "team_project"> | "target_date";
|
||||||
|
|
||||||
export enum EIssueGroupedAction {
|
export enum EIssueGroupedAction {
|
||||||
ADD = "ADD",
|
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)",
|
"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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import { useFavorite } from "@/hooks/store/use-favorite";
|
||||||
import useSize from "@/hooks/use-window-size";
|
import useSize from "@/hooks/use-window-size";
|
||||||
// plane web components
|
// plane web components
|
||||||
import { SidebarAppSwitcher } from "@/plane-web/components/sidebar";
|
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";
|
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||||
|
|
||||||
export const AppSidebar: FC = observer(() => {
|
export const AppSidebar: FC = observer(() => {
|
||||||
|
|
@ -47,7 +48,7 @@ export const AppSidebar: FC = observer(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (windowSize[0] < 768) !sidebarCollapsed && toggleSidebar();
|
if (windowSize[0] < 768 && !sidebarCollapsed) toggleSidebar();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [windowSize]);
|
}, [windowSize]);
|
||||||
|
|
||||||
|
|
@ -73,9 +74,12 @@ export const AppSidebar: FC = observer(() => {
|
||||||
"px-4": !sidebarCollapsed,
|
"px-4": !sidebarCollapsed,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
{/* Workspace switcher and settings */}
|
||||||
<SidebarDropdown />
|
<SidebarDropdown />
|
||||||
<div className="flex-shrink-0 h-4" />
|
<div className="flex-shrink-0 h-4" />
|
||||||
<SidebarAppSwitcher />
|
{/* App switcher */}
|
||||||
|
{canPerformWorkspaceMemberActions && <SidebarAppSwitcher />}
|
||||||
|
{/* Quick actions */}
|
||||||
<SidebarQuickActions />
|
<SidebarQuickActions />
|
||||||
</div>
|
</div>
|
||||||
<hr
|
<hr
|
||||||
|
|
@ -88,18 +92,23 @@ export const AppSidebar: FC = observer(() => {
|
||||||
"vertical-scrollbar px-4": !sidebarCollapsed,
|
"vertical-scrollbar px-4": !sidebarCollapsed,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
{/* User Menu */}
|
||||||
<SidebarUserMenu />
|
<SidebarUserMenu />
|
||||||
|
{/* Workspace Menu */}
|
||||||
<SidebarWorkspaceMenu />
|
<SidebarWorkspaceMenu />
|
||||||
<hr
|
<hr
|
||||||
className={cn("flex-shrink-0 border-custom-sidebar-border-300 h-[0.5px] w-3/5 mx-auto my-1", {
|
className={cn("flex-shrink-0 border-custom-sidebar-border-300 h-[0.5px] w-3/5 mx-auto my-1", {
|
||||||
"opacity-0": !sidebarCollapsed,
|
"opacity-0": !sidebarCollapsed,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
{/* Favorites Menu */}
|
||||||
{canPerformWorkspaceMemberActions && !isFavoriteEmpty && <SidebarFavoritesMenu />}
|
{canPerformWorkspaceMemberActions && !isFavoriteEmpty && <SidebarFavoritesMenu />}
|
||||||
|
{/* Teams List */}
|
||||||
|
<SidebarTeamsList />
|
||||||
|
{/* Projects List */}
|
||||||
<SidebarProjectsList />
|
<SidebarProjectsList />
|
||||||
</div>
|
</div>
|
||||||
|
{/* Help Section */}
|
||||||
<SidebarHelpSection />
|
<SidebarHelpSection />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Disclosure } from "@headlessui/react";
|
import { Disclosure } from "@headlessui/react";
|
||||||
// ui
|
// ui
|
||||||
|
|
@ -22,68 +23,80 @@ import { ActiveCycleIssueDetails } from "@/store/issue/cycle";
|
||||||
interface IActiveCycleDetails {
|
interface IActiveCycleDetails {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
cycleId?: string;
|
||||||
|
showHeader?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) => {
|
export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) => {
|
||||||
const { workspaceSlug, projectId } = props;
|
const { workspaceSlug, projectId, cycleId: propsCycleId, showHeader = true } = props;
|
||||||
const { currentProjectActiveCycle, currentProjectActiveCycleId } = useCycle();
|
const { currentProjectActiveCycleId } = useCycle();
|
||||||
|
// derived values
|
||||||
|
const cycleId = propsCycleId ?? currentProjectActiveCycleId;
|
||||||
|
// fetch cycle details
|
||||||
const {
|
const {
|
||||||
handleFiltersUpdate,
|
handleFiltersUpdate,
|
||||||
cycle: activeCycle,
|
cycle: activeCycle,
|
||||||
cycleIssueDetails,
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Disclosure as="div" className="flex flex-shrink-0 flex-col" defaultOpen>
|
{showHeader ? (
|
||||||
{({ open }) => (
|
<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 className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 cursor-pointer">
|
||||||
</Disclosure.Button>
|
<CycleListGroupHeader title="Active cycle" type="current" isExpanded={open} />
|
||||||
<Disclosure.Panel>
|
</Disclosure.Button>
|
||||||
{!currentProjectActiveCycle ? (
|
<Disclosure.Panel>{ActiveCyclesComponent}</Disclosure.Panel>
|
||||||
<EmptyState type={EmptyStateType.PROJECT_CYCLE_ACTIVE} size="sm" />
|
</>
|
||||||
) : (
|
)}
|
||||||
<div className="flex flex-col border-b border-custom-border-200">
|
</Disclosure>
|
||||||
{currentProjectActiveCycleId && (
|
) : (
|
||||||
<CyclesListItem
|
<>{ActiveCyclesComponent}</>
|
||||||
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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
export * from "./applied-filters";
|
export * from "./applied-filters";
|
||||||
export * from "./issue-types";
|
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";
|
"use client";
|
||||||
|
|
||||||
// icons
|
// icons
|
||||||
import { Home, Inbox, PenSquare } from "lucide-react";
|
import { Briefcase, Home, Inbox, Layers, PenSquare, BarChart2 } from "lucide-react";
|
||||||
// ui
|
// ui
|
||||||
import { UserActivityIcon } from "@plane/ui";
|
import { UserActivityIcon, ContrastIcon } from "@plane/ui";
|
||||||
import { Props } from "@/components/icons/types";
|
import { Props } from "@/components/icons/types";
|
||||||
|
// constants
|
||||||
import { TLinkOptions } from "@/constants/dashboard";
|
import { TLinkOptions } from "@/constants/dashboard";
|
||||||
|
// plane web constants
|
||||||
import { EUserPermissions } from "@/plane-web/constants/user-permissions";
|
import { EUserPermissions } from "@/plane-web/constants/user-permissions";
|
||||||
// plane web types
|
// plane web types
|
||||||
import { TSidebarUserMenuItemKeys } from "@/plane-web/types/dashboard";
|
import { TSidebarUserMenuItemKeys, TSidebarWorkspaceMenuItemKeys } from "@/plane-web/types/dashboard";
|
||||||
|
|
||||||
export type TSidebarUserMenuItems = {
|
export type TSidebarMenuItems<T extends TSidebarUserMenuItemKeys | TSidebarWorkspaceMenuItemKeys> = {
|
||||||
key: TSidebarUserMenuItemKeys;
|
key: T;
|
||||||
label: string;
|
label: string;
|
||||||
href: string;
|
href: string;
|
||||||
access: EUserPermissions[];
|
access: EUserPermissions[];
|
||||||
|
|
@ -19,6 +21,8 @@ export type TSidebarUserMenuItems = {
|
||||||
Icon: React.FC<Props>;
|
Icon: React.FC<Props>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TSidebarUserMenuItems = TSidebarMenuItems<TSidebarUserMenuItemKeys>;
|
||||||
|
|
||||||
export const SIDEBAR_USER_MENU_ITEMS: TSidebarUserMenuItems[] = [
|
export const SIDEBAR_USER_MENU_ITEMS: TSidebarUserMenuItems[] = [
|
||||||
{
|
{
|
||||||
key: "home",
|
key: "home",
|
||||||
|
|
@ -54,3 +58,47 @@ export const SIDEBAR_USER_MENU_ITEMS: TSidebarUserMenuItems[] = [
|
||||||
Icon: PenSquare,
|
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";
|
import { TIssueActivityComment } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { ILayoutDisplayFiltersOptions } from "@/constants/issue";
|
||||||
|
|
||||||
export enum EActivityFilterType {
|
export enum EActivityFilterType {
|
||||||
ACTIVITY = "ACTIVITY",
|
ACTIVITY = "ACTIVITY",
|
||||||
|
|
@ -19,7 +21,7 @@ export const ACTIVITY_FILTER_TYPE_OPTIONS: Record<EActivityFilterType, { label:
|
||||||
export const defaultActivityFilters: TActivityFilters[] = [EActivityFilterType.ACTIVITY, EActivityFilterType.COMMENT];
|
export const defaultActivityFilters: TActivityFilters[] = [EActivityFilterType.ACTIVITY, EActivityFilterType.COMMENT];
|
||||||
|
|
||||||
export type TActivityFilterOption = {
|
export type TActivityFilterOption = {
|
||||||
key: EActivityFilterType;
|
key: TActivityFilters;
|
||||||
label: string;
|
label: string;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
|
@ -32,3 +34,7 @@ export const filterActivityOnSelectedFilters = (
|
||||||
activity.filter((activity) => filter.includes(activity.activity_type as TActivityFilters));
|
activity.filter((activity) => filter.includes(activity.activity_type as TActivityFilters));
|
||||||
|
|
||||||
export const ENABLE_ISSUE_DEPENDENCIES = false;
|
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
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
export const isUserFeatureEnabled = (featureKey: TSidebarUserMenuItemKeys) => true;
|
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
|
// types / constants
|
||||||
import { BaseCommandPaletteStore, IBaseCommandPaletteStore } from "@/store/base-command-palette.store";
|
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 {
|
export class CommandPaletteStore extends BaseCommandPaletteStore implements ICommandPaletteStore {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
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 TSidebarUserMenuItemKeys = "home" | "your-work" | "notifications" | "drafts";
|
||||||
|
|
||||||
|
export type TSidebarWorkspaceMenuItemKeys = "projects" | "all-issues" | "active-cycles" | "analytics";
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
// ui
|
// ui
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
|
|
@ -13,19 +12,19 @@ import useCyclesDetails from "../active-cycle/use-cycles-details";
|
||||||
type Props = {
|
type Props = {
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
isArchived?: boolean;
|
isArchived?: boolean;
|
||||||
cycleId?: string;
|
cycleId: string;
|
||||||
|
projectId: string;
|
||||||
|
workspaceSlug: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||||
const { handleClose, isArchived } = props;
|
const { handleClose, isArchived, projectId, workspaceSlug, cycleId } = props;
|
||||||
// router
|
|
||||||
const { workspaceSlug, projectId, cycleId } = useParams();
|
|
||||||
|
|
||||||
// store hooks
|
// store hooks
|
||||||
const { cycle: cycleDetails } = useCyclesDetails({
|
const { cycle: cycleDetails } = useCyclesDetails({
|
||||||
workspaceSlug: workspaceSlug.toString(),
|
workspaceSlug,
|
||||||
projectId: projectId.toString(),
|
projectId,
|
||||||
cycleId: cycleId?.toString() || props.cycleId,
|
cycleId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!cycleDetails)
|
if (!cycleDetails)
|
||||||
|
|
@ -47,21 +46,17 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||||
<div className="relative pb-2">
|
<div className="relative pb-2">
|
||||||
<div className="flex flex-col gap-5 w-full">
|
<div className="flex flex-col gap-5 w-full">
|
||||||
<CycleSidebarHeader
|
<CycleSidebarHeader
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId.toString()}
|
projectId={projectId}
|
||||||
cycleDetails={cycleDetails}
|
cycleDetails={cycleDetails}
|
||||||
isArchived={isArchived}
|
isArchived={isArchived}
|
||||||
handleClose={handleClose}
|
handleClose={handleClose}
|
||||||
/>
|
/>
|
||||||
<CycleSidebarDetails projectId={projectId.toString()} cycleDetails={cycleDetails} />
|
<CycleSidebarDetails projectId={projectId} cycleDetails={cycleDetails} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{workspaceSlug && projectId && cycleDetails?.id && (
|
{workspaceSlug && projectId && cycleDetails?.id && (
|
||||||
<CycleAnalyticsProgress
|
<CycleAnalyticsProgress workspaceSlug={workspaceSlug} projectId={projectId} cycleId={cycleDetails?.id} />
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
|
||||||
projectId={projectId.toString()}
|
|
||||||
cycleId={cycleDetails?.id}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,13 @@ import { useAppRouter } from "@/hooks/use-app-router";
|
||||||
import { CycleDetailsSidebar } from "./";
|
import { CycleDetailsSidebar } from "./";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
projectId: string;
|
projectId?: string;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
isArchived?: boolean;
|
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
|
// router
|
||||||
const router = useAppRouter();
|
const router = useAppRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
@ -23,22 +24,25 @@ export const CyclePeekOverview: React.FC<Props> = observer(({ projectId, workspa
|
||||||
// refs
|
// refs
|
||||||
const ref = React.useRef(null);
|
const ref = React.useRef(null);
|
||||||
// store hooks
|
// 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 handleClose = () => {
|
||||||
const query = generateQueryParams(searchParams, ["peekCycle"]);
|
const query = generateQueryParams(searchParams, ["peekCycle"]);
|
||||||
router.push(`${pathname}?${query}`);
|
router.push(`${pathname}?${query}`, {}, { showProgressBar: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!peekCycle) return;
|
if (!peekCycle || !projectId) return;
|
||||||
if (isArchived) fetchArchivedCycleDetails(workspaceSlug, projectId, peekCycle.toString());
|
if (isArchived) fetchArchivedCycleDetails(workspaceSlug, projectId, peekCycle.toString());
|
||||||
else fetchCycleDetails(workspaceSlug, projectId, peekCycle.toString());
|
else fetchCycleDetails(workspaceSlug, projectId, peekCycle.toString());
|
||||||
}, [fetchArchivedCycleDetails, fetchCycleDetails, isArchived, peekCycle, projectId, workspaceSlug]);
|
}, [fetchArchivedCycleDetails, fetchCycleDetails, isArchived, peekCycle, projectId, workspaceSlug]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{peekCycle && (
|
{peekCycle && projectId && (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
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]"
|
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)",
|
"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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ export const CycleForm: React.FC<Props> = (props) => {
|
||||||
onChange(val);
|
onChange(val);
|
||||||
setActiveProject(val);
|
setActiveProject(val);
|
||||||
}}
|
}}
|
||||||
|
multiple={false}
|
||||||
buttonVariant="border-with-text"
|
buttonVariant="border-with-text"
|
||||||
renderCondition={(project) => shouldRenderProject(project)}
|
renderCondition={(project) => shouldRenderProject(project)}
|
||||||
tabIndex={getIndex("cover_image")}
|
tabIndex={getIndex("cover_image")}
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,9 @@ import { CycleQuickActions } from "@/components/cycles";
|
||||||
import { DateRangeDropdown } from "@/components/dropdowns";
|
import { DateRangeDropdown } from "@/components/dropdowns";
|
||||||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
||||||
// constants
|
// constants
|
||||||
import { CYCLE_STATUS } from "@/constants/cycle";
|
|
||||||
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "@/constants/event-tracker";
|
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "@/constants/event-tracker";
|
||||||
// helpers
|
// helpers
|
||||||
import { findHowManyDaysLeft, getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||||
import { getFileURL } from "@/helpers/file.helper";
|
import { getFileURL } from "@/helpers/file.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { generateQueryParams } from "@/helpers/router.helper";
|
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 cycleStatus = cycleDetails.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft";
|
||||||
const isEditingAllowed = allowPermissions(
|
const isEditingAllowed = allowPermissions(
|
||||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||||
EUserPermissionsLevel.PROJECT
|
EUserPermissionsLevel.PROJECT,
|
||||||
|
workspaceSlug,
|
||||||
|
projectId
|
||||||
);
|
);
|
||||||
const renderIcon = Boolean(cycleDetails.start_date) || Boolean(cycleDetails.end_date);
|
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
|
// handlers
|
||||||
const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => {
|
const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
|
@ -201,9 +200,9 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
||||||
|
|
||||||
const query = generateQueryParams(searchParams, ["peekCycle"]);
|
const query = generateQueryParams(searchParams, ["peekCycle"]);
|
||||||
if (searchParams.has("peekCycle") && searchParams.get("peekCycle") === cycleId) {
|
if (searchParams.has("peekCycle") && searchParams.get("peekCycle") === cycleId) {
|
||||||
router.push(`${pathname}?${query}`);
|
router.push(`${pathname}?${query}`, {}, { showProgressBar: false });
|
||||||
} else {
|
} 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 { observer } from "mobx-react";
|
||||||
import { usePathname, useSearchParams } from "next/navigation";
|
import { usePathname, useSearchParams } from "next/navigation";
|
||||||
// icons
|
// icons
|
||||||
import { Check, Info } from "lucide-react";
|
import { Check } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import type { TCycleGroups } from "@plane/types";
|
import type { TCycleGroups } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
|
|
@ -72,9 +72,9 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
|
||||||
|
|
||||||
const query = generateQueryParams(searchParams, ["peekCycle"]);
|
const query = generateQueryParams(searchParams, ["peekCycle"]);
|
||||||
if (searchParams.has("peekCycle") && searchParams.get("peekCycle") === cycleId) {
|
if (searchParams.has("peekCycle") && searchParams.get("peekCycle") === cycleId) {
|
||||||
router.push(`${pathname}?${query}`);
|
router.push(`${pathname}?${query}`, {}, { showProgressBar: false });
|
||||||
} else {
|
} 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 "./root";
|
||||||
export * from "./cycle-list-item-action";
|
export * from "./cycle-list-item-action";
|
||||||
export * from "./cycle-list-group-header";
|
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 = {
|
type TLayoutDropDown = {
|
||||||
onChange: (value: EIssueLayoutTypes) => void;
|
onChange: (value: EIssueLayoutTypes) => void;
|
||||||
value: EIssueLayoutTypes;
|
value: EIssueLayoutTypes;
|
||||||
|
disabledLayouts?: EIssueLayoutTypes[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LayoutDropDown = observer((props: TLayoutDropDown) => {
|
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(
|
const options = useMemo(
|
||||||
() =>
|
() =>
|
||||||
Object.values(ISSUE_LAYOUT_MAP).map((issueLayout) => ({
|
availableLayouts.map((issueLayout) => ({
|
||||||
data: issueLayout.key,
|
data: issueLayout.key,
|
||||||
value: issueLayout.key,
|
value: issueLayout.key,
|
||||||
})),
|
})),
|
||||||
[]
|
[availableLayouts]
|
||||||
);
|
);
|
||||||
|
|
||||||
const buttonContent = useCallback((isOpen: boolean, buttonValue: string | string[] | undefined) => {
|
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 { observer } from "mobx-react";
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
import { Check, ChevronDown, Search } from "lucide-react";
|
import { Check, ChevronDown, Search } from "lucide-react";
|
||||||
|
|
@ -25,12 +25,21 @@ type Props = TDropdownProps & {
|
||||||
button?: ReactNode;
|
button?: ReactNode;
|
||||||
dropdownArrow?: boolean;
|
dropdownArrow?: boolean;
|
||||||
dropdownArrowClassName?: string;
|
dropdownArrowClassName?: string;
|
||||||
onChange: (val: string) => void;
|
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
renderCondition?: (project: TProject) => boolean;
|
renderCondition?: (project: TProject) => boolean;
|
||||||
value: string | null;
|
|
||||||
renderByDefault?: boolean;
|
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) => {
|
export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||||
const {
|
const {
|
||||||
|
|
@ -43,6 +52,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||||
dropdownArrow = false,
|
dropdownArrow = false,
|
||||||
dropdownArrowClassName = "",
|
dropdownArrowClassName = "",
|
||||||
hideIcon = false,
|
hideIcon = false,
|
||||||
|
multiple,
|
||||||
onChange,
|
onChange,
|
||||||
onClose,
|
onClose,
|
||||||
placeholder = "Project",
|
placeholder = "Project",
|
||||||
|
|
@ -99,8 +109,6 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||||
const filteredOptions =
|
const filteredOptions =
|
||||||
query === "" ? options : options?.filter((o) => o?.query.toLowerCase().includes(query.toLowerCase()));
|
query === "" ? options : options?.filter((o) => o?.query.toLowerCase().includes(query.toLowerCase()));
|
||||||
|
|
||||||
const selectedProject = value ? getProjectById(value) : null;
|
|
||||||
|
|
||||||
const { handleClose, handleKeyDown, handleOnClick, searchInputKeyDown } = useDropdown({
|
const { handleClose, handleKeyDown, handleOnClick, searchInputKeyDown } = useDropdown({
|
||||||
dropdownRef,
|
dropdownRef,
|
||||||
inputRef,
|
inputRef,
|
||||||
|
|
@ -111,9 +119,40 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||||
setQuery,
|
setQuery,
|
||||||
});
|
});
|
||||||
|
|
||||||
const dropdownOnChange = (val: string) => {
|
const dropdownOnChange = (val: string & string[]) => {
|
||||||
onChange(val);
|
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 = (
|
const comboButton = (
|
||||||
|
|
@ -147,18 +186,14 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||||
className={buttonClassName}
|
className={buttonClassName}
|
||||||
isActive={isOpen}
|
isActive={isOpen}
|
||||||
tooltipHeading="Project"
|
tooltipHeading="Project"
|
||||||
tooltipContent={selectedProject?.name ?? placeholder}
|
tooltipContent={value?.length ? `${value.length} project${value.length !== 1 ? "s" : ""}` : placeholder}
|
||||||
showTooltip={showTooltip}
|
showTooltip={showTooltip}
|
||||||
variant={buttonVariant}
|
variant={buttonVariant}
|
||||||
renderToolTipByDefault={renderByDefault}
|
renderToolTipByDefault={renderByDefault}
|
||||||
>
|
>
|
||||||
{!hideIcon && selectedProject && (
|
{!hideIcon && getProjectIcon(value)}
|
||||||
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
|
|
||||||
<Logo logo={selectedProject.logo_props} size={12} />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
{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 && (
|
{dropdownArrow && (
|
||||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
<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}
|
onKeyDown={handleKeyDown}
|
||||||
button={comboButton}
|
button={comboButton}
|
||||||
renderByDefault={renderByDefault}
|
renderByDefault={renderByDefault}
|
||||||
|
multiple={multiple}
|
||||||
>
|
>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<Combobox.Options className="fixed z-10" static>
|
<Combobox.Options className="fixed z-10" static>
|
||||||
|
|
|
||||||
|
|
@ -16,28 +16,25 @@ interface RichTextEditorWrapperProps
|
||||||
extends Omit<IRichTextEditor, "disabledExtensions" | "fileHandler" | "mentionHandler"> {
|
extends Omit<IRichTextEditor, "disabledExtensions" | "fileHandler" | "mentionHandler"> {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
projectId: string;
|
memberIds: string[];
|
||||||
|
projectId?: string;
|
||||||
uploadFile: (file: File) => Promise<string>;
|
uploadFile: (file: File) => Promise<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProps>((props, ref) => {
|
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
|
// store hooks
|
||||||
const { data: currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
const {
|
const { getUserDetails } = useMember();
|
||||||
getUserDetails,
|
|
||||||
project: { getProjectMemberIds },
|
|
||||||
} = useMember();
|
|
||||||
// editor flaggings
|
// editor flaggings
|
||||||
const { richTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
|
const { richTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
|
||||||
// derived values
|
// derived values
|
||||||
const projectMemberIds = getProjectMemberIds(projectId);
|
const memberDetails = memberIds?.map((id) => getUserDetails(id) as IUserLite);
|
||||||
const projectMemberDetails = projectMemberIds?.map((id) => getUserDetails(id) as IUserLite);
|
|
||||||
// use-mention
|
// use-mention
|
||||||
const { mentionHighlights, mentionSuggestions } = useMention({
|
const { mentionHighlights, mentionSuggestions } = useMention({
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
projectId,
|
projectId,
|
||||||
members: projectMemberDetails,
|
members: memberDetails,
|
||||||
user: currentUser ?? undefined,
|
user: currentUser ?? undefined,
|
||||||
});
|
});
|
||||||
// file size
|
// file size
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
MoveRight,
|
MoveRight,
|
||||||
Copy,
|
Copy,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { TNameDescriptionLoader } from "@plane/types";
|
||||||
import { Button, ControlLink, CustomMenu, Row, TOAST_TYPE, setToast } from "@plane/ui";
|
import { Button, ControlLink, CustomMenu, Row, TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
|
|
@ -25,7 +26,7 @@ import {
|
||||||
InboxIssueStatus,
|
InboxIssueStatus,
|
||||||
SelectDuplicateInboxIssueModal,
|
SelectDuplicateInboxIssueModal,
|
||||||
} from "@/components/inbox";
|
} from "@/components/inbox";
|
||||||
import { CreateUpdateIssueModal, IssueUpdateStatus } from "@/components/issues";
|
import { CreateUpdateIssueModal, NameDescriptionUpdateStatus } from "@/components/issues";
|
||||||
// helpers
|
// helpers
|
||||||
import { findHowManyDaysLeft } from "@/helpers/date-time.helper";
|
import { findHowManyDaysLeft } from "@/helpers/date-time.helper";
|
||||||
import { EInboxIssueStatus } from "@/helpers/inbox.helper";
|
import { EInboxIssueStatus } from "@/helpers/inbox.helper";
|
||||||
|
|
@ -41,7 +42,7 @@ type TInboxIssueActionsHeader = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
inboxIssue: IInboxIssueStore | undefined;
|
inboxIssue: IInboxIssueStore | undefined;
|
||||||
isSubmitting: "submitting" | "submitted" | "saved";
|
isSubmitting: TNameDescriptionLoader;
|
||||||
isMobileSidebar: boolean;
|
isMobileSidebar: boolean;
|
||||||
setIsMobileSidebar: (value: boolean) => void;
|
setIsMobileSidebar: (value: boolean) => void;
|
||||||
isNotificationEmbed: boolean;
|
isNotificationEmbed: boolean;
|
||||||
|
|
@ -282,7 +283,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||||
)}
|
)}
|
||||||
<InboxIssueStatus inboxIssue={inboxIssue} iconSize={12} />
|
<InboxIssueStatus inboxIssue={inboxIssue} iconSize={12} />
|
||||||
<div className="flex items-center justify-end w-full">
|
<div className="flex items-center justify-end w-full">
|
||||||
<IssueUpdateStatus isSubmitting={isSubmitting} />
|
<NameDescriptionUpdateStatus isSubmitting={isSubmitting} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,11 @@ import {
|
||||||
PanelLeft,
|
PanelLeft,
|
||||||
MoveRight,
|
MoveRight,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { TNameDescriptionLoader } from "@plane/types";
|
||||||
import { Header, CustomMenu, EHeaderVariant } from "@plane/ui";
|
import { Header, CustomMenu, EHeaderVariant } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { InboxIssueStatus } from "@/components/inbox";
|
import { InboxIssueStatus } from "@/components/inbox";
|
||||||
import { IssueUpdateStatus } from "@/components/issues";
|
import { NameDescriptionUpdateStatus } from "@/components/issues";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
import { findHowManyDaysLeft } from "@/helpers/date-time.helper";
|
import { findHowManyDaysLeft } from "@/helpers/date-time.helper";
|
||||||
|
|
@ -30,7 +31,7 @@ import type { IInboxIssueStore } from "@/store/inbox/inbox-issue.store";
|
||||||
type Props = {
|
type Props = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
inboxIssue: IInboxIssueStore | undefined;
|
inboxIssue: IInboxIssueStore | undefined;
|
||||||
isSubmitting: "submitting" | "submitted" | "saved";
|
isSubmitting: TNameDescriptionLoader;
|
||||||
handleInboxIssueNavigation: (direction: "next" | "prev") => void;
|
handleInboxIssueNavigation: (direction: "next" | "prev") => void;
|
||||||
canMarkAsAccepted: boolean;
|
canMarkAsAccepted: boolean;
|
||||||
canMarkAsDeclined: boolean;
|
canMarkAsDeclined: boolean;
|
||||||
|
|
@ -117,7 +118,7 @@ export const InboxIssueActionsMobileHeader: React.FC<Props> = observer((props) =
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<InboxIssueStatus inboxIssue={inboxIssue} iconSize={12} />
|
<InboxIssueStatus inboxIssue={inboxIssue} iconSize={12} />
|
||||||
<div className="flex items-center justify-end w-full">
|
<div className="flex items-center justify-end w-full">
|
||||||
<IssueUpdateStatus isSubmitting={isSubmitting} />
|
<NameDescriptionUpdateStatus isSubmitting={isSubmitting} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto">
|
<div className="ml-auto">
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { Dispatch, SetStateAction, useEffect, useMemo } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
// plane types
|
// plane types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue, TNameDescriptionLoader } from "@plane/types";
|
||||||
// plane ui
|
// plane ui
|
||||||
import { Loader, TOAST_TYPE, setToast } from "@plane/ui";
|
import { Loader, TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
|
|
@ -34,8 +34,8 @@ type Props = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
inboxIssue: IInboxIssueStore;
|
inboxIssue: IInboxIssueStore;
|
||||||
isEditable: boolean;
|
isEditable: boolean;
|
||||||
isSubmitting: "submitting" | "submitted" | "saved";
|
isSubmitting: TNameDescriptionLoader;
|
||||||
setIsSubmitting: Dispatch<SetStateAction<"submitting" | "submitted" | "saved">>;
|
setIsSubmitting: Dispatch<SetStateAction<TNameDescriptionLoader>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { FC, useEffect, useState } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { TNameDescriptionLoader } from "@plane/types";
|
||||||
// components
|
// components
|
||||||
import { ContentWrapper } from "@plane/ui";
|
import { ContentWrapper } from "@plane/ui";
|
||||||
import { InboxIssueActionsHeader, InboxIssueMainContent } from "@/components/inbox";
|
import { InboxIssueActionsHeader, InboxIssueMainContent } from "@/components/inbox";
|
||||||
|
|
@ -32,7 +33,7 @@ export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
|
||||||
/// router
|
/// router
|
||||||
const router = useAppRouter();
|
const router = useAppRouter();
|
||||||
// states
|
// states
|
||||||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
const [isSubmitting, setIsSubmitting] = useState<TNameDescriptionLoader>("saved");
|
||||||
// hooks
|
// hooks
|
||||||
const { data: currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
const { currentTab, fetchInboxIssueById, getIssueInboxByIssueId, getIsIssueAvailable } = useProjectInbox();
|
const { currentTab, fetchInboxIssueById, getIssueInboxByIssueId, getIsIssueAvailable } = useProjectInbox();
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import { ETabIndices } from "@/constants/tab-indices";
|
||||||
import { getDescriptionPlaceholder } from "@/helpers/issue.helper";
|
import { getDescriptionPlaceholder } from "@/helpers/issue.helper";
|
||||||
import { getTabIndex } from "@/helpers/tab-indices.helper";
|
import { getTabIndex } from "@/helpers/tab-indices.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useProjectInbox } from "@/hooks/store";
|
import { useMember, useProjectInbox } from "@/hooks/store";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
// services
|
// services
|
||||||
import { FileService } from "@/services/file.service";
|
import { FileService } from "@/services/file.service";
|
||||||
|
|
@ -51,6 +51,11 @@ export const InboxIssueDescription: FC<TInboxIssueDescription> = observer((props
|
||||||
// hooks
|
// hooks
|
||||||
const { loader } = useProjectInbox();
|
const { loader } = useProjectInbox();
|
||||||
const { isMobile } = usePlatformOS();
|
const { isMobile } = usePlatformOS();
|
||||||
|
const {
|
||||||
|
project: { getProjectMemberIds },
|
||||||
|
} = useMember();
|
||||||
|
// derived values
|
||||||
|
const memberIds = getProjectMemberIds(projectId) ?? [];
|
||||||
|
|
||||||
const { getIndex } = getTabIndex(ETabIndices.INTAKE_ISSUE_FORM, isMobile);
|
const { getIndex } = getTabIndex(ETabIndices.INTAKE_ISSUE_FORM, isMobile);
|
||||||
|
|
||||||
|
|
@ -68,6 +73,7 @@ export const InboxIssueDescription: FC<TInboxIssueDescription> = observer((props
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
|
memberIds={memberIds}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
dragDropEnabled={false}
|
dragDropEnabled={false}
|
||||||
onChange={(_description: object, description_html: string) => handleData("description_html", description_html)}
|
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 { observer } from "mobx-react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue, TNameDescriptionLoader } from "@plane/types";
|
||||||
import { EFileAssetType } from "@plane/types/src/enums";
|
import { EFileAssetType } from "@plane/types/src/enums";
|
||||||
// ui
|
// ui
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
|
|
@ -15,7 +15,7 @@ import { TIssueOperations } from "@/components/issues/issue-detail";
|
||||||
// helpers
|
// helpers
|
||||||
import { getDescriptionPlaceholder } from "@/helpers/issue.helper";
|
import { getDescriptionPlaceholder } from "@/helpers/issue.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useWorkspace } from "@/hooks/store";
|
import { useMember, useWorkspace } from "@/hooks/store";
|
||||||
// services
|
// services
|
||||||
import { FileService } from "@/services/file.service";
|
import { FileService } from "@/services/file.service";
|
||||||
const fileService = new FileService();
|
const fileService = new FileService();
|
||||||
|
|
@ -29,7 +29,7 @@ export type IssueDescriptionInputProps = {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
issueOperations: TIssueOperations;
|
issueOperations: TIssueOperations;
|
||||||
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
||||||
setIsSubmitting: (initialValue: "submitting" | "submitted" | "saved") => void;
|
setIsSubmitting: (initialValue: TNameDescriptionLoader) => void;
|
||||||
swrIssueDescription?: string | null | undefined;
|
swrIssueDescription?: string | null | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -46,6 +46,12 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
|
||||||
setIsSubmitting,
|
setIsSubmitting,
|
||||||
placeholder,
|
placeholder,
|
||||||
} = props;
|
} = props;
|
||||||
|
// store hooks
|
||||||
|
const {
|
||||||
|
project: { getProjectMemberIds },
|
||||||
|
} = useMember();
|
||||||
|
// derived values
|
||||||
|
const memberIds = getProjectMemberIds(projectId) ?? [];
|
||||||
|
|
||||||
const { handleSubmit, reset, control } = useForm<TIssue>({
|
const { handleSubmit, reset, control } = useForm<TIssue>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
|
@ -108,6 +114,7 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
|
||||||
value={swrIssueDescription ?? null}
|
value={swrIssueDescription ?? null}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
|
memberIds={memberIds}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
dragDropEnabled
|
dragDropEnabled
|
||||||
onChange={(_description: object, description_html: string) => {
|
onChange={(_description: object, description_html: string) => {
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { TNameDescriptionLoader } from "@plane/types";
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
IssueActivity,
|
IssueActivity,
|
||||||
IssueUpdateStatus,
|
NameDescriptionUpdateStatus,
|
||||||
IssueReaction,
|
IssueReaction,
|
||||||
IssueParentDetail,
|
IssueParentDetail,
|
||||||
IssueTitleInput,
|
IssueTitleInput,
|
||||||
|
|
@ -38,7 +39,7 @@ type Props = {
|
||||||
export const IssueMainContent: React.FC<Props> = observer((props) => {
|
export const IssueMainContent: React.FC<Props> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, issueId, issueOperations, isEditable, isArchived } = props;
|
const { workspaceSlug, projectId, issueId, issueOperations, isEditable, isArchived } = props;
|
||||||
// states
|
// states
|
||||||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
const [isSubmitting, setIsSubmitting] = useState<TNameDescriptionLoader>("saved");
|
||||||
// hooks
|
// hooks
|
||||||
const windowSize = useSize();
|
const windowSize = useSize();
|
||||||
const { data: currentUser } = useUser();
|
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">
|
<div className="mb-2.5 flex items-center justify-between gap-4">
|
||||||
<IssueTypeSwitcher issueId={issueId} disabled={isArchived || !isEditable} />
|
<IssueTypeSwitcher issueId={issueId} disabled={isArchived || !isEditable} />
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<IssueUpdateStatus isSubmitting={isSubmitting} />
|
<NameDescriptionUpdateStatus isSubmitting={isSubmitting} />
|
||||||
{duplicateIssues?.length > 0 && (
|
{duplicateIssues?.length > 0 && (
|
||||||
<DeDupeIssuePopoverRoot
|
<DeDupeIssuePopoverRoot
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,9 @@ import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||||
import { getFileURL } from "@/helpers/file.helper";
|
import { getFileURL } from "@/helpers/file.helper";
|
||||||
// store
|
// store
|
||||||
import { store } from "@/lib/store-context";
|
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";
|
import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/base-issues.store";
|
||||||
|
|
||||||
export const HIGHLIGHT_CLASS = "highlight";
|
export const HIGHLIGHT_CLASS = "highlight";
|
||||||
|
|
@ -100,6 +103,7 @@ export const getGroupByColumns = ({
|
||||||
labels: () => getLabelsColumns(isWorkspaceLevel),
|
labels: () => getLabelsColumns(isWorkspaceLevel),
|
||||||
assignees: getAssigneeColumns,
|
assignees: getAssigneeColumns,
|
||||||
created_by: getCreatedByColumns,
|
created_by: getCreatedByColumns,
|
||||||
|
team_project: getTeamProjectColumns,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get and return the columns for the specified group by option
|
// Get and return the columns for the specified group by option
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import { ETabIndices } from "@/constants/tab-indices";
|
||||||
import { getDescriptionPlaceholder } from "@/helpers/issue.helper";
|
import { getDescriptionPlaceholder } from "@/helpers/issue.helper";
|
||||||
import { getTabIndex } from "@/helpers/tab-indices.helper";
|
import { getTabIndex } from "@/helpers/tab-indices.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useInstance, useWorkspace } from "@/hooks/store";
|
import { useInstance, useMember, useWorkspace } from "@/hooks/store";
|
||||||
import useKeypress from "@/hooks/use-keypress";
|
import useKeypress from "@/hooks/use-keypress";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
// services
|
// services
|
||||||
|
|
@ -76,6 +76,11 @@ export const IssueDescriptionEditor: React.FC<TIssueDescriptionEditorProps> = ob
|
||||||
const workspaceId = getWorkspaceBySlug(workspaceSlug?.toString())?.id as string;
|
const workspaceId = getWorkspaceBySlug(workspaceSlug?.toString())?.id as string;
|
||||||
const { config } = useInstance();
|
const { config } = useInstance();
|
||||||
const { isMobile } = usePlatformOS();
|
const { isMobile } = usePlatformOS();
|
||||||
|
const {
|
||||||
|
project: { getProjectMemberIds },
|
||||||
|
} = useMember();
|
||||||
|
// derived values
|
||||||
|
const memberIds = projectId ? (getProjectMemberIds(projectId) ?? []) : [];
|
||||||
|
|
||||||
const { getIndex } = getTabIndex(ETabIndices.ISSUE_FORM, isMobile);
|
const { getIndex } = getTabIndex(ETabIndices.ISSUE_FORM, isMobile);
|
||||||
|
|
||||||
|
|
@ -179,6 +184,7 @@ export const IssueDescriptionEditor: React.FC<TIssueDescriptionEditorProps> = ob
|
||||||
value={descriptionHtmlData}
|
value={descriptionHtmlData}
|
||||||
workspaceSlug={workspaceSlug?.toString() as string}
|
workspaceSlug={workspaceSlug?.toString() as string}
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
|
memberIds={memberIds}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
onChange={(_description: object, description_html: string) => {
|
onChange={(_description: object, description_html: string) => {
|
||||||
onChange(description_html);
|
onChange(description_html);
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ export const IssueProjectSelect: React.FC<TIssueProjectSelectProps> = observer((
|
||||||
onChange(projectId);
|
onChange(projectId);
|
||||||
handleFormChange();
|
handleFormChange();
|
||||||
}}
|
}}
|
||||||
|
multiple={false}
|
||||||
buttonVariant="border-with-text"
|
buttonVariant="border-with-text"
|
||||||
renderCondition={(project) => shouldRenderProject(project)}
|
renderCondition={(project) => shouldRenderProject(project)}
|
||||||
tabIndex={getIndex("project_id")}
|
tabIndex={getIndex("project_id")}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { RefreshCw } from "lucide-react";
|
import { RefreshCw } from "lucide-react";
|
||||||
|
// types
|
||||||
|
import { TNameDescriptionLoader } from "@plane/types";
|
||||||
|
|
||||||
type Props = {
|
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;
|
const { isSubmitting } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -17,7 +19,7 @@ export const IssueUpdateStatus: React.FC<Props> = observer((props) => {
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isSubmitting !== "submitted" && isSubmitting !== "saved" && (
|
{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>
|
<span className="text-sm text-custom-text-300">{isSubmitting === "submitting" ? "Saving..." : "Saved"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import { FC } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ArchiveRestoreIcon, Link2, MoveDiagonal, MoveRight, Trash2 } from "lucide-react";
|
import { ArchiveRestoreIcon, Link2, MoveDiagonal, MoveRight, Trash2 } from "lucide-react";
|
||||||
|
// types
|
||||||
|
import { TNameDescriptionLoader } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import {
|
import {
|
||||||
ArchiveIcon,
|
ArchiveIcon,
|
||||||
|
|
@ -16,7 +18,7 @@ import {
|
||||||
setToast,
|
setToast,
|
||||||
} from "@plane/ui";
|
} from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { IssueSubscription, IssueUpdateStatus } from "@/components/issues";
|
import { IssueSubscription, NameDescriptionUpdateStatus } from "@/components/issues";
|
||||||
import { ARCHIVABLE_STATE_GROUPS } from "@/constants/state";
|
import { ARCHIVABLE_STATE_GROUPS } from "@/constants/state";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
|
|
@ -58,7 +60,7 @@ export type PeekOverviewHeaderProps = {
|
||||||
toggleDeleteIssueModal: (issueId: string | null) => void;
|
toggleDeleteIssueModal: (issueId: string | null) => void;
|
||||||
toggleArchiveIssueModal: (issueId: string | null) => void;
|
toggleArchiveIssueModal: (issueId: string | null) => void;
|
||||||
handleRestoreIssue: () => void;
|
handleRestoreIssue: () => void;
|
||||||
isSubmitting: "submitting" | "submitted" | "saved";
|
isSubmitting: TNameDescriptionLoader;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((props) => {
|
export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((props) => {
|
||||||
|
|
@ -157,7 +159,7 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-x-4">
|
<div className="flex items-center gap-x-4">
|
||||||
<IssueUpdateStatus isSubmitting={isSubmitting} />
|
<NameDescriptionUpdateStatus isSubmitting={isSubmitting} />
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{currentUser && !isArchived && (
|
{currentUser && !isArchived && (
|
||||||
<IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
<IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ export const ModuleForm: React.FC<Props> = (props) => {
|
||||||
onChange(val);
|
onChange(val);
|
||||||
setActiveProject(val);
|
setActiveProject(val);
|
||||||
}}
|
}}
|
||||||
|
multiple={false}
|
||||||
buttonVariant="border-with-text"
|
buttonVariant="border-with-text"
|
||||||
renderCondition={(project) => shouldRenderProject(project)}
|
renderCondition={(project) => shouldRenderProject(project)}
|
||||||
tabIndex={getIndex("cover_image")}
|
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 AllFiltersImage from "@/public/empty-state/project/all-filters.svg";
|
||||||
import NameFilterImage from "@/public/empty-state/project/name-filter.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
|
// store hooks
|
||||||
const { toggleCreateProjectModal } = useCommandPalette();
|
const { toggleCreateProjectModal } = useCommandPalette();
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement } = useEventTracker();
|
||||||
const { workspaceProjectIds, filteredProjectIds, getProjectById, loader } = useProject();
|
const {
|
||||||
|
workspaceProjectIds: storeWorkspaceProjectIds,
|
||||||
|
filteredProjectIds: storeFilteredProjectIds,
|
||||||
|
getProjectById,
|
||||||
|
loader,
|
||||||
|
} = useProject();
|
||||||
const { searchQuery, currentWorkspaceDisplayFilters } = useProjectFilter();
|
const { searchQuery, currentWorkspaceDisplayFilters } = useProjectFilter();
|
||||||
|
// derived values
|
||||||
|
const workspaceProjectIds = totalProjectIdsProps ?? storeWorkspaceProjectIds;
|
||||||
|
const filteredProjectIds = filteredProjectIdsProps ?? storeFilteredProjectIds;
|
||||||
|
|
||||||
if (!filteredProjectIds || !workspaceProjectIds || loader) return <ProjectsLoader />;
|
if (!filteredProjectIds || !workspaceProjectIds || loader) return <ProjectsLoader />;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,27 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { Search, Briefcase, X } from "lucide-react";
|
import { Briefcase } from "lucide-react";
|
||||||
// plane helpers
|
|
||||||
import { useOutsideClickDetector } from "@plane/hooks";
|
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button, Header } from "@plane/ui";
|
import { Breadcrumbs, Button, Header } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink } from "@/components/common";
|
import { BreadcrumbLink } from "@/components/common";
|
||||||
// helpers
|
|
||||||
import { cn } from "@/helpers/common.helper";
|
|
||||||
// hooks
|
// 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";
|
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||||
|
// components
|
||||||
import HeaderFilters from "./filters";
|
import HeaderFilters from "./filters";
|
||||||
|
import { ProjectSearch } from "./search-projects";
|
||||||
|
|
||||||
export const ProjectsBaseHeader = observer(() => {
|
export const ProjectsBaseHeader = observer(() => {
|
||||||
// states
|
|
||||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
|
||||||
// refs
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
// store hooks
|
// store hooks
|
||||||
const { toggleCreateProjectModal } = useCommandPalette();
|
const { toggleCreateProjectModal } = useCommandPalette();
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement } = useEventTracker();
|
||||||
const { allowPermissions } = useUserPermissions();
|
const { allowPermissions } = useUserPermissions();
|
||||||
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
const { searchQuery, updateSearchQuery } = useProjectFilter();
|
|
||||||
|
|
||||||
// outside click detector hook
|
|
||||||
useOutsideClickDetector(inputRef, () => {
|
|
||||||
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
|
|
||||||
});
|
|
||||||
// auth
|
// auth
|
||||||
const isAuthorizedUser = allowPermissions(
|
const isAuthorizedUser = allowPermissions(
|
||||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||||
|
|
@ -42,17 +29,6 @@ export const ProjectsBaseHeader = observer(() => {
|
||||||
);
|
);
|
||||||
const isArchived = pathname.includes("/archives");
|
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 (
|
return (
|
||||||
<Header>
|
<Header>
|
||||||
<Header.LeftItem>
|
<Header.LeftItem>
|
||||||
|
|
@ -65,51 +41,7 @@ export const ProjectsBaseHeader = observer(() => {
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
</Header.LeftItem>
|
</Header.LeftItem>
|
||||||
<Header.RightItem>
|
<Header.RightItem>
|
||||||
<div className="flex items-center">
|
<ProjectSearch />
|
||||||
{!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>
|
|
||||||
|
|
||||||
<div className="hidden md:flex">
|
<div className="hidden md:flex">
|
||||||
<HeaderFilters />
|
<HeaderFilters />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -18,4 +18,6 @@ export * from "./member-list-item";
|
||||||
export * from "./project-settings-member-defaults";
|
export * from "./project-settings-member-defaults";
|
||||||
export * from "./send-project-invitation-modal";
|
export * from "./send-project-invitation-modal";
|
||||||
export * from "./confirm-project-member-remove";
|
export * from "./confirm-project-member-remove";
|
||||||
|
export * from "./multi-select-modal";
|
||||||
|
export * from "./search-projects";
|
||||||
export * from "@/plane-web/components/projects/create/root";
|
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
|
// components
|
||||||
import { SidebarNavItem } from "@/components/sidebar";
|
import { SidebarNavItem } from "@/components/sidebar";
|
||||||
// constants
|
// constants
|
||||||
import { SIDEBAR_WORKSPACE_MENU_ITEMS } from "@/constants/dashboard";
|
|
||||||
import { SIDEBAR_CLICKED } from "@/constants/event-tracker";
|
import { SIDEBAR_CLICKED } from "@/constants/event-tracker";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
|
|
@ -23,22 +22,26 @@ import useLocalStorage from "@/hooks/use-local-storage";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
// plane web components
|
// plane web components
|
||||||
import { UpgradeBadge } from "@/plane-web/components/workspace";
|
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";
|
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||||
|
// plane web hooks
|
||||||
|
import { isWorkspaceFeatureEnabled } from "@/plane-web/helpers/dashboard.helper";
|
||||||
|
|
||||||
export const SidebarWorkspaceMenu = observer(() => {
|
export const SidebarWorkspaceMenu = observer(() => {
|
||||||
// state
|
// state
|
||||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||||
// refs
|
// refs
|
||||||
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
// router params
|
||||||
|
const { workspaceSlug } = useParams();
|
||||||
|
// pathname
|
||||||
|
const pathname = usePathname();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
|
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
|
||||||
const { captureEvent } = useEventTracker();
|
const { captureEvent } = useEventTracker();
|
||||||
const { isMobile } = usePlatformOS();
|
const { isMobile } = usePlatformOS();
|
||||||
const { allowPermissions } = useUserPermissions();
|
const { allowPermissions } = useUserPermissions();
|
||||||
// router params
|
|
||||||
const { workspaceSlug } = useParams();
|
|
||||||
// pathname
|
|
||||||
const pathname = usePathname();
|
|
||||||
// local storage
|
// local storage
|
||||||
const { setValue: toggleWorkspaceMenu, storedValue } = useLocalStorage<boolean>("is_workspace_menu_open", true);
|
const { setValue: toggleWorkspaceMenu, storedValue } = useLocalStorage<boolean>("is_workspace_menu_open", true);
|
||||||
// derived values
|
// derived values
|
||||||
|
|
@ -158,8 +161,9 @@ export const SidebarWorkspaceMenu = observer(() => {
|
||||||
})}
|
})}
|
||||||
static
|
static
|
||||||
>
|
>
|
||||||
{SIDEBAR_WORKSPACE_MENU_ITEMS.map(
|
{SIDEBAR_WORKSPACE_MENU_ITEMS.map((link) => {
|
||||||
(link) =>
|
if (!isWorkspaceFeatureEnabled(link.key, workspaceSlug.toString())) return null;
|
||||||
|
return (
|
||||||
allowPermissions(link.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) && (
|
allowPermissions(link.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
key={link.key}
|
key={link.key}
|
||||||
|
|
@ -188,7 +192,8 @@ export const SidebarWorkspaceMenu = observer(() => {
|
||||||
</Link>
|
</Link>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
)}
|
);
|
||||||
|
})}
|
||||||
</Disclosure.Panel>
|
</Disclosure.Panel>
|
||||||
)}
|
)}
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,8 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { linearGradientDef } from "@nivo/core";
|
import { linearGradientDef } from "@nivo/core";
|
||||||
// icons
|
|
||||||
import { BarChart2, Briefcase, Layers } from "lucide-react";
|
|
||||||
// types
|
// types
|
||||||
import { TIssuesListTypes, TStateGroups } from "@plane/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
|
// assets
|
||||||
import CompletedIssuesDark from "@/public/empty-state/dashboard/dark/completed-issues.svg";
|
import CompletedIssuesDark from "@/public/empty-state/dashboard/dark/completed-issues.svg";
|
||||||
import OverdueIssuesDark from "@/public/empty-state/dashboard/dark/overdue-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 = {
|
export type TLinkOptions = {
|
||||||
userId: string | undefined;
|
userId: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
TIssuePriorities,
|
TIssuePriorities,
|
||||||
TIssueGroupingFilters,
|
TIssueGroupingFilters,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
|
import { ADDITIONAL_ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/plane-web/constants";
|
||||||
|
|
||||||
export const DRAG_ALLOWED_GROUPS: TIssueGroupByOptions[] = [
|
export const DRAG_ALLOWED_GROUPS: TIssueGroupByOptions[] = [
|
||||||
"state",
|
"state",
|
||||||
|
|
@ -23,9 +24,11 @@ export const DRAG_ALLOWED_GROUPS: TIssueGroupByOptions[] = [
|
||||||
export enum EIssuesStoreType {
|
export enum EIssuesStoreType {
|
||||||
GLOBAL = "GLOBAL",
|
GLOBAL = "GLOBAL",
|
||||||
PROFILE = "PROFILE",
|
PROFILE = "PROFILE",
|
||||||
|
TEAM = "TEAM",
|
||||||
PROJECT = "PROJECT",
|
PROJECT = "PROJECT",
|
||||||
CYCLE = "CYCLE",
|
CYCLE = "CYCLE",
|
||||||
MODULE = "MODULE",
|
MODULE = "MODULE",
|
||||||
|
TEAM_VIEW = "TEAM_VIEW",
|
||||||
PROJECT_VIEW = "PROJECT_VIEW",
|
PROJECT_VIEW = "PROJECT_VIEW",
|
||||||
ARCHIVED = "ARCHIVED",
|
ARCHIVED = "ARCHIVED",
|
||||||
DRAFT = "DRAFT",
|
DRAFT = "DRAFT",
|
||||||
|
|
@ -42,7 +45,9 @@ export enum EIssueLayoutTypes {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TCreateModalStoreTypes =
|
export type TCreateModalStoreTypes =
|
||||||
|
| EIssuesStoreType.TEAM
|
||||||
| EIssuesStoreType.PROJECT
|
| EIssuesStoreType.PROJECT
|
||||||
|
| EIssuesStoreType.TEAM_VIEW
|
||||||
| EIssuesStoreType.PROJECT_VIEW
|
| EIssuesStoreType.PROJECT_VIEW
|
||||||
| EIssuesStoreType.PROFILE
|
| EIssuesStoreType.PROFILE
|
||||||
| EIssuesStoreType.CYCLE
|
| EIssuesStoreType.CYCLE
|
||||||
|
|
@ -78,6 +83,7 @@ export const ISSUE_GROUP_BY_OPTIONS: {
|
||||||
{ key: "state", title: "States" },
|
{ key: "state", title: "States" },
|
||||||
{ key: "state_detail.group", title: "State Groups" },
|
{ key: "state_detail.group", title: "State Groups" },
|
||||||
{ key: "priority", title: "Priority" },
|
{ 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: "project", title: "Project" }, // required this on my issues
|
||||||
{ key: "cycle", title: "Cycle" }, // required this on my issues
|
{ key: "cycle", title: "Cycle" }, // required this on my issues
|
||||||
{ key: "module", title: "Module" }, // 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 {
|
export enum EIssueListRow {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import merge from "lodash/merge";
|
import merge from "lodash/merge";
|
||||||
// mobx store
|
|
||||||
import { TIssueMap } from "@plane/types";
|
import { TIssueMap } from "@plane/types";
|
||||||
|
// mobx store
|
||||||
import { EIssuesStoreType } from "@/constants/issue";
|
import { EIssuesStoreType } from "@/constants/issue";
|
||||||
import { StoreContext } from "@/lib/store-context";
|
import { StoreContext } from "@/lib/store-context";
|
||||||
// types
|
// 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 { IArchivedIssues, IArchivedIssuesFilter } from "@/store/issue/archived";
|
||||||
import { ICycleIssues, ICycleIssuesFilter } from "@/store/issue/cycle";
|
import { ICycleIssues, ICycleIssuesFilter } from "@/store/issue/cycle";
|
||||||
import { IDraftIssues, IDraftIssuesFilter } from "@/store/issue/draft";
|
import { IDraftIssues, IDraftIssuesFilter } from "@/store/issue/draft";
|
||||||
|
|
@ -33,6 +35,10 @@ export type TStoreIssues = {
|
||||||
issues: IProfileIssues;
|
issues: IProfileIssues;
|
||||||
issuesFilter: IProfileIssuesFilter;
|
issuesFilter: IProfileIssuesFilter;
|
||||||
};
|
};
|
||||||
|
[EIssuesStoreType.TEAM]: defaultIssueStore & {
|
||||||
|
issues: ITeamIssues;
|
||||||
|
issuesFilter: ITeamIssuesFilter;
|
||||||
|
};
|
||||||
[EIssuesStoreType.PROJECT]: defaultIssueStore & {
|
[EIssuesStoreType.PROJECT]: defaultIssueStore & {
|
||||||
issues: IProjectIssues;
|
issues: IProjectIssues;
|
||||||
issuesFilter: IProjectIssuesFilter;
|
issuesFilter: IProjectIssuesFilter;
|
||||||
|
|
@ -45,6 +51,10 @@ export type TStoreIssues = {
|
||||||
issues: IModuleIssues;
|
issues: IModuleIssues;
|
||||||
issuesFilter: IModuleIssuesFilter;
|
issuesFilter: IModuleIssuesFilter;
|
||||||
};
|
};
|
||||||
|
[EIssuesStoreType.TEAM_VIEW]: defaultIssueStore & {
|
||||||
|
issues: ITeamViewIssues;
|
||||||
|
issuesFilter: ITeamViewIssuesFilter;
|
||||||
|
};
|
||||||
[EIssuesStoreType.PROJECT_VIEW]: defaultIssueStore & {
|
[EIssuesStoreType.PROJECT_VIEW]: defaultIssueStore & {
|
||||||
issues: IProjectViewIssues;
|
issues: IProjectViewIssues;
|
||||||
issuesFilter: IProjectViewIssuesFilter;
|
issuesFilter: IProjectViewIssuesFilter;
|
||||||
|
|
@ -82,16 +92,16 @@ export const useIssues = <T extends EIssuesStoreType>(storeType?: T): TStoreIssu
|
||||||
issues: context.issue.workspaceDraftIssues,
|
issues: context.issue.workspaceDraftIssues,
|
||||||
issuesFilter: context.issue.workspaceDraftIssuesFilter,
|
issuesFilter: context.issue.workspaceDraftIssuesFilter,
|
||||||
}) as TStoreIssues[T];
|
}) as TStoreIssues[T];
|
||||||
case EIssuesStoreType.WORKSPACE_DRAFT:
|
|
||||||
return merge(defaultStore, {
|
|
||||||
issues: context.issue.workspaceDraftIssues,
|
|
||||||
issuesFilter: context.issue.workspaceDraftIssuesFilter,
|
|
||||||
}) as TStoreIssues[T];
|
|
||||||
case EIssuesStoreType.PROFILE:
|
case EIssuesStoreType.PROFILE:
|
||||||
return merge(defaultStore, {
|
return merge(defaultStore, {
|
||||||
issues: context.issue.profileIssues,
|
issues: context.issue.profileIssues,
|
||||||
issuesFilter: context.issue.profileIssuesFilter,
|
issuesFilter: context.issue.profileIssuesFilter,
|
||||||
}) as TStoreIssues[T];
|
}) as TStoreIssues[T];
|
||||||
|
case EIssuesStoreType.TEAM:
|
||||||
|
return merge(defaultStore, {
|
||||||
|
issues: context.issue.teamIssues,
|
||||||
|
issuesFilter: context.issue.teamIssuesFilter,
|
||||||
|
}) as TStoreIssues[T];
|
||||||
case EIssuesStoreType.PROJECT:
|
case EIssuesStoreType.PROJECT:
|
||||||
return merge(defaultStore, {
|
return merge(defaultStore, {
|
||||||
issues: context.issue.projectIssues,
|
issues: context.issue.projectIssues,
|
||||||
|
|
@ -107,6 +117,11 @@ export const useIssues = <T extends EIssuesStoreType>(storeType?: T): TStoreIssu
|
||||||
issues: context.issue.moduleIssues,
|
issues: context.issue.moduleIssues,
|
||||||
issuesFilter: context.issue.moduleIssuesFilter,
|
issuesFilter: context.issue.moduleIssuesFilter,
|
||||||
}) as TStoreIssues[T];
|
}) 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:
|
case EIssuesStoreType.PROJECT_VIEW:
|
||||||
return merge(defaultStore, {
|
return merge(defaultStore, {
|
||||||
issues: context.issue.projectViewIssues,
|
issues: context.issue.projectViewIssues,
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,9 @@ type DNDStoreType =
|
||||||
| EIssuesStoreType.DRAFT
|
| EIssuesStoreType.DRAFT
|
||||||
| EIssuesStoreType.PROFILE
|
| EIssuesStoreType.PROFILE
|
||||||
| EIssuesStoreType.ARCHIVED
|
| EIssuesStoreType.ARCHIVED
|
||||||
| EIssuesStoreType.WORKSPACE_DRAFT;
|
| EIssuesStoreType.WORKSPACE_DRAFT
|
||||||
|
| EIssuesStoreType.TEAM
|
||||||
|
| EIssuesStoreType.TEAM_VIEW;
|
||||||
|
|
||||||
export const useGroupIssuesDragNDrop = (
|
export const useGroupIssuesDragNDrop = (
|
||||||
storeType: DNDStoreType,
|
storeType: DNDStoreType,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export const IssuesStoreContext = createContext<EIssuesStoreType | undefined>(un
|
||||||
export const useIssueStoreType = () => {
|
export const useIssueStoreType = () => {
|
||||||
const storeType = useContext(IssuesStoreContext);
|
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 store type exists in context, use that store type
|
||||||
if (storeType) return storeType;
|
if (storeType) return storeType;
|
||||||
|
|
@ -26,6 +26,10 @@ export const useIssueStoreType = () => {
|
||||||
|
|
||||||
if (projectId) return EIssuesStoreType.PROJECT;
|
if (projectId) return EIssuesStoreType.PROJECT;
|
||||||
|
|
||||||
|
if (teamId) return EIssuesStoreType.TEAM;
|
||||||
|
|
||||||
|
if (teamId && viewId) return EIssuesStoreType.TEAM_VIEW;
|
||||||
|
|
||||||
return EIssuesStoreType.PROJECT;
|
return EIssuesStoreType.PROJECT;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,10 @@ import {
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
|
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
|
||||||
import { EDraftIssuePaginationType } from "@/constants/workspace-drafts";
|
import { EDraftIssuePaginationType } from "@/constants/workspace-drafts";
|
||||||
|
import { useTeamIssueActions, useTeamViewIssueActions } from "@/plane-web/helpers/issue-action-helper";
|
||||||
import { useIssues } from "./store";
|
import { useIssues } from "./store";
|
||||||
|
|
||||||
interface IssueActions {
|
export interface IssueActions {
|
||||||
fetchIssues: (
|
fetchIssues: (
|
||||||
loadType: TLoader,
|
loadType: TLoader,
|
||||||
options: IssuePaginationOptions,
|
options: IssuePaginationOptions,
|
||||||
|
|
@ -38,9 +39,11 @@ interface IssueActions {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useIssuesActions = (storeType: EIssuesStoreType): IssueActions => {
|
export const useIssuesActions = (storeType: EIssuesStoreType): IssueActions => {
|
||||||
|
const teamIssueActions = useTeamIssueActions();
|
||||||
const projectIssueActions = useProjectIssueActions();
|
const projectIssueActions = useProjectIssueActions();
|
||||||
const cycleIssueActions = useCycleIssueActions();
|
const cycleIssueActions = useCycleIssueActions();
|
||||||
const moduleIssueActions = useModuleIssueActions();
|
const moduleIssueActions = useModuleIssueActions();
|
||||||
|
const teamViewIssueActions = useTeamViewIssueActions();
|
||||||
const projectViewIssueActions = useProjectViewIssueActions();
|
const projectViewIssueActions = useProjectViewIssueActions();
|
||||||
const globalIssueActions = useGlobalIssueActions();
|
const globalIssueActions = useGlobalIssueActions();
|
||||||
const profileIssueActions = useProfileIssueActions();
|
const profileIssueActions = useProfileIssueActions();
|
||||||
|
|
@ -49,10 +52,14 @@ export const useIssuesActions = (storeType: EIssuesStoreType): IssueActions => {
|
||||||
const workspaceDraftIssueActions = useWorkspaceDraftIssueActions();
|
const workspaceDraftIssueActions = useWorkspaceDraftIssueActions();
|
||||||
|
|
||||||
switch (storeType) {
|
switch (storeType) {
|
||||||
|
case EIssuesStoreType.TEAM_VIEW:
|
||||||
|
return teamViewIssueActions;
|
||||||
case EIssuesStoreType.PROJECT_VIEW:
|
case EIssuesStoreType.PROJECT_VIEW:
|
||||||
return projectViewIssueActions;
|
return projectViewIssueActions;
|
||||||
case EIssuesStoreType.PROFILE:
|
case EIssuesStoreType.PROFILE:
|
||||||
return profileIssueActions;
|
return profileIssueActions;
|
||||||
|
case EIssuesStoreType.TEAM:
|
||||||
|
return teamIssueActions;
|
||||||
case EIssuesStoreType.ARCHIVED:
|
case EIssuesStoreType.ARCHIVED:
|
||||||
return archivedIssueActions;
|
return archivedIssueActions;
|
||||||
case EIssuesStoreType.DRAFT:
|
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 useLocalStorage = <T,>(key: string, initialValue: T) => {
|
||||||
const [storedValue, setStoredValue] = useState<T | null>(() => getValueFromLocalStorage(key, initialValue));
|
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
|
// services
|
||||||
import { EIssuesStoreType, TCreateModalStoreTypes } from "@/constants/issue";
|
import { EIssuesStoreType, TCreateModalStoreTypes } from "@/constants/issue";
|
||||||
// types / constants
|
// types / constants
|
||||||
|
|
@ -22,8 +22,6 @@ export interface IBaseCommandPaletteStore {
|
||||||
isDeleteIssueModalOpen: boolean;
|
isDeleteIssueModalOpen: boolean;
|
||||||
isBulkDeleteIssueModalOpen: boolean;
|
isBulkDeleteIssueModalOpen: boolean;
|
||||||
createIssueStoreType: TCreateModalStoreTypes;
|
createIssueStoreType: TCreateModalStoreTypes;
|
||||||
// computed
|
|
||||||
isAnyModalOpen: boolean;
|
|
||||||
// toggle actions
|
// toggle actions
|
||||||
toggleCommandPaletteModal: (value?: boolean) => void;
|
toggleCommandPaletteModal: (value?: boolean) => void;
|
||||||
toggleShortcutModal: (value?: boolean) => void;
|
toggleShortcutModal: (value?: boolean) => void;
|
||||||
|
|
@ -65,8 +63,6 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor
|
||||||
isBulkDeleteIssueModalOpen: observable.ref,
|
isBulkDeleteIssueModalOpen: observable.ref,
|
||||||
createPageModal: observable,
|
createPageModal: observable,
|
||||||
createIssueStoreType: observable,
|
createIssueStoreType: observable,
|
||||||
// computed
|
|
||||||
isAnyModalOpen: computed,
|
|
||||||
// projectPages: computed,
|
// projectPages: computed,
|
||||||
// toggle actions
|
// toggle actions
|
||||||
toggleCommandPaletteModal: action,
|
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 whether any base modal is open
|
||||||
* @returns boolean
|
* @protected - allows access from child classes
|
||||||
*/
|
*/
|
||||||
get isAnyModalOpen() {
|
protected getCoreModalsState(): boolean {
|
||||||
return Boolean(
|
return Boolean(
|
||||||
this.isCreateIssueModalOpen ||
|
this.isCreateIssueModalOpen ||
|
||||||
this.isCreateCycleModalOpen ||
|
this.isCreateCycleModalOpen ||
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,7 @@ const ISSUE_GROUP_BY_KEY: Record<TIssueDisplayFilterOptions, keyof TIssue> = {
|
||||||
target_date: "target_date",
|
target_date: "target_date",
|
||||||
cycle: "cycle_id",
|
cycle: "cycle_id",
|
||||||
module: "module_ids",
|
module: "module_ids",
|
||||||
|
team_project: "project_id",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ISSUE_FILTER_DEFAULT_DATA: Record<TIssueDisplayFilterOptions, keyof TIssue> = {
|
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",
|
created_by: "created_by",
|
||||||
assignees: "assignee_ids",
|
assignees: "assignee_ids",
|
||||||
target_date: "target_date",
|
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
|
// 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,
|
start_date: filters?.start_date || undefined,
|
||||||
target_date: filters?.target_date || undefined,
|
target_date: filters?.target_date || undefined,
|
||||||
project: filters?.project || undefined,
|
project: filters?.project || undefined,
|
||||||
|
team_project: filters?.team_project || undefined,
|
||||||
subscriber: filters?.subscriber || undefined,
|
subscriber: filters?.subscriber || undefined,
|
||||||
issue_type: filters?.issue_type || undefined,
|
issue_type: filters?.issue_type || undefined,
|
||||||
// display filters
|
// display filters
|
||||||
|
|
@ -142,6 +143,7 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
|
||||||
start_date: filters?.start_date || null,
|
start_date: filters?.start_date || null,
|
||||||
target_date: filters?.target_date || null,
|
target_date: filters?.target_date || null,
|
||||||
project: filters?.project || null,
|
project: filters?.project || null,
|
||||||
|
team_project: filters?.team_project || null,
|
||||||
subscriber: filters?.subscriber || null,
|
subscriber: filters?.subscriber || null,
|
||||||
issue_type: filters?.issue_type || null,
|
issue_type: filters?.issue_type || null,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,17 @@
|
||||||
import isEmpty from "lodash/isEmpty";
|
import isEmpty from "lodash/isEmpty";
|
||||||
import { autorun, makeObservable, observable } from "mobx";
|
import { autorun, makeObservable, observable } from "mobx";
|
||||||
import { ICycle, IIssueLabel, IModule, IProject, IState, IUserLite } from "@plane/types";
|
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
|
// root store
|
||||||
|
import { RootStore } from "@/plane-web/store/root.store";
|
||||||
import { IWorkspaceMembership } from "@/store/member/workspace-member.store";
|
import { IWorkspaceMembership } from "@/store/member/workspace-member.store";
|
||||||
import { CoreRootStore } from "../root.store";
|
|
||||||
import { IStateStore, StateStore } from "../state.store";
|
import { IStateStore, StateStore } from "../state.store";
|
||||||
// issues data store
|
// issues data store
|
||||||
import { IArchivedIssuesFilter, ArchivedIssuesFilter, IArchivedIssues, ArchivedIssues } from "./archived";
|
import { IArchivedIssuesFilter, ArchivedIssuesFilter, IArchivedIssues, ArchivedIssues } from "./archived";
|
||||||
|
|
@ -33,6 +41,7 @@ import {
|
||||||
export interface IIssueRootStore {
|
export interface IIssueRootStore {
|
||||||
currentUserId: string | undefined;
|
currentUserId: string | undefined;
|
||||||
workspaceSlug: string | undefined;
|
workspaceSlug: string | undefined;
|
||||||
|
teamId: string | undefined;
|
||||||
projectId: string | undefined;
|
projectId: string | undefined;
|
||||||
cycleId: string | undefined;
|
cycleId: string | undefined;
|
||||||
moduleId: string | undefined;
|
moduleId: string | undefined;
|
||||||
|
|
@ -49,7 +58,7 @@ export interface IIssueRootStore {
|
||||||
moduleMap: Record<string, IModule> | undefined;
|
moduleMap: Record<string, IModule> | undefined;
|
||||||
cycleMap: Record<string, ICycle> | undefined;
|
cycleMap: Record<string, ICycle> | undefined;
|
||||||
|
|
||||||
rootStore: CoreRootStore;
|
rootStore: RootStore;
|
||||||
|
|
||||||
issues: IIssueStore;
|
issues: IIssueStore;
|
||||||
|
|
||||||
|
|
@ -64,6 +73,9 @@ export interface IIssueRootStore {
|
||||||
profileIssuesFilter: IProfileIssuesFilter;
|
profileIssuesFilter: IProfileIssuesFilter;
|
||||||
profileIssues: IProfileIssues;
|
profileIssues: IProfileIssues;
|
||||||
|
|
||||||
|
teamIssuesFilter: ITeamIssuesFilter;
|
||||||
|
teamIssues: ITeamIssues;
|
||||||
|
|
||||||
projectIssuesFilter: IProjectIssuesFilter;
|
projectIssuesFilter: IProjectIssuesFilter;
|
||||||
projectIssues: IProjectIssues;
|
projectIssues: IProjectIssues;
|
||||||
|
|
||||||
|
|
@ -73,6 +85,9 @@ export interface IIssueRootStore {
|
||||||
moduleIssuesFilter: IModuleIssuesFilter;
|
moduleIssuesFilter: IModuleIssuesFilter;
|
||||||
moduleIssues: IModuleIssues;
|
moduleIssues: IModuleIssues;
|
||||||
|
|
||||||
|
teamViewIssuesFilter: ITeamViewIssuesFilter;
|
||||||
|
teamViewIssues: ITeamViewIssues;
|
||||||
|
|
||||||
projectViewIssuesFilter: IProjectViewIssuesFilter;
|
projectViewIssuesFilter: IProjectViewIssuesFilter;
|
||||||
projectViewIssues: IProjectViewIssues;
|
projectViewIssues: IProjectViewIssues;
|
||||||
|
|
||||||
|
|
@ -89,6 +104,7 @@ export interface IIssueRootStore {
|
||||||
export class IssueRootStore implements IIssueRootStore {
|
export class IssueRootStore implements IIssueRootStore {
|
||||||
currentUserId: string | undefined = undefined;
|
currentUserId: string | undefined = undefined;
|
||||||
workspaceSlug: string | undefined = undefined;
|
workspaceSlug: string | undefined = undefined;
|
||||||
|
teamId: string | undefined = undefined;
|
||||||
projectId: string | undefined = undefined;
|
projectId: string | undefined = undefined;
|
||||||
cycleId: string | undefined = undefined;
|
cycleId: string | undefined = undefined;
|
||||||
moduleId: string | undefined = undefined;
|
moduleId: string | undefined = undefined;
|
||||||
|
|
@ -105,7 +121,7 @@ export class IssueRootStore implements IIssueRootStore {
|
||||||
moduleMap: Record<string, IModule> | undefined = undefined;
|
moduleMap: Record<string, IModule> | undefined = undefined;
|
||||||
cycleMap: Record<string, ICycle> | undefined = undefined;
|
cycleMap: Record<string, ICycle> | undefined = undefined;
|
||||||
|
|
||||||
rootStore: CoreRootStore;
|
rootStore: RootStore;
|
||||||
|
|
||||||
issues: IIssueStore;
|
issues: IIssueStore;
|
||||||
|
|
||||||
|
|
@ -120,6 +136,9 @@ export class IssueRootStore implements IIssueRootStore {
|
||||||
profileIssuesFilter: IProfileIssuesFilter;
|
profileIssuesFilter: IProfileIssuesFilter;
|
||||||
profileIssues: IProfileIssues;
|
profileIssues: IProfileIssues;
|
||||||
|
|
||||||
|
teamIssuesFilter: ITeamIssuesFilter;
|
||||||
|
teamIssues: ITeamIssues;
|
||||||
|
|
||||||
projectIssuesFilter: IProjectIssuesFilter;
|
projectIssuesFilter: IProjectIssuesFilter;
|
||||||
projectIssues: IProjectIssues;
|
projectIssues: IProjectIssues;
|
||||||
|
|
||||||
|
|
@ -129,6 +148,9 @@ export class IssueRootStore implements IIssueRootStore {
|
||||||
moduleIssuesFilter: IModuleIssuesFilter;
|
moduleIssuesFilter: IModuleIssuesFilter;
|
||||||
moduleIssues: IModuleIssues;
|
moduleIssues: IModuleIssues;
|
||||||
|
|
||||||
|
teamViewIssuesFilter: ITeamViewIssuesFilter;
|
||||||
|
teamViewIssues: ITeamViewIssues;
|
||||||
|
|
||||||
projectViewIssuesFilter: IProjectViewIssuesFilter;
|
projectViewIssuesFilter: IProjectViewIssuesFilter;
|
||||||
projectViewIssues: IProjectViewIssues;
|
projectViewIssues: IProjectViewIssues;
|
||||||
|
|
||||||
|
|
@ -141,9 +163,10 @@ export class IssueRootStore implements IIssueRootStore {
|
||||||
issueKanBanView: IIssueKanBanViewStore;
|
issueKanBanView: IIssueKanBanViewStore;
|
||||||
issueCalendarView: ICalendarStore;
|
issueCalendarView: ICalendarStore;
|
||||||
|
|
||||||
constructor(rootStore: CoreRootStore) {
|
constructor(rootStore: RootStore) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
workspaceSlug: observable.ref,
|
workspaceSlug: observable.ref,
|
||||||
|
teamId: observable.ref,
|
||||||
projectId: observable.ref,
|
projectId: observable.ref,
|
||||||
cycleId: observable.ref,
|
cycleId: observable.ref,
|
||||||
moduleId: observable.ref,
|
moduleId: observable.ref,
|
||||||
|
|
@ -166,6 +189,7 @@ export class IssueRootStore implements IIssueRootStore {
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
if (rootStore?.user?.data?.id) this.currentUserId = rootStore?.user?.data?.id;
|
if (rootStore?.user?.data?.id) this.currentUserId = rootStore?.user?.data?.id;
|
||||||
if (this.workspaceSlug !== rootStore.router.workspaceSlug) this.workspaceSlug = rootStore.router.workspaceSlug;
|
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.projectId !== rootStore.router.projectId) this.projectId = rootStore.router.projectId;
|
||||||
if (this.cycleId !== rootStore.router.cycleId) this.cycleId = rootStore.router.cycleId;
|
if (this.cycleId !== rootStore.router.cycleId) this.cycleId = rootStore.router.cycleId;
|
||||||
if (this.moduleId !== rootStore.router.moduleId) this.moduleId = rootStore.router.moduleId;
|
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.projectIssuesFilter = new ProjectIssuesFilter(this);
|
||||||
this.projectIssues = new ProjectIssues(this, this.projectIssuesFilter);
|
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.cycleIssuesFilter = new CycleIssuesFilter(this);
|
||||||
this.cycleIssues = new CycleIssues(this, this.cycleIssuesFilter);
|
this.cycleIssues = new CycleIssues(this, this.cycleIssuesFilter);
|
||||||
|
|
||||||
this.moduleIssuesFilter = new ModuleIssuesFilter(this);
|
this.moduleIssuesFilter = new ModuleIssuesFilter(this);
|
||||||
this.moduleIssues = new ModuleIssues(this, this.moduleIssuesFilter);
|
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.projectViewIssuesFilter = new ProjectViewIssuesFilter(this);
|
||||||
this.projectViewIssues = new ProjectViewIssues(this, this.projectViewIssuesFilter);
|
this.projectViewIssues = new ProjectViewIssues(this, this.projectViewIssuesFilter);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import set from "lodash/set";
|
import set from "lodash/set";
|
||||||
import { action, computed, makeObservable, observable, reaction, runInAction } from "mobx";
|
import { action, computed, makeObservable, observable, reaction, runInAction } from "mobx";
|
||||||
// types
|
// types
|
||||||
import { TDocumentPayload, TLogoProps, TPage } from "@plane/types";
|
import { TDocumentPayload, TLogoProps, TNameDescriptionLoader, TPage } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { EPageAccess } from "@/constants/page";
|
import { EPageAccess } from "@/constants/page";
|
||||||
import { EUserPermissions } from "@/plane-web/constants/user-permissions";
|
import { EUserPermissions } from "@/plane-web/constants/user-permissions";
|
||||||
|
|
@ -10,11 +10,9 @@ import { ProjectPageService } from "@/services/page";
|
||||||
// store
|
// store
|
||||||
import { CoreRootStore } from "../root.store";
|
import { CoreRootStore } from "../root.store";
|
||||||
|
|
||||||
export type TLoader = "submitting" | "submitted" | "saved";
|
|
||||||
|
|
||||||
export interface IPage extends TPage {
|
export interface IPage extends TPage {
|
||||||
// observables
|
// observables
|
||||||
isSubmitting: TLoader;
|
isSubmitting: TNameDescriptionLoader;
|
||||||
// computed
|
// computed
|
||||||
asJSON: TPage | undefined;
|
asJSON: TPage | undefined;
|
||||||
isCurrentUserOwner: boolean; // it will give the user is the owner of the page or not
|
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;
|
isContentEditable: boolean;
|
||||||
// helpers
|
// helpers
|
||||||
oldName: string;
|
oldName: string;
|
||||||
setIsSubmitting: (value: TLoader) => void;
|
setIsSubmitting: (value: TNameDescriptionLoader) => void;
|
||||||
cleanup: () => void;
|
cleanup: () => void;
|
||||||
// actions
|
// actions
|
||||||
update: (pageData: Partial<TPage>) => Promise<TPage | undefined>;
|
update: (pageData: Partial<TPage>) => Promise<TPage | undefined>;
|
||||||
|
|
@ -47,7 +45,7 @@ export interface IPage extends TPage {
|
||||||
|
|
||||||
export class Page implements IPage {
|
export class Page implements IPage {
|
||||||
// loaders
|
// loaders
|
||||||
isSubmitting: TLoader = "saved";
|
isSubmitting: TNameDescriptionLoader = "saved";
|
||||||
// page properties
|
// page properties
|
||||||
id: string | undefined;
|
id: string | undefined;
|
||||||
name: string | undefined;
|
name: string | undefined;
|
||||||
|
|
@ -324,7 +322,7 @@ export class Page implements IPage {
|
||||||
* @description update the submitting state
|
* @description update the submitting state
|
||||||
* @param value
|
* @param value
|
||||||
*/
|
*/
|
||||||
setIsSubmitting = (value: TLoader) => {
|
setIsSubmitting = (value: TNameDescriptionLoader) => {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.isSubmitting = value;
|
this.isSubmitting = value;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ export interface IRouterStore {
|
||||||
setQuery: (query: ParsedUrlQuery) => void;
|
setQuery: (query: ParsedUrlQuery) => void;
|
||||||
// computed
|
// computed
|
||||||
workspaceSlug: string | undefined;
|
workspaceSlug: string | undefined;
|
||||||
|
teamId: string | undefined;
|
||||||
projectId: string | undefined;
|
projectId: string | undefined;
|
||||||
cycleId: string | undefined;
|
cycleId: string | undefined;
|
||||||
moduleId: string | undefined;
|
moduleId: string | undefined;
|
||||||
|
|
@ -34,6 +35,7 @@ export class RouterStore implements IRouterStore {
|
||||||
setQuery: action.bound,
|
setQuery: action.bound,
|
||||||
//computed
|
//computed
|
||||||
workspaceSlug: computed,
|
workspaceSlug: computed,
|
||||||
|
teamId: computed,
|
||||||
projectId: computed,
|
projectId: computed,
|
||||||
cycleId: computed,
|
cycleId: computed,
|
||||||
moduleId: computed,
|
moduleId: computed,
|
||||||
|
|
@ -66,6 +68,14 @@ export class RouterStore implements IRouterStore {
|
||||||
return this.query?.workspaceSlug?.toString();
|
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 the project id from the query
|
||||||
* @returns string|undefined
|
* @returns string|undefined
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
export * from "./applied-filters";
|
export * from "./applied-filters";
|
||||||
export * from "./issue-types";
|
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 = (
|
export const handleIssueQueryParamsByLayout = (
|
||||||
layout: EIssueLayoutTypes | undefined,
|
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 => {
|
): TIssueParams[] | null => {
|
||||||
const queryParams: TIssueParams[] = [];
|
const queryParams: TIssueParams[] = [];
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue