From edbb9833bf69e1153dece3731af332740fa68af8 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Thu, 12 Dec 2024 03:06:14 +0530 Subject: [PATCH 01/17] feat: add navigation dropdown component --- packages/ui/src/breadcrumbs/index.ts | 1 + .../src/breadcrumbs/navigation-dropdown.tsx | 96 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 packages/ui/src/breadcrumbs/navigation-dropdown.tsx diff --git a/packages/ui/src/breadcrumbs/index.ts b/packages/ui/src/breadcrumbs/index.ts index 669f5575772..05a8bdbf1b6 100644 --- a/packages/ui/src/breadcrumbs/index.ts +++ b/packages/ui/src/breadcrumbs/index.ts @@ -1 +1,2 @@ export * from "./breadcrumbs"; +export * from "./navigation-dropdown"; diff --git a/packages/ui/src/breadcrumbs/navigation-dropdown.tsx b/packages/ui/src/breadcrumbs/navigation-dropdown.tsx new file mode 100644 index 00000000000..a716ca65e19 --- /dev/null +++ b/packages/ui/src/breadcrumbs/navigation-dropdown.tsx @@ -0,0 +1,96 @@ +"use client"; + +import * as React from "react"; +import { CheckIcon, ChevronDownIcon } from "lucide-react"; +// ui +import { CustomMenu, TContextMenuItem } from "../dropdowns"; +// helpers +import { cn } from "../../helpers"; + +type TBreadcrumbNavigationDropdownProps = { + selectedItemKey: string; + navigationItems: TContextMenuItem[]; + navigationDisabled?: boolean; +}; + +export const BreadcrumbNavigationDropdown = (props: TBreadcrumbNavigationDropdownProps) => { + const { selectedItemKey, navigationItems, navigationDisabled = false } = props; + // derived values + const selectedItem = navigationItems.find((item) => item.key === selectedItemKey); + const selectedItemIcon = selectedItem?.icon ? ( + + ) : undefined; + + // if no selected item, return null + if (!selectedItem) return null; + + const NavigationButton = ({ className }: { className?: string }) => ( +
  • + {selectedItemIcon && ( +
    {selectedItemIcon}
    + )} +
    {selectedItem.title}
    +
  • + ); + + if (navigationDisabled) { + return ; + } + + return ( + + + + + } + placement="bottom-start" + closeOnSelect + > + {navigationItems.map((item) => { + if (item.shouldRender === false) return null; + return ( + { + e.preventDefault(); + e.stopPropagation(); + if (item.key === selectedItemKey) return; + item.action(); + }} + className={cn( + "flex items-center gap-2", + { + "text-custom-text-400": item.disabled, + }, + item.className + )} + disabled={item.disabled} + > + {item.icon && } +
    +
    {item.title}
    + {item.description && ( +

    + {item.description} +

    + )} +
    + {item.key === selectedItemKey && } +
    + ); + })} +
    + ); +}; From 1ccbf6abc353f0fa824c53bfdad121da6b2b83c8 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Thu, 12 Dec 2024 03:15:22 +0530 Subject: [PATCH 02/17] chore: enhance title/ description loader and componenet modularity --- packages/types/src/common.d.ts | 2 ++ packages/types/src/enums.ts | 1 + .../components/inbox/content/inbox-issue-header.tsx | 7 ++++--- .../inbox/content/inbox-issue-mobile-header.tsx | 7 ++++--- web/core/components/inbox/content/issue-root.tsx | 6 +++--- web/core/components/inbox/content/root.tsx | 3 ++- web/core/components/issues/description-input.tsx | 13 ++++++++++--- web/core/components/issues/issue-update-status.tsx | 8 +++++--- web/core/store/pages/page.ts | 12 +++++------- 9 files changed, 36 insertions(+), 23 deletions(-) diff --git a/packages/types/src/common.d.ts b/packages/types/src/common.d.ts index 5fe31ad0006..7e755fcc28e 100644 --- a/packages/types/src/common.d.ts +++ b/packages/types/src/common.d.ts @@ -22,3 +22,5 @@ export type TLogoProps = { background_color?: string; }; }; + +export type TNameDescriptionLoader = "submitting" | "submitted" | "saved"; diff --git a/packages/types/src/enums.ts b/packages/types/src/enums.ts index df6a462b02e..e37e2f4a5d5 100644 --- a/packages/types/src/enums.ts +++ b/packages/types/src/enums.ts @@ -59,4 +59,5 @@ export enum EFileAssetType { USER_AVATAR = "USER_AVATAR", USER_COVER = "USER_COVER", WORKSPACE_LOGO = "WORKSPACE_LOGO", + TEAM_SPACE_DESCRIPTION = "TEAM_SPACE_DESCRIPTION", } diff --git a/web/core/components/inbox/content/inbox-issue-header.tsx b/web/core/components/inbox/content/inbox-issue-header.tsx index ad7d63ff0f4..cb0dbcfc5f1 100644 --- a/web/core/components/inbox/content/inbox-issue-header.tsx +++ b/web/core/components/inbox/content/inbox-issue-header.tsx @@ -15,6 +15,7 @@ import { MoveRight, Copy, } from "lucide-react"; +import { TNameDescriptionLoader } from "@plane/types"; import { Button, ControlLink, CustomMenu, Row, TOAST_TYPE, setToast } from "@plane/ui"; // components import { @@ -25,7 +26,7 @@ import { InboxIssueStatus, SelectDuplicateInboxIssueModal, } from "@/components/inbox"; -import { CreateUpdateIssueModal, IssueUpdateStatus } from "@/components/issues"; +import { CreateUpdateIssueModal, NameDescriptionUpdateStatus } from "@/components/issues"; // helpers import { findHowManyDaysLeft } from "@/helpers/date-time.helper"; import { EInboxIssueStatus } from "@/helpers/inbox.helper"; @@ -41,7 +42,7 @@ type TInboxIssueActionsHeader = { workspaceSlug: string; projectId: string; inboxIssue: IInboxIssueStore | undefined; - isSubmitting: "submitting" | "submitted" | "saved"; + isSubmitting: TNameDescriptionLoader; isMobileSidebar: boolean; setIsMobileSidebar: (value: boolean) => void; isNotificationEmbed: boolean; @@ -282,7 +283,7 @@ export const InboxIssueActionsHeader: FC = observer((p )}
    - +
    diff --git a/web/core/components/inbox/content/inbox-issue-mobile-header.tsx b/web/core/components/inbox/content/inbox-issue-mobile-header.tsx index 7a66d0976f8..987a663a5ea 100644 --- a/web/core/components/inbox/content/inbox-issue-mobile-header.tsx +++ b/web/core/components/inbox/content/inbox-issue-mobile-header.tsx @@ -15,10 +15,11 @@ import { PanelLeft, MoveRight, } from "lucide-react"; +import { TNameDescriptionLoader } from "@plane/types"; import { Header, CustomMenu, EHeaderVariant } from "@plane/ui"; // components import { InboxIssueStatus } from "@/components/inbox"; -import { IssueUpdateStatus } from "@/components/issues"; +import { NameDescriptionUpdateStatus } from "@/components/issues"; // helpers import { cn } from "@/helpers/common.helper"; import { findHowManyDaysLeft } from "@/helpers/date-time.helper"; @@ -30,7 +31,7 @@ import type { IInboxIssueStore } from "@/store/inbox/inbox-issue.store"; type Props = { workspaceSlug: string; inboxIssue: IInboxIssueStore | undefined; - isSubmitting: "submitting" | "submitted" | "saved"; + isSubmitting: TNameDescriptionLoader; handleInboxIssueNavigation: (direction: "next" | "prev") => void; canMarkAsAccepted: boolean; canMarkAsDeclined: boolean; @@ -117,7 +118,7 @@ export const InboxIssueActionsMobileHeader: React.FC = observer((props) =
    - +
    diff --git a/web/core/components/inbox/content/issue-root.tsx b/web/core/components/inbox/content/issue-root.tsx index 2673245b0f1..b154bd2056a 100644 --- a/web/core/components/inbox/content/issue-root.tsx +++ b/web/core/components/inbox/content/issue-root.tsx @@ -4,7 +4,7 @@ import { Dispatch, SetStateAction, useEffect, useMemo } from "react"; import { observer } from "mobx-react"; import { usePathname } from "next/navigation"; // plane types -import { TIssue } from "@plane/types"; +import { TIssue, TNameDescriptionLoader } from "@plane/types"; // plane ui import { Loader, TOAST_TYPE, setToast } from "@plane/ui"; // components @@ -34,8 +34,8 @@ type Props = { projectId: string; inboxIssue: IInboxIssueStore; isEditable: boolean; - isSubmitting: "submitting" | "submitted" | "saved"; - setIsSubmitting: Dispatch>; + isSubmitting: TNameDescriptionLoader; + setIsSubmitting: Dispatch>; }; export const InboxIssueMainContent: React.FC = observer((props) => { diff --git a/web/core/components/inbox/content/root.tsx b/web/core/components/inbox/content/root.tsx index 64d1257e91a..e8d86a91f15 100644 --- a/web/core/components/inbox/content/root.tsx +++ b/web/core/components/inbox/content/root.tsx @@ -1,6 +1,7 @@ import { FC, useEffect, useState } from "react"; import { observer } from "mobx-react"; import useSWR from "swr"; +import { TNameDescriptionLoader } from "@plane/types"; // components import { ContentWrapper } from "@plane/ui"; import { InboxIssueActionsHeader, InboxIssueMainContent } from "@/components/inbox"; @@ -32,7 +33,7 @@ export const InboxContentRoot: FC = observer((props) => { /// router const router = useAppRouter(); // states - const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); + const [isSubmitting, setIsSubmitting] = useState("saved"); // hooks const { data: currentUser } = useUser(); const { currentTab, fetchInboxIssueById, getIssueInboxByIssueId, getIsIssueAvailable } = useProjectInbox(); diff --git a/web/core/components/issues/description-input.tsx b/web/core/components/issues/description-input.tsx index 8c18618c506..fd629f9ed0d 100644 --- a/web/core/components/issues/description-input.tsx +++ b/web/core/components/issues/description-input.tsx @@ -5,7 +5,7 @@ import debounce from "lodash/debounce"; import { observer } from "mobx-react"; import { Controller, useForm } from "react-hook-form"; // types -import { TIssue } from "@plane/types"; +import { TIssue, TNameDescriptionLoader } from "@plane/types"; import { EFileAssetType } from "@plane/types/src/enums"; // ui import { Loader } from "@plane/ui"; @@ -15,7 +15,7 @@ import { TIssueOperations } from "@/components/issues/issue-detail"; // helpers import { getDescriptionPlaceholder } from "@/helpers/issue.helper"; // hooks -import { useWorkspace } from "@/hooks/store"; +import { useMember, useWorkspace } from "@/hooks/store"; // services import { FileService } from "@/services/file.service"; const fileService = new FileService(); @@ -29,7 +29,7 @@ export type IssueDescriptionInputProps = { disabled?: boolean; issueOperations: TIssueOperations; placeholder?: string | ((isFocused: boolean, value: string) => string); - setIsSubmitting: (initialValue: "submitting" | "submitted" | "saved") => void; + setIsSubmitting: (initialValue: TNameDescriptionLoader) => void; swrIssueDescription?: string | null | undefined; }; @@ -46,6 +46,12 @@ export const IssueDescriptionInput: FC = observer((p setIsSubmitting, placeholder, } = props; + // store hooks + const { + project: { getProjectMemberIds }, + } = useMember(); + // derived values + const memberIds = getProjectMemberIds(projectId) ?? []; const { handleSubmit, reset, control } = useForm({ defaultValues: { @@ -108,6 +114,7 @@ export const IssueDescriptionInput: FC = observer((p value={swrIssueDescription ?? null} workspaceSlug={workspaceSlug} workspaceId={workspaceId} + memberIds={memberIds} projectId={projectId} dragDropEnabled onChange={(_description: object, description_html: string) => { diff --git a/web/core/components/issues/issue-update-status.tsx b/web/core/components/issues/issue-update-status.tsx index 6eb064b529d..37c4bf19a2e 100644 --- a/web/core/components/issues/issue-update-status.tsx +++ b/web/core/components/issues/issue-update-status.tsx @@ -1,12 +1,14 @@ import React from "react"; import { observer } from "mobx-react"; import { RefreshCw } from "lucide-react"; +// types +import { TNameDescriptionLoader } from "@plane/types"; type Props = { - isSubmitting: "submitting" | "submitted" | "saved"; + isSubmitting: TNameDescriptionLoader; }; -export const IssueUpdateStatus: React.FC = observer((props) => { +export const NameDescriptionUpdateStatus: React.FC = observer((props) => { const { isSubmitting } = props; return ( @@ -17,7 +19,7 @@ export const IssueUpdateStatus: React.FC = observer((props) => { }`} > {isSubmitting !== "submitted" && isSubmitting !== "saved" && ( - + )} {isSubmitting === "submitting" ? "Saving..." : "Saved"}
    diff --git a/web/core/store/pages/page.ts b/web/core/store/pages/page.ts index edf136455b2..d609cab6498 100644 --- a/web/core/store/pages/page.ts +++ b/web/core/store/pages/page.ts @@ -1,7 +1,7 @@ import set from "lodash/set"; import { action, computed, makeObservable, observable, reaction, runInAction } from "mobx"; // types -import { TDocumentPayload, TLogoProps, TPage } from "@plane/types"; +import { TDocumentPayload, TLogoProps, TNameDescriptionLoader, TPage } from "@plane/types"; // constants import { EPageAccess } from "@/constants/page"; import { EUserPermissions } from "@/plane-web/constants/user-permissions"; @@ -10,11 +10,9 @@ import { ProjectPageService } from "@/services/page"; // store import { CoreRootStore } from "../root.store"; -export type TLoader = "submitting" | "submitted" | "saved"; - export interface IPage extends TPage { // observables - isSubmitting: TLoader; + isSubmitting: TNameDescriptionLoader; // computed asJSON: TPage | undefined; isCurrentUserOwner: boolean; // it will give the user is the owner of the page or not @@ -28,7 +26,7 @@ export interface IPage extends TPage { isContentEditable: boolean; // helpers oldName: string; - setIsSubmitting: (value: TLoader) => void; + setIsSubmitting: (value: TNameDescriptionLoader) => void; cleanup: () => void; // actions update: (pageData: Partial) => Promise; @@ -47,7 +45,7 @@ export interface IPage extends TPage { export class Page implements IPage { // loaders - isSubmitting: TLoader = "saved"; + isSubmitting: TNameDescriptionLoader = "saved"; // page properties id: string | undefined; name: string | undefined; @@ -324,7 +322,7 @@ export class Page implements IPage { * @description update the submitting state * @param value */ - setIsSubmitting = (value: TLoader) => { + setIsSubmitting = (value: TNameDescriptionLoader) => { runInAction(() => { this.isSubmitting = value; }); From 6a27815a3492313d0fa594a83efecca852d2a219 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Thu, 12 Dec 2024 03:19:13 +0530 Subject: [PATCH 03/17] chore: issue store filter update --- packages/constants/src/issue.ts | 1 + packages/types/src/issues.d.ts | 3 ++- packages/types/src/view-props.d.ts | 3 +++ space/core/store/helpers/base-issues.store.ts | 2 +- web/ce/components/issues/filters/index.ts | 1 + web/ce/components/issues/filters/team-project.tsx | 12 ++++++++++++ web/ce/components/issues/issue-layouts/utils.tsx | 4 ++++ web/core/store/issue/helpers/base-issues.store.ts | 2 ++ .../store/issue/helpers/issue-filter-helper.store.ts | 2 ++ web/ee/components/issues/filters/index.ts | 1 + web/ee/components/issues/filters/team-project.tsx | 1 + web/helpers/issue.helper.ts | 2 +- 12 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 web/ce/components/issues/filters/team-project.tsx create mode 100644 web/ce/components/issues/issue-layouts/utils.tsx create mode 100644 web/ee/components/issues/filters/team-project.tsx diff --git a/packages/constants/src/issue.ts b/packages/constants/src/issue.ts index 5db398c7634..9f6a1a2e2e9 100644 --- a/packages/constants/src/issue.ts +++ b/packages/constants/src/issue.ts @@ -11,6 +11,7 @@ export enum EIssueGroupByToServerOptions { "target_date" = "target_date", "project" = "project_id", "created_by" = "created_by", + "team_project" = "project_id", } export enum EIssueGroupBYServerToProperty { diff --git a/packages/types/src/issues.d.ts b/packages/types/src/issues.d.ts index 9bbfa36b1f0..b6d32bdf816 100644 --- a/packages/types/src/issues.d.ts +++ b/packages/types/src/issues.d.ts @@ -211,7 +211,8 @@ export type GroupByColumnTypes = | "priority" | "labels" | "assignees" - | "created_by"; + | "created_by" + | "team_project"; export interface IGroupByColumn { id: string; diff --git a/packages/types/src/view-props.d.ts b/packages/types/src/view-props.d.ts index 57baa4cfdd1..aa1c75cdbdc 100644 --- a/packages/types/src/view-props.d.ts +++ b/packages/types/src/view-props.d.ts @@ -18,6 +18,7 @@ export type TIssueGroupByOptions = | "cycle" | "module" | "target_date" + | "team_project" | null; export type TIssueOrderByOptions = @@ -69,6 +70,7 @@ export type TIssueParams = | "start_date" | "target_date" | "project" + | "team_project" | "group_by" | "sub_group_by" | "order_by" @@ -92,6 +94,7 @@ export interface IIssueFilterOptions { cycle?: string[] | null; module?: string[] | null; project?: string[] | null; + team_project?: string[] | null; start_date?: string[] | null; state?: string[] | null; state_group?: string[] | null; diff --git a/space/core/store/helpers/base-issues.store.ts b/space/core/store/helpers/base-issues.store.ts index 004aa06c630..7abfa324a8d 100644 --- a/space/core/store/helpers/base-issues.store.ts +++ b/space/core/store/helpers/base-issues.store.ts @@ -26,7 +26,7 @@ import { CoreRootStore } from "../root.store"; // constants // helpers -export type TIssueDisplayFilterOptions = Exclude | "target_date"; +export type TIssueDisplayFilterOptions = Exclude | "target_date"; export enum EIssueGroupedAction { ADD = "ADD", diff --git a/web/ce/components/issues/filters/index.ts b/web/ce/components/issues/filters/index.ts index 2cd80e3a7e5..f0f36b6c97e 100644 --- a/web/ce/components/issues/filters/index.ts +++ b/web/ce/components/issues/filters/index.ts @@ -1,2 +1,3 @@ export * from "./applied-filters"; export * from "./issue-types"; +export * from "./team-project"; diff --git a/web/ce/components/issues/filters/team-project.tsx b/web/ce/components/issues/filters/team-project.tsx new file mode 100644 index 00000000000..4f4787fef8b --- /dev/null +++ b/web/ce/components/issues/filters/team-project.tsx @@ -0,0 +1,12 @@ +"use client"; + +import React from "react"; +import { observer } from "mobx-react"; + +type Props = { + appliedFilters: string[] | null; + handleUpdate: (val: string) => void; + searchQuery: string; +}; + +export const FilterTeamProjects: React.FC = observer(() => null); diff --git a/web/ce/components/issues/issue-layouts/utils.tsx b/web/ce/components/issues/issue-layouts/utils.tsx new file mode 100644 index 00000000000..48dca43bd92 --- /dev/null +++ b/web/ce/components/issues/issue-layouts/utils.tsx @@ -0,0 +1,4 @@ +// types +import { IGroupByColumn } from "@plane/types"; + +export const getTeamProjectColumns = (): IGroupByColumn[] | undefined => undefined; diff --git a/web/core/store/issue/helpers/base-issues.store.ts b/web/core/store/issue/helpers/base-issues.store.ts index d5545db4425..17e264d4240 100644 --- a/web/core/store/issue/helpers/base-issues.store.ts +++ b/web/core/store/issue/helpers/base-issues.store.ts @@ -129,6 +129,7 @@ const ISSUE_GROUP_BY_KEY: Record = { target_date: "target_date", cycle: "cycle_id", module: "module_ids", + team_project: "project_id", }; export const ISSUE_FILTER_DEFAULT_DATA: Record = { @@ -142,6 +143,7 @@ export const ISSUE_FILTER_DEFAULT_DATA: Record { const queryParams: TIssueParams[] = []; From 738f0ecdfed48f0a484f39636806e4d7e4b1812a Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Thu, 12 Dec 2024 03:19:44 +0530 Subject: [PATCH 04/17] chore: added few icons to ui package --- packages/ui/src/icons/index.ts | 3 +++ packages/ui/src/icons/lead-icon.tsx | 26 ++++++++++++++++++++++++++ packages/ui/src/icons/teams.tsx | 19 +++++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 packages/ui/src/icons/lead-icon.tsx create mode 100644 packages/ui/src/icons/teams.tsx diff --git a/packages/ui/src/icons/index.ts b/packages/ui/src/icons/index.ts index f8a2b1c849b..573efd99fb7 100644 --- a/packages/ui/src/icons/index.ts +++ b/packages/ui/src/icons/index.ts @@ -16,6 +16,7 @@ export * from "./epic-icon"; export * from "./full-screen-panel-icon"; export * from "./github-icon"; export * from "./gitlab-icon"; +export * from "./info-fill-icon"; export * from "./info-icon"; export * from "./layer-stack"; export * from "./layers-icon"; @@ -38,3 +39,5 @@ export * from "./done-icon"; export * from "./pending-icon"; export * from "./pi-chat"; export * from "./workspace-icon"; +export * from "./teams"; +export * from "./lead-icon"; diff --git a/packages/ui/src/icons/lead-icon.tsx b/packages/ui/src/icons/lead-icon.tsx new file mode 100644 index 00000000000..75575d35ec5 --- /dev/null +++ b/packages/ui/src/icons/lead-icon.tsx @@ -0,0 +1,26 @@ +import * as React from "react"; + +import { ISvgIcons } from "./type"; + +export const LeadIcon: React.FC = ({ className = "text-current", ...rest }) => ( + + + + + + + + + + + + +); diff --git a/packages/ui/src/icons/teams.tsx b/packages/ui/src/icons/teams.tsx new file mode 100644 index 00000000000..b730555989e --- /dev/null +++ b/packages/ui/src/icons/teams.tsx @@ -0,0 +1,19 @@ +import * as React from "react"; + +import { ISvgIcons } from "./type"; + +export const TeamsIcon: React.FC = ({ className = "text-current", ...rest }) => ( + + + + +); From 16b9776f5234cc6b3365ee90f1fcf42d99a304d9 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Thu, 12 Dec 2024 03:20:09 +0530 Subject: [PATCH 05/17] chore: improvements for tabs componenet --- packages/ui/src/tabs/tabs.tsx | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/tabs/tabs.tsx b/packages/ui/src/tabs/tabs.tsx index a323d9721fd..92bc3ad7276 100644 --- a/packages/ui/src/tabs/tabs.tsx +++ b/packages/ui/src/tabs/tabs.tsx @@ -1,4 +1,4 @@ -import React, { FC, Fragment } from "react"; +import React, { FC, Fragment, useEffect, useState } from "react"; import { Tab } from "@headlessui/react"; import { LucideProps } from "lucide-react"; // helpers @@ -11,11 +11,12 @@ type TabItem = { label?: React.ReactNode; content: React.ReactNode; disabled?: boolean; + onClick?: () => void; }; type TTabsProps = { tabs: TabItem[]; - storageKey: string; + storageKey?: string; actions?: React.ReactNode; defaultTab?: string; containerClassName?: string; @@ -23,6 +24,8 @@ type TTabsProps = { tabListClassName?: string; tabClassName?: string; tabPanelClassName?: string; + size?: "sm" | "md" | "lg"; + storeInLocalStorage?: boolean; }; export const Tabs: FC = (props: TTabsProps) => { @@ -36,15 +39,28 @@ export const Tabs: FC = (props: TTabsProps) => { tabListClassName = "", tabClassName = "", tabPanelClassName = "", + size = "md", + storeInLocalStorage = true, } = props; // local storage - const { storedValue, setValue } = useLocalStorage(`tab-${storageKey}`, defaultTab); + const { storedValue, setValue } = useLocalStorage( + storeInLocalStorage && storageKey ? `tab-${storageKey}` : `tab-${tabs[0]?.key}`, + defaultTab + ); + // state + const [selectedTab, setSelectedTab] = useState(storedValue ?? defaultTab); + + useEffect(() => { + if (storeInLocalStorage) { + setValue(selectedTab); + } + }, [selectedTab, setValue, storeInLocalStorage, storageKey]); const currentTabIndex = (tabKey: string): number => tabs.findIndex((tab) => tab.key === tabKey); return (
    - +
    = (props: TTabsProps) => { : tab.disabled ? "text-custom-text-400 cursor-not-allowed" : "text-custom-text-400 hover:text-custom-text-300 hover:bg-custom-background-80/60", + { + "text-xs": size === "sm", + "text-sm": size === "md", + "text-base": size === "lg", + }, tabClassName ) } key={tab.key} onClick={() => { - if (!tab.disabled) setValue(tab.key); + if (!tab.disabled) setSelectedTab(tab.key); + tab.onClick?.(); }} disabled={tab.disabled} > From 6ee99a2c4b456146d9a2a4669bddd1dcb5e75b8f Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Thu, 12 Dec 2024 03:23:13 +0530 Subject: [PATCH 06/17] chore: enhance sidebar modularity --- .../[workspaceSlug]/(projects)/sidebar.tsx | 17 ++++-- .../workspace/sidebar/teams-sidebar-list.tsx | 1 + web/ce/constants/dashboard.ts | 58 +++++++++++++++++-- web/ce/helpers/dashboard.helper.ts | 5 +- web/ce/types/dashboard.ts | 2 + .../workspace/sidebar/workspace-menu.tsx | 21 ++++--- web/core/constants/dashboard.ts | 48 --------------- .../workspace/sidebar/teams-sidebar-list.tsx | 1 + web/ee/constants/dashboard.ts | 1 + 9 files changed, 88 insertions(+), 66 deletions(-) create mode 100644 web/ce/components/workspace/sidebar/teams-sidebar-list.tsx create mode 100644 web/ee/components/workspace/sidebar/teams-sidebar-list.tsx create mode 100644 web/ee/constants/dashboard.ts diff --git a/web/app/[workspaceSlug]/(projects)/sidebar.tsx b/web/app/[workspaceSlug]/(projects)/sidebar.tsx index ac55fdec81e..11d23e35f05 100644 --- a/web/app/[workspaceSlug]/(projects)/sidebar.tsx +++ b/web/app/[workspaceSlug]/(projects)/sidebar.tsx @@ -21,6 +21,7 @@ import { useFavorite } from "@/hooks/store/use-favorite"; import useSize from "@/hooks/use-window-size"; // plane web components import { SidebarAppSwitcher } from "@/plane-web/components/sidebar"; +import { SidebarTeamsList } from "@/plane-web/components/workspace/sidebar/teams-sidebar-list"; import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; export const AppSidebar: FC = observer(() => { @@ -47,7 +48,7 @@ export const AppSidebar: FC = observer(() => { }); useEffect(() => { - if (windowSize[0] < 768) !sidebarCollapsed && toggleSidebar(); + if (windowSize[0] < 768 && !sidebarCollapsed) toggleSidebar(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [windowSize]); @@ -73,9 +74,12 @@ export const AppSidebar: FC = observer(() => { "px-4": !sidebarCollapsed, })} > + {/* Workspace switcher and settings */}
    - + {/* App switcher */} + {canPerformWorkspaceMemberActions && } + {/* Quick actions */}

    { "vertical-scrollbar px-4": !sidebarCollapsed, })} > + {/* User Menu */} - + {/* Workspace Menu */}
    + {/* Favorites Menu */} {canPerformWorkspaceMemberActions && !isFavoriteEmpty && } - + {/* Teams List */} + + {/* Projects List */}
    + {/* Help Section */}
    diff --git a/web/ce/components/workspace/sidebar/teams-sidebar-list.tsx b/web/ce/components/workspace/sidebar/teams-sidebar-list.tsx new file mode 100644 index 00000000000..92cbdfc5f5f --- /dev/null +++ b/web/ce/components/workspace/sidebar/teams-sidebar-list.tsx @@ -0,0 +1 @@ +export const SidebarTeamsList = () => null; diff --git a/web/ce/constants/dashboard.ts b/web/ce/constants/dashboard.ts index 8872982fc5b..0df2719a772 100644 --- a/web/ce/constants/dashboard.ts +++ b/web/ce/constants/dashboard.ts @@ -1,17 +1,19 @@ "use client"; // icons -import { Home, Inbox, PenSquare } from "lucide-react"; +import { Briefcase, Home, Inbox, Layers, PenSquare, BarChart2 } from "lucide-react"; // ui -import { UserActivityIcon } from "@plane/ui"; +import { UserActivityIcon, ContrastIcon } from "@plane/ui"; import { Props } from "@/components/icons/types"; +// constants import { TLinkOptions } from "@/constants/dashboard"; +// plane web constants import { EUserPermissions } from "@/plane-web/constants/user-permissions"; // plane web types -import { TSidebarUserMenuItemKeys } from "@/plane-web/types/dashboard"; +import { TSidebarUserMenuItemKeys, TSidebarWorkspaceMenuItemKeys } from "@/plane-web/types/dashboard"; -export type TSidebarUserMenuItems = { - key: TSidebarUserMenuItemKeys; +export type TSidebarMenuItems = { + key: T; label: string; href: string; access: EUserPermissions[]; @@ -19,6 +21,8 @@ export type TSidebarUserMenuItems = { Icon: React.FC; }; +export type TSidebarUserMenuItems = TSidebarMenuItems; + export const SIDEBAR_USER_MENU_ITEMS: TSidebarUserMenuItems[] = [ { key: "home", @@ -54,3 +58,47 @@ export const SIDEBAR_USER_MENU_ITEMS: TSidebarUserMenuItems[] = [ Icon: PenSquare, }, ]; + +export type TSidebarWorkspaceMenuItems = TSidebarMenuItems; + +export const SIDEBAR_WORKSPACE_MENU: Partial> = { + projects: { + key: "projects", + label: "Projects", + href: `/projects`, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/projects/`, + Icon: Briefcase, + }, + "all-issues": { + key: "all-issues", + label: "Views", + href: `/workspace-views/all-issues`, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], + highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/workspace-views/`), + Icon: Layers, + }, + "active-cycles": { + key: "active-cycles", + label: "Cycles", + href: `/active-cycles`, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/active-cycles/`, + Icon: ContrastIcon, + }, + analytics: { + key: "analytics", + label: "Analytics", + href: `/analytics`, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/analytics/`), + Icon: BarChart2, + }, +}; + +export const SIDEBAR_WORKSPACE_MENU_ITEMS: TSidebarWorkspaceMenuItems[] = [ + SIDEBAR_WORKSPACE_MENU?.projects, + SIDEBAR_WORKSPACE_MENU?.["all-issues"], + SIDEBAR_WORKSPACE_MENU?.["active-cycles"], + SIDEBAR_WORKSPACE_MENU?.analytics, +].filter((item): item is TSidebarWorkspaceMenuItems => item !== undefined); diff --git a/web/ce/helpers/dashboard.helper.ts b/web/ce/helpers/dashboard.helper.ts index b2fba63adb0..c96c818a1f0 100644 --- a/web/ce/helpers/dashboard.helper.ts +++ b/web/ce/helpers/dashboard.helper.ts @@ -1,5 +1,8 @@ // plane web types -import { TSidebarUserMenuItemKeys } from "@/plane-web/types/dashboard"; +import { TSidebarUserMenuItemKeys, TSidebarWorkspaceMenuItemKeys } from "@/plane-web/types/dashboard"; // eslint-disable-next-line @typescript-eslint/no-unused-vars export const isUserFeatureEnabled = (featureKey: TSidebarUserMenuItemKeys) => true; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const isWorkspaceFeatureEnabled = (featureKey: TSidebarWorkspaceMenuItemKeys, workspaceSlug: string) => true; diff --git a/web/ce/types/dashboard.ts b/web/ce/types/dashboard.ts index d615ac4afce..de35f60c6a7 100644 --- a/web/ce/types/dashboard.ts +++ b/web/ce/types/dashboard.ts @@ -1 +1,3 @@ export type TSidebarUserMenuItemKeys = "home" | "your-work" | "notifications" | "drafts"; + +export type TSidebarWorkspaceMenuItemKeys = "projects" | "all-issues" | "active-cycles" | "analytics"; diff --git a/web/core/components/workspace/sidebar/workspace-menu.tsx b/web/core/components/workspace/sidebar/workspace-menu.tsx index cf786f40592..42cb9cced07 100644 --- a/web/core/components/workspace/sidebar/workspace-menu.tsx +++ b/web/core/components/workspace/sidebar/workspace-menu.tsx @@ -13,7 +13,6 @@ import { CustomMenu, Tooltip } from "@plane/ui"; // components import { SidebarNavItem } from "@/components/sidebar"; // constants -import { SIDEBAR_WORKSPACE_MENU_ITEMS } from "@/constants/dashboard"; import { SIDEBAR_CLICKED } from "@/constants/event-tracker"; // helpers import { cn } from "@/helpers/common.helper"; @@ -23,22 +22,26 @@ import useLocalStorage from "@/hooks/use-local-storage"; import { usePlatformOS } from "@/hooks/use-platform-os"; // plane web components import { UpgradeBadge } from "@/plane-web/components/workspace"; +// plane web constants +import { SIDEBAR_WORKSPACE_MENU_ITEMS } from "@/plane-web/constants/dashboard"; import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; +// plane web hooks +import { isWorkspaceFeatureEnabled } from "@/plane-web/helpers/dashboard.helper"; export const SidebarWorkspaceMenu = observer(() => { // state const [isMenuActive, setIsMenuActive] = useState(false); // refs const actionSectionRef = useRef(null); + // router params + const { workspaceSlug } = useParams(); + // pathname + const pathname = usePathname(); // store hooks const { toggleSidebar, sidebarCollapsed } = useAppTheme(); const { captureEvent } = useEventTracker(); const { isMobile } = usePlatformOS(); const { allowPermissions } = useUserPermissions(); - // router params - const { workspaceSlug } = useParams(); - // pathname - const pathname = usePathname(); // local storage const { setValue: toggleWorkspaceMenu, storedValue } = useLocalStorage("is_workspace_menu_open", true); // derived values @@ -158,8 +161,9 @@ export const SidebarWorkspaceMenu = observer(() => { })} static > - {SIDEBAR_WORKSPACE_MENU_ITEMS.map( - (link) => + {SIDEBAR_WORKSPACE_MENU_ITEMS.map((link) => { + if (!isWorkspaceFeatureEnabled(link.key, workspaceSlug.toString())) return null; + return ( allowPermissions(link.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) && ( { ) - )} + ); + })} )} diff --git a/web/core/constants/dashboard.ts b/web/core/constants/dashboard.ts index 8d9848a486f..c70664eee47 100644 --- a/web/core/constants/dashboard.ts +++ b/web/core/constants/dashboard.ts @@ -1,14 +1,8 @@ "use client"; import { linearGradientDef } from "@nivo/core"; -// icons -import { BarChart2, Briefcase, Layers } from "lucide-react"; // types import { TIssuesListTypes, TStateGroups } from "@plane/types"; -// ui -import { ContrastIcon } from "@plane/ui"; -import { Props } from "@/components/icons/types"; -import { EUserPermissions } from "@/plane-web/constants/user-permissions"; // assets import CompletedIssuesDark from "@/public/empty-state/dashboard/dark/completed-issues.svg"; import OverdueIssuesDark from "@/public/empty-state/dashboard/dark/overdue-issues.svg"; @@ -250,48 +244,6 @@ export const CREATED_ISSUES_EMPTY_STATES = { }, }; -export const SIDEBAR_WORKSPACE_MENU_ITEMS: { - key: string; - label: string; - href: string; - access: EUserPermissions[]; - highlight: (pathname: string, baseUrl: string) => boolean; - Icon: React.FC; -}[] = [ - { - key: "projects", - label: "Projects", - href: `/projects`, - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], - highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/projects/`, - Icon: Briefcase, - }, - { - key: "all-issues", - label: "Views", - href: `/workspace-views/all-issues`, - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], - highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/workspace-views/`), - Icon: Layers, - }, - { - key: "active-cycles", - label: "Cycles", - href: `/active-cycles`, - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/active-cycles/`, - Icon: ContrastIcon, - }, - { - key: "analytics", - label: "Analytics", - href: `/analytics`, - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/analytics/`), - Icon: BarChart2, - }, -]; - export type TLinkOptions = { userId: string | undefined; }; diff --git a/web/ee/components/workspace/sidebar/teams-sidebar-list.tsx b/web/ee/components/workspace/sidebar/teams-sidebar-list.tsx new file mode 100644 index 00000000000..2cd5f20dc17 --- /dev/null +++ b/web/ee/components/workspace/sidebar/teams-sidebar-list.tsx @@ -0,0 +1 @@ +export * from "ce/components/workspace/sidebar/teams-sidebar-list"; diff --git a/web/ee/constants/dashboard.ts b/web/ee/constants/dashboard.ts new file mode 100644 index 00000000000..8612fbe013f --- /dev/null +++ b/web/ee/constants/dashboard.ts @@ -0,0 +1 @@ +export * from "ce/constants/dashboard"; From 34ea3424b8e539faba32b99f8781e4c883644ce3 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Thu, 12 Dec 2024 03:25:53 +0530 Subject: [PATCH 07/17] chore: update issue and router store to add support for additional issue layouts --- web/ce/constants/issues.ts | 8 +++- web/ce/helpers/issue-action-helper.ts | 15 ++++++++ web/ce/store/issue/team-views/filter.store.ts | 12 ++++++ web/ce/store/issue/team-views/index.ts | 2 + web/ce/store/issue/team-views/issue.store.ts | 13 +++++++ web/ce/store/issue/team/filter.store.ts | 12 ++++++ web/ce/store/issue/team/index.ts | 2 + web/ce/store/issue/team/issue.store.ts | 13 +++++++ web/core/constants/issue.ts | 7 ++++ web/core/hooks/store/use-issues.ts | 27 ++++++++++--- web/core/hooks/use-group-dragndrop.ts | 4 +- web/core/hooks/use-issue-layout-store.ts | 6 ++- web/core/hooks/use-issues-actions.tsx | 9 ++++- web/core/hooks/use-local-storage.tsx | 1 + web/core/store/issue/root.store.ts | 38 +++++++++++++++++-- web/core/store/router.store.ts | 10 +++++ .../components/issues/issue-layouts/utils.tsx | 1 + web/ee/helpers/issue-action-helper.ts | 1 + web/ee/store/issue/team-views/index.ts | 1 + web/ee/store/issue/team/index.ts | 1 + 20 files changed, 169 insertions(+), 14 deletions(-) create mode 100644 web/ce/helpers/issue-action-helper.ts create mode 100644 web/ce/store/issue/team-views/filter.store.ts create mode 100644 web/ce/store/issue/team-views/index.ts create mode 100644 web/ce/store/issue/team-views/issue.store.ts create mode 100644 web/ce/store/issue/team/filter.store.ts create mode 100644 web/ce/store/issue/team/index.ts create mode 100644 web/ce/store/issue/team/issue.store.ts create mode 100644 web/ee/components/issues/issue-layouts/utils.tsx create mode 100644 web/ee/helpers/issue-action-helper.ts create mode 100644 web/ee/store/issue/team-views/index.ts create mode 100644 web/ee/store/issue/team/index.ts diff --git a/web/ce/constants/issues.ts b/web/ce/constants/issues.ts index dc6ffbcb8c2..99b8ef90de1 100644 --- a/web/ce/constants/issues.ts +++ b/web/ce/constants/issues.ts @@ -1,4 +1,6 @@ import { TIssueActivityComment } from "@plane/types"; +// constants +import { ILayoutDisplayFiltersOptions } from "@/constants/issue"; export enum EActivityFilterType { ACTIVITY = "ACTIVITY", @@ -19,7 +21,7 @@ export const ACTIVITY_FILTER_TYPE_OPTIONS: Record void; @@ -32,3 +34,7 @@ export const filterActivityOnSelectedFilters = ( activity.filter((activity) => filter.includes(activity.activity_type as TActivityFilters)); export const ENABLE_ISSUE_DEPENDENCIES = false; + +export const ADDITIONAL_ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { + [pageType: string]: { [layoutType: string]: ILayoutDisplayFiltersOptions }; +} = {}; diff --git a/web/ce/helpers/issue-action-helper.ts b/web/ce/helpers/issue-action-helper.ts new file mode 100644 index 00000000000..b1644e2aa26 --- /dev/null +++ b/web/ce/helpers/issue-action-helper.ts @@ -0,0 +1,15 @@ +import { IssueActions } from "@/hooks/use-issues-actions"; + +export const useTeamIssueActions: () => IssueActions = () => ({ + fetchIssues: () => Promise.resolve(undefined), + fetchNextIssues: () => Promise.resolve(undefined), + removeIssue: () => Promise.resolve(undefined), + updateFilters: () => Promise.resolve(undefined), +}); + +export const useTeamViewIssueActions: () => IssueActions = () => ({ + fetchIssues: () => Promise.resolve(undefined), + fetchNextIssues: () => Promise.resolve(undefined), + removeIssue: () => Promise.resolve(undefined), + updateFilters: () => Promise.resolve(undefined), +}); diff --git a/web/ce/store/issue/team-views/filter.store.ts b/web/ce/store/issue/team-views/filter.store.ts new file mode 100644 index 00000000000..ea076a2aa0b --- /dev/null +++ b/web/ce/store/issue/team-views/filter.store.ts @@ -0,0 +1,12 @@ +import { IIssueRootStore } from "@/store/issue/root.store"; + +export type ITeamViewIssuesFilter = object; + +export class TeamViewIssuesFilter { + // root store + rootIssueStore: IIssueRootStore; + + constructor(_rootStore: IIssueRootStore) { + this.rootIssueStore = _rootStore; + } +} diff --git a/web/ce/store/issue/team-views/index.ts b/web/ce/store/issue/team-views/index.ts new file mode 100644 index 00000000000..0fe6c946b0c --- /dev/null +++ b/web/ce/store/issue/team-views/index.ts @@ -0,0 +1,2 @@ +export * from "./filter.store"; +export * from "./issue.store"; diff --git a/web/ce/store/issue/team-views/issue.store.ts b/web/ce/store/issue/team-views/issue.store.ts new file mode 100644 index 00000000000..f93bf21cb2e --- /dev/null +++ b/web/ce/store/issue/team-views/issue.store.ts @@ -0,0 +1,13 @@ +import { IIssueRootStore } from "@/store/issue/root.store"; +import { ITeamViewIssuesFilter } from "./filter.store"; + +export type ITeamViewIssues = object; + +export class TeamViewIssues { + // filter store + teamViewFilterStore: ITeamViewIssuesFilter; + + constructor(_rootStore: IIssueRootStore, teamViewFilterStore: ITeamViewIssuesFilter) { + this.teamViewFilterStore = teamViewFilterStore; + } +} diff --git a/web/ce/store/issue/team/filter.store.ts b/web/ce/store/issue/team/filter.store.ts new file mode 100644 index 00000000000..4fa93f37dd0 --- /dev/null +++ b/web/ce/store/issue/team/filter.store.ts @@ -0,0 +1,12 @@ +import { IIssueRootStore } from "@/store/issue/root.store"; + +export type ITeamIssuesFilter = object; + +export class TeamIssuesFilter { + // root store + rootIssueStore: IIssueRootStore; + + constructor(_rootStore: IIssueRootStore) { + this.rootIssueStore = _rootStore; + } +} diff --git a/web/ce/store/issue/team/index.ts b/web/ce/store/issue/team/index.ts new file mode 100644 index 00000000000..0fe6c946b0c --- /dev/null +++ b/web/ce/store/issue/team/index.ts @@ -0,0 +1,2 @@ +export * from "./filter.store"; +export * from "./issue.store"; diff --git a/web/ce/store/issue/team/issue.store.ts b/web/ce/store/issue/team/issue.store.ts new file mode 100644 index 00000000000..9bd3dfe3b4f --- /dev/null +++ b/web/ce/store/issue/team/issue.store.ts @@ -0,0 +1,13 @@ +import { IIssueRootStore } from "@/store/issue/root.store"; +import { ITeamIssuesFilter } from "./filter.store"; + +export type ITeamIssues = object; + +export class TeamIssues { + // filter store + teamIssueFilterStore: ITeamIssuesFilter; + + constructor(_rootStore: IIssueRootStore, teamIssueFilterStore: ITeamIssuesFilter) { + this.teamIssueFilterStore = teamIssueFilterStore; + } +} diff --git a/web/core/constants/issue.ts b/web/core/constants/issue.ts index 8629c05a253..3780567b35d 100644 --- a/web/core/constants/issue.ts +++ b/web/core/constants/issue.ts @@ -10,6 +10,7 @@ import { TIssuePriorities, TIssueGroupingFilters, } from "@plane/types"; +import { ADDITIONAL_ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/plane-web/constants"; export const DRAG_ALLOWED_GROUPS: TIssueGroupByOptions[] = [ "state", @@ -23,9 +24,11 @@ export const DRAG_ALLOWED_GROUPS: TIssueGroupByOptions[] = [ export enum EIssuesStoreType { GLOBAL = "GLOBAL", PROFILE = "PROFILE", + TEAM = "TEAM", PROJECT = "PROJECT", CYCLE = "CYCLE", MODULE = "MODULE", + TEAM_VIEW = "TEAM_VIEW", PROJECT_VIEW = "PROJECT_VIEW", ARCHIVED = "ARCHIVED", DRAFT = "DRAFT", @@ -42,7 +45,9 @@ export enum EIssueLayoutTypes { } export type TCreateModalStoreTypes = + | EIssuesStoreType.TEAM | EIssuesStoreType.PROJECT + | EIssuesStoreType.TEAM_VIEW | EIssuesStoreType.PROJECT_VIEW | EIssuesStoreType.PROFILE | EIssuesStoreType.CYCLE @@ -78,6 +83,7 @@ export const ISSUE_GROUP_BY_OPTIONS: { { key: "state", title: "States" }, { key: "state_detail.group", title: "State Groups" }, { key: "priority", title: "Priority" }, + { key: "team_project", title: "Team Project" }, // required this on team issues { key: "project", title: "Project" }, // required this on my issues { key: "cycle", title: "Cycle" }, // required this on my issues { key: "module", title: "Module" }, // required this on my issues @@ -463,6 +469,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { }, }, }, + ...ADDITIONAL_ISSUE_DISPLAY_FILTERS_BY_LAYOUT, }; export enum EIssueListRow { diff --git a/web/core/hooks/store/use-issues.ts b/web/core/hooks/store/use-issues.ts index ca40dce9ddf..8c9bc905af4 100644 --- a/web/core/hooks/store/use-issues.ts +++ b/web/core/hooks/store/use-issues.ts @@ -1,10 +1,12 @@ import { useContext } from "react"; import merge from "lodash/merge"; -// mobx store import { TIssueMap } from "@plane/types"; +// mobx store import { EIssuesStoreType } from "@/constants/issue"; import { StoreContext } from "@/lib/store-context"; // types +import { ITeamIssues, ITeamIssuesFilter } from "@/plane-web/store/issue/team"; +import { ITeamViewIssues, ITeamViewIssuesFilter } from "@/plane-web/store/issue/team-views"; import { IArchivedIssues, IArchivedIssuesFilter } from "@/store/issue/archived"; import { ICycleIssues, ICycleIssuesFilter } from "@/store/issue/cycle"; import { IDraftIssues, IDraftIssuesFilter } from "@/store/issue/draft"; @@ -33,6 +35,10 @@ export type TStoreIssues = { issues: IProfileIssues; issuesFilter: IProfileIssuesFilter; }; + [EIssuesStoreType.TEAM]: defaultIssueStore & { + issues: ITeamIssues; + issuesFilter: ITeamIssuesFilter; + }; [EIssuesStoreType.PROJECT]: defaultIssueStore & { issues: IProjectIssues; issuesFilter: IProjectIssuesFilter; @@ -45,6 +51,10 @@ export type TStoreIssues = { issues: IModuleIssues; issuesFilter: IModuleIssuesFilter; }; + [EIssuesStoreType.TEAM_VIEW]: defaultIssueStore & { + issues: ITeamViewIssues; + issuesFilter: ITeamViewIssuesFilter; + }; [EIssuesStoreType.PROJECT_VIEW]: defaultIssueStore & { issues: IProjectViewIssues; issuesFilter: IProjectViewIssuesFilter; @@ -82,16 +92,16 @@ export const useIssues = (storeType?: T): TStoreIssu issues: context.issue.workspaceDraftIssues, issuesFilter: context.issue.workspaceDraftIssuesFilter, }) as TStoreIssues[T]; - case EIssuesStoreType.WORKSPACE_DRAFT: - return merge(defaultStore, { - issues: context.issue.workspaceDraftIssues, - issuesFilter: context.issue.workspaceDraftIssuesFilter, - }) as TStoreIssues[T]; case EIssuesStoreType.PROFILE: return merge(defaultStore, { issues: context.issue.profileIssues, issuesFilter: context.issue.profileIssuesFilter, }) as TStoreIssues[T]; + case EIssuesStoreType.TEAM: + return merge(defaultStore, { + issues: context.issue.teamIssues, + issuesFilter: context.issue.teamIssuesFilter, + }) as TStoreIssues[T]; case EIssuesStoreType.PROJECT: return merge(defaultStore, { issues: context.issue.projectIssues, @@ -107,6 +117,11 @@ export const useIssues = (storeType?: T): TStoreIssu issues: context.issue.moduleIssues, issuesFilter: context.issue.moduleIssuesFilter, }) as TStoreIssues[T]; + case EIssuesStoreType.TEAM_VIEW: + return merge(defaultStore, { + issues: context.issue.teamViewIssues, + issuesFilter: context.issue.teamViewIssuesFilter, + }) as TStoreIssues[T]; case EIssuesStoreType.PROJECT_VIEW: return merge(defaultStore, { issues: context.issue.projectViewIssues, diff --git a/web/core/hooks/use-group-dragndrop.ts b/web/core/hooks/use-group-dragndrop.ts index 08695d341d2..bef3bd1f115 100644 --- a/web/core/hooks/use-group-dragndrop.ts +++ b/web/core/hooks/use-group-dragndrop.ts @@ -17,7 +17,9 @@ type DNDStoreType = | EIssuesStoreType.DRAFT | EIssuesStoreType.PROFILE | EIssuesStoreType.ARCHIVED - | EIssuesStoreType.WORKSPACE_DRAFT; + | EIssuesStoreType.WORKSPACE_DRAFT + | EIssuesStoreType.TEAM + | EIssuesStoreType.TEAM_VIEW; export const useGroupIssuesDragNDrop = ( storeType: DNDStoreType, diff --git a/web/core/hooks/use-issue-layout-store.ts b/web/core/hooks/use-issue-layout-store.ts index 17187d88377..122e97fe53c 100644 --- a/web/core/hooks/use-issue-layout-store.ts +++ b/web/core/hooks/use-issue-layout-store.ts @@ -8,7 +8,7 @@ export const IssuesStoreContext = createContext(un export const useIssueStoreType = () => { const storeType = useContext(IssuesStoreContext); - const { globalViewId, viewId, projectId, cycleId, moduleId, userId } = useParams(); + const { globalViewId, viewId, projectId, cycleId, moduleId, userId, teamId } = useParams(); // If store type exists in context, use that store type if (storeType) return storeType; @@ -26,6 +26,10 @@ export const useIssueStoreType = () => { if (projectId) return EIssuesStoreType.PROJECT; + if (teamId) return EIssuesStoreType.TEAM; + + if (teamId && viewId) return EIssuesStoreType.TEAM_VIEW; + return EIssuesStoreType.PROJECT; }; diff --git a/web/core/hooks/use-issues-actions.tsx b/web/core/hooks/use-issues-actions.tsx index 8fb2ed228ab..a0778a47d19 100644 --- a/web/core/hooks/use-issues-actions.tsx +++ b/web/core/hooks/use-issues-actions.tsx @@ -14,9 +14,10 @@ import { } from "@plane/types"; import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue"; import { EDraftIssuePaginationType } from "@/constants/workspace-drafts"; +import { useTeamIssueActions, useTeamViewIssueActions } from "@/plane-web/helpers/issue-action-helper"; import { useIssues } from "./store"; -interface IssueActions { +export interface IssueActions { fetchIssues: ( loadType: TLoader, options: IssuePaginationOptions, @@ -38,9 +39,11 @@ interface IssueActions { } export const useIssuesActions = (storeType: EIssuesStoreType): IssueActions => { + const teamIssueActions = useTeamIssueActions(); const projectIssueActions = useProjectIssueActions(); const cycleIssueActions = useCycleIssueActions(); const moduleIssueActions = useModuleIssueActions(); + const teamViewIssueActions = useTeamViewIssueActions(); const projectViewIssueActions = useProjectViewIssueActions(); const globalIssueActions = useGlobalIssueActions(); const profileIssueActions = useProfileIssueActions(); @@ -49,10 +52,14 @@ export const useIssuesActions = (storeType: EIssuesStoreType): IssueActions => { const workspaceDraftIssueActions = useWorkspaceDraftIssueActions(); switch (storeType) { + case EIssuesStoreType.TEAM_VIEW: + return teamViewIssueActions; case EIssuesStoreType.PROJECT_VIEW: return projectViewIssueActions; case EIssuesStoreType.PROFILE: return profileIssueActions; + case EIssuesStoreType.TEAM: + return teamIssueActions; case EIssuesStoreType.ARCHIVED: return archivedIssueActions; case EIssuesStoreType.DRAFT: diff --git a/web/core/hooks/use-local-storage.tsx b/web/core/hooks/use-local-storage.tsx index 538b8a93b77..6ba44662a9f 100644 --- a/web/core/hooks/use-local-storage.tsx +++ b/web/core/hooks/use-local-storage.tsx @@ -21,6 +21,7 @@ export const setValueIntoLocalStorage = (key: string, value: any) => { } }; +// TODO: Remove this once we migrate to the new hooks from plane/helpers const useLocalStorage = (key: string, initialValue: T) => { const [storedValue, setStoredValue] = useState(() => getValueFromLocalStorage(key, initialValue)); diff --git a/web/core/store/issue/root.store.ts b/web/core/store/issue/root.store.ts index ae67bbe9aea..944a32f7196 100644 --- a/web/core/store/issue/root.store.ts +++ b/web/core/store/issue/root.store.ts @@ -1,9 +1,17 @@ import isEmpty from "lodash/isEmpty"; import { autorun, makeObservable, observable } from "mobx"; import { ICycle, IIssueLabel, IModule, IProject, IState, IUserLite } from "@plane/types"; +// plane web store +import { ITeamIssuesFilter, ITeamIssues, TeamIssues, TeamIssuesFilter } from "@/plane-web/store/issue/team"; +import { + ITeamViewIssues, + ITeamViewIssuesFilter, + TeamViewIssues, + TeamViewIssuesFilter, +} from "@/plane-web/store/issue/team-views"; // root store +import { RootStore } from "@/plane-web/store/root.store"; import { IWorkspaceMembership } from "@/store/member/workspace-member.store"; -import { CoreRootStore } from "../root.store"; import { IStateStore, StateStore } from "../state.store"; // issues data store import { IArchivedIssuesFilter, ArchivedIssuesFilter, IArchivedIssues, ArchivedIssues } from "./archived"; @@ -33,6 +41,7 @@ import { export interface IIssueRootStore { currentUserId: string | undefined; workspaceSlug: string | undefined; + teamId: string | undefined; projectId: string | undefined; cycleId: string | undefined; moduleId: string | undefined; @@ -49,7 +58,7 @@ export interface IIssueRootStore { moduleMap: Record | undefined; cycleMap: Record | undefined; - rootStore: CoreRootStore; + rootStore: RootStore; issues: IIssueStore; @@ -64,6 +73,9 @@ export interface IIssueRootStore { profileIssuesFilter: IProfileIssuesFilter; profileIssues: IProfileIssues; + teamIssuesFilter: ITeamIssuesFilter; + teamIssues: ITeamIssues; + projectIssuesFilter: IProjectIssuesFilter; projectIssues: IProjectIssues; @@ -73,6 +85,9 @@ export interface IIssueRootStore { moduleIssuesFilter: IModuleIssuesFilter; moduleIssues: IModuleIssues; + teamViewIssuesFilter: ITeamViewIssuesFilter; + teamViewIssues: ITeamViewIssues; + projectViewIssuesFilter: IProjectViewIssuesFilter; projectViewIssues: IProjectViewIssues; @@ -89,6 +104,7 @@ export interface IIssueRootStore { export class IssueRootStore implements IIssueRootStore { currentUserId: string | undefined = undefined; workspaceSlug: string | undefined = undefined; + teamId: string | undefined = undefined; projectId: string | undefined = undefined; cycleId: string | undefined = undefined; moduleId: string | undefined = undefined; @@ -105,7 +121,7 @@ export class IssueRootStore implements IIssueRootStore { moduleMap: Record | undefined = undefined; cycleMap: Record | undefined = undefined; - rootStore: CoreRootStore; + rootStore: RootStore; issues: IIssueStore; @@ -120,6 +136,9 @@ export class IssueRootStore implements IIssueRootStore { profileIssuesFilter: IProfileIssuesFilter; profileIssues: IProfileIssues; + teamIssuesFilter: ITeamIssuesFilter; + teamIssues: ITeamIssues; + projectIssuesFilter: IProjectIssuesFilter; projectIssues: IProjectIssues; @@ -129,6 +148,9 @@ export class IssueRootStore implements IIssueRootStore { moduleIssuesFilter: IModuleIssuesFilter; moduleIssues: IModuleIssues; + teamViewIssuesFilter: ITeamViewIssuesFilter; + teamViewIssues: ITeamViewIssues; + projectViewIssuesFilter: IProjectViewIssuesFilter; projectViewIssues: IProjectViewIssues; @@ -141,9 +163,10 @@ export class IssueRootStore implements IIssueRootStore { issueKanBanView: IIssueKanBanViewStore; issueCalendarView: ICalendarStore; - constructor(rootStore: CoreRootStore) { + constructor(rootStore: RootStore) { makeObservable(this, { workspaceSlug: observable.ref, + teamId: observable.ref, projectId: observable.ref, cycleId: observable.ref, moduleId: observable.ref, @@ -166,6 +189,7 @@ export class IssueRootStore implements IIssueRootStore { autorun(() => { if (rootStore?.user?.data?.id) this.currentUserId = rootStore?.user?.data?.id; if (this.workspaceSlug !== rootStore.router.workspaceSlug) this.workspaceSlug = rootStore.router.workspaceSlug; + if (this.teamId !== rootStore.router.teamId) this.teamId = rootStore.router.teamId; if (this.projectId !== rootStore.router.projectId) this.projectId = rootStore.router.projectId; if (this.cycleId !== rootStore.router.cycleId) this.cycleId = rootStore.router.cycleId; if (this.moduleId !== rootStore.router.moduleId) this.moduleId = rootStore.router.moduleId; @@ -201,12 +225,18 @@ export class IssueRootStore implements IIssueRootStore { this.projectIssuesFilter = new ProjectIssuesFilter(this); this.projectIssues = new ProjectIssues(this, this.projectIssuesFilter); + this.teamIssuesFilter = new TeamIssuesFilter(this); + this.teamIssues = new TeamIssues(this, this.teamIssuesFilter); + this.cycleIssuesFilter = new CycleIssuesFilter(this); this.cycleIssues = new CycleIssues(this, this.cycleIssuesFilter); this.moduleIssuesFilter = new ModuleIssuesFilter(this); this.moduleIssues = new ModuleIssues(this, this.moduleIssuesFilter); + this.teamViewIssuesFilter = new TeamViewIssuesFilter(this); + this.teamViewIssues = new TeamViewIssues(this, this.teamViewIssuesFilter); + this.projectViewIssuesFilter = new ProjectViewIssuesFilter(this); this.projectViewIssues = new ProjectViewIssues(this, this.projectViewIssuesFilter); diff --git a/web/core/store/router.store.ts b/web/core/store/router.store.ts index ae5013836c7..051ed49b4a0 100644 --- a/web/core/store/router.store.ts +++ b/web/core/store/router.store.ts @@ -9,6 +9,7 @@ export interface IRouterStore { setQuery: (query: ParsedUrlQuery) => void; // computed workspaceSlug: string | undefined; + teamId: string | undefined; projectId: string | undefined; cycleId: string | undefined; moduleId: string | undefined; @@ -34,6 +35,7 @@ export class RouterStore implements IRouterStore { setQuery: action.bound, //computed workspaceSlug: computed, + teamId: computed, projectId: computed, cycleId: computed, moduleId: computed, @@ -66,6 +68,14 @@ export class RouterStore implements IRouterStore { return this.query?.workspaceSlug?.toString(); } + /** + * Returns the team id from the query + * @returns string|undefined + */ + get teamId() { + return this.query?.teamId?.toString(); + } + /** * Returns the project id from the query * @returns string|undefined diff --git a/web/ee/components/issues/issue-layouts/utils.tsx b/web/ee/components/issues/issue-layouts/utils.tsx new file mode 100644 index 00000000000..1716a8950ec --- /dev/null +++ b/web/ee/components/issues/issue-layouts/utils.tsx @@ -0,0 +1 @@ +export * from "ce/components/issues/issue-layouts/utils"; diff --git a/web/ee/helpers/issue-action-helper.ts b/web/ee/helpers/issue-action-helper.ts new file mode 100644 index 00000000000..1e1ed08f01c --- /dev/null +++ b/web/ee/helpers/issue-action-helper.ts @@ -0,0 +1 @@ +export * from "ce/helpers/issue-action-helper"; diff --git a/web/ee/store/issue/team-views/index.ts b/web/ee/store/issue/team-views/index.ts new file mode 100644 index 00000000000..c02c38b4fa1 --- /dev/null +++ b/web/ee/store/issue/team-views/index.ts @@ -0,0 +1 @@ +export * from "ce/store/issue/team-views"; diff --git a/web/ee/store/issue/team/index.ts b/web/ee/store/issue/team/index.ts new file mode 100644 index 00000000000..2e02634a967 --- /dev/null +++ b/web/ee/store/issue/team/index.ts @@ -0,0 +1 @@ +export * from "ce/store/issue/team"; From e4307304a31740150f9520f295139665769d5092 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Thu, 12 Dec 2024 03:28:33 +0530 Subject: [PATCH 08/17] chore: enhanced cycle componenets modularity --- .../cycles/(detail)/[cycleId]/page.tsx | 7 +- .../components/cycles/active-cycle/root.tsx | 117 ++++++++++-------- .../cycles/analytics-sidebar/root.tsx | 27 ++-- .../components/cycles/cycle-peek-overview.tsx | 24 ++-- .../cycles/list/cycle-list-item-action.tsx | 13 +- .../cycles/list/cycles-list-item.tsx | 6 +- 6 files changed, 108 insertions(+), 86 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx index a1f7071a449..7a29f055306 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx @@ -77,7 +77,12 @@ const CycleDetailPage = observer(() => { "0px 1px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 4px 0px rgba(16, 24, 40, 0.06), 0px 1px 8px -1px rgba(16, 24, 40, 0.06)", }} > - + )} diff --git a/web/ce/components/cycles/active-cycle/root.tsx b/web/ce/components/cycles/active-cycle/root.tsx index a173cfda03a..5ebddc63f23 100644 --- a/web/ce/components/cycles/active-cycle/root.tsx +++ b/web/ce/components/cycles/active-cycle/root.tsx @@ -1,5 +1,6 @@ "use client"; +import { useMemo } from "react"; import { observer } from "mobx-react"; import { Disclosure } from "@headlessui/react"; // ui @@ -22,68 +23,80 @@ import { ActiveCycleIssueDetails } from "@/store/issue/cycle"; interface IActiveCycleDetails { workspaceSlug: string; projectId: string; + cycleId?: string; + showHeader?: boolean; } export const ActiveCycleRoot: React.FC = observer((props) => { - const { workspaceSlug, projectId } = props; - const { currentProjectActiveCycle, currentProjectActiveCycleId } = useCycle(); + const { workspaceSlug, projectId, cycleId: propsCycleId, showHeader = true } = props; + const { currentProjectActiveCycleId } = useCycle(); + // derived values + const cycleId = propsCycleId ?? currentProjectActiveCycleId; + // fetch cycle details const { handleFiltersUpdate, cycle: activeCycle, cycleIssueDetails, - } = useCyclesDetails({ workspaceSlug, projectId, cycleId: currentProjectActiveCycleId }); + } = useCyclesDetails({ workspaceSlug, projectId, cycleId }); + + const ActiveCyclesComponent = useMemo( + () => ( + <> + {!cycleId || !activeCycle ? ( + + ) : ( +
    + {cycleId && ( + + )} + +
    + + + +
    +
    +
    + )} + + ), + [cycleId, activeCycle, workspaceSlug, projectId, handleFiltersUpdate, cycleIssueDetails] + ); return ( <> - - {({ open }) => ( - <> - - - - - {!currentProjectActiveCycle ? ( - - ) : ( -
    - {currentProjectActiveCycleId && ( - - )} - -
    - - - -
    -
    -
    - )} -
    - - )} -
    + {showHeader ? ( + + {({ open }) => ( + <> + + + + {ActiveCyclesComponent} + + )} + + ) : ( + <>{ActiveCyclesComponent} + )} ); }); diff --git a/web/core/components/cycles/analytics-sidebar/root.tsx b/web/core/components/cycles/analytics-sidebar/root.tsx index fd8c984a690..b709c0e6317 100644 --- a/web/core/components/cycles/analytics-sidebar/root.tsx +++ b/web/core/components/cycles/analytics-sidebar/root.tsx @@ -2,7 +2,6 @@ import React from "react"; import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; // ui import { Loader } from "@plane/ui"; // components @@ -13,19 +12,19 @@ import useCyclesDetails from "../active-cycle/use-cycles-details"; type Props = { handleClose: () => void; isArchived?: boolean; - cycleId?: string; + cycleId: string; + projectId: string; + workspaceSlug: string; }; export const CycleDetailsSidebar: React.FC = observer((props) => { - const { handleClose, isArchived } = props; - // router - const { workspaceSlug, projectId, cycleId } = useParams(); + const { handleClose, isArchived, projectId, workspaceSlug, cycleId } = props; // store hooks const { cycle: cycleDetails } = useCyclesDetails({ - workspaceSlug: workspaceSlug.toString(), - projectId: projectId.toString(), - cycleId: cycleId?.toString() || props.cycleId, + workspaceSlug, + projectId, + cycleId, }); if (!cycleDetails) @@ -47,21 +46,17 @@ export const CycleDetailsSidebar: React.FC = observer((props) => {
    - +
    {workspaceSlug && projectId && cycleDetails?.id && ( - + )}
    ); diff --git a/web/core/components/cycles/cycle-peek-overview.tsx b/web/core/components/cycles/cycle-peek-overview.tsx index 759569cfa9f..187425b8d72 100644 --- a/web/core/components/cycles/cycle-peek-overview.tsx +++ b/web/core/components/cycles/cycle-peek-overview.tsx @@ -9,12 +9,13 @@ import { useAppRouter } from "@/hooks/use-app-router"; import { CycleDetailsSidebar } from "./"; type Props = { - projectId: string; + projectId?: string; workspaceSlug: string; isArchived?: boolean; }; -export const CyclePeekOverview: React.FC = observer(({ projectId, workspaceSlug, isArchived = false }) => { +export const CyclePeekOverview: React.FC = observer((props) => { + const { projectId: propsProjectId, workspaceSlug, isArchived } = props; // router const router = useAppRouter(); const pathname = usePathname(); @@ -23,22 +24,25 @@ export const CyclePeekOverview: React.FC = observer(({ projectId, workspa // refs const ref = React.useRef(null); // store hooks - const { fetchCycleDetails, fetchArchivedCycleDetails } = useCycle(); + const { getCycleById, fetchCycleDetails, fetchArchivedCycleDetails } = useCycle(); + // derived values + const cycleDetails = peekCycle ? getCycleById(peekCycle.toString()) : undefined; + const projectId = propsProjectId || cycleDetails?.project_id; const handleClose = () => { const query = generateQueryParams(searchParams, ["peekCycle"]); - router.push(`${pathname}?${query}`); + router.push(`${pathname}?${query}`, {}, { showProgressBar: false }); }; useEffect(() => { - if (!peekCycle) return; + if (!peekCycle || !projectId) return; if (isArchived) fetchArchivedCycleDetails(workspaceSlug, projectId, peekCycle.toString()); else fetchCycleDetails(workspaceSlug, projectId, peekCycle.toString()); }, [fetchArchivedCycleDetails, fetchCycleDetails, isArchived, peekCycle, projectId, workspaceSlug]); return ( <> - {peekCycle && ( + {peekCycle && projectId && (
    = observer(({ projectId, workspa "0px 1px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 4px 0px rgba(16, 24, 40, 0.06), 0px 1px 8px -1px rgba(16, 24, 40, 0.06)", }} > - +
    )} diff --git a/web/core/components/cycles/list/cycle-list-item-action.tsx b/web/core/components/cycles/list/cycle-list-item-action.tsx index 989e0436e36..73dca345d7c 100644 --- a/web/core/components/cycles/list/cycle-list-item-action.tsx +++ b/web/core/components/cycles/list/cycle-list-item-action.tsx @@ -14,10 +14,9 @@ import { CycleQuickActions } from "@/components/cycles"; import { DateRangeDropdown } from "@/components/dropdowns"; import { ButtonAvatars } from "@/components/dropdowns/member/avatar"; // constants -import { CYCLE_STATUS } from "@/constants/cycle"; import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "@/constants/event-tracker"; // helpers -import { findHowManyDaysLeft, getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; +import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { getFileURL } from "@/helpers/file.helper"; // hooks import { generateQueryParams } from "@/helpers/router.helper"; @@ -69,11 +68,11 @@ export const CycleListItemAction: FC = observer((props) => { const cycleStatus = cycleDetails.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft"; const isEditingAllowed = allowPermissions( [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.PROJECT + EUserPermissionsLevel.PROJECT, + workspaceSlug, + projectId ); const renderIcon = Boolean(cycleDetails.start_date) || Boolean(cycleDetails.end_date); - const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus); - const daysLeft = findHowManyDaysLeft(cycleDetails.end_date) ?? 0; // handlers const handleAddToFavorites = (e: MouseEvent) => { @@ -201,9 +200,9 @@ export const CycleListItemAction: FC = observer((props) => { const query = generateQueryParams(searchParams, ["peekCycle"]); if (searchParams.has("peekCycle") && searchParams.get("peekCycle") === cycleId) { - router.push(`${pathname}?${query}`); + router.push(`${pathname}?${query}`, {}, { showProgressBar: false }); } else { - router.push(`${pathname}?${query && `${query}&`}peekCycle=${cycleId}`); + router.push(`${pathname}?${query && `${query}&`}peekCycle=${cycleId}`, {}, { showProgressBar: false }); } }; diff --git a/web/core/components/cycles/list/cycles-list-item.tsx b/web/core/components/cycles/list/cycles-list-item.tsx index 8d531216a4b..5954a0a7f33 100644 --- a/web/core/components/cycles/list/cycles-list-item.tsx +++ b/web/core/components/cycles/list/cycles-list-item.tsx @@ -4,7 +4,7 @@ import { FC, MouseEvent, useRef } from "react"; import { observer } from "mobx-react"; import { usePathname, useSearchParams } from "next/navigation"; // icons -import { Check, Info } from "lucide-react"; +import { Check } from "lucide-react"; // types import type { TCycleGroups } from "@plane/types"; // ui @@ -72,9 +72,9 @@ export const CyclesListItem: FC = observer((props) => { const query = generateQueryParams(searchParams, ["peekCycle"]); if (searchParams.has("peekCycle") && searchParams.get("peekCycle") === cycleId) { - router.push(`${pathname}?${query}`); + router.push(`${pathname}?${query}`, {}, { showProgressBar: false }); } else { - router.push(`${pathname}?${query && `${query}&`}peekCycle=${cycleId}`); + router.push(`${pathname}?${query && `${query}&`}peekCycle=${cycleId}`, {}, { showProgressBar: false }); } }; From abbb15f9f42e0f87ac46ccf458cddd9e2e23d523 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Thu, 12 Dec 2024 03:29:42 +0530 Subject: [PATCH 09/17] feat: added project grouping header for cycles list --- .../list/cycle-list-project-group-header.tsx | 44 +++++++++++++++++++ web/core/components/cycles/list/index.ts | 1 + 2 files changed, 45 insertions(+) create mode 100644 web/core/components/cycles/list/cycle-list-project-group-header.tsx diff --git a/web/core/components/cycles/list/cycle-list-project-group-header.tsx b/web/core/components/cycles/list/cycle-list-project-group-header.tsx new file mode 100644 index 00000000000..d663eca0d86 --- /dev/null +++ b/web/core/components/cycles/list/cycle-list-project-group-header.tsx @@ -0,0 +1,44 @@ +"use client"; + +import React, { FC } from "react"; +import { observer } from "mobx-react"; +import { ChevronRight } from "lucide-react"; +// icons +import { Row, Logo } from "@plane/ui"; +// helpers +import { cn } from "@/helpers/common.helper"; +import { useProject } from "@/hooks/store/use-project"; + +type Props = { + projectId: string; + count?: number; + showCount?: boolean; + isExpanded?: boolean; +}; + +export const CycleListProjectGroupHeader: FC = observer((props) => { + const { projectId, count, showCount = false, isExpanded = false } = props; + // store hooks + const { getProjectById } = useProject(); + // derived values + const project = getProjectById(projectId); + + if (!project) return null; + return ( + + +
    + +
    +
    +
    {project.name}
    + {showCount &&
    {`${count ?? "0"}`}
    } +
    +
    + ); +}); diff --git a/web/core/components/cycles/list/index.ts b/web/core/components/cycles/list/index.ts index 4eebc577943..25419a0560a 100644 --- a/web/core/components/cycles/list/index.ts +++ b/web/core/components/cycles/list/index.ts @@ -3,3 +3,4 @@ export * from "./cycles-list-map"; export * from "./root"; export * from "./cycle-list-item-action"; export * from "./cycle-list-group-header"; +export * from "./cycle-list-project-group-header"; From 43c2069484c6e75dacb98fecba51c593686be67e Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Thu, 12 Dec 2024 03:30:53 +0530 Subject: [PATCH 10/17] chore: enhanced project dropdown componenet by adding multiple selection functionality --- web/core/components/cycles/form.tsx | 1 + web/core/components/dropdowns/project.tsx | 66 ++++++++++++++----- .../issue-modal/components/project-select.tsx | 1 + web/core/components/modules/form.tsx | 1 + 4 files changed, 54 insertions(+), 15 deletions(-) diff --git a/web/core/components/cycles/form.tsx b/web/core/components/cycles/form.tsx index 660d33cdb80..7651c5d4433 100644 --- a/web/core/components/cycles/form.tsx +++ b/web/core/components/cycles/form.tsx @@ -75,6 +75,7 @@ export const CycleForm: React.FC = (props) => { onChange(val); setActiveProject(val); }} + multiple={false} buttonVariant="border-with-text" renderCondition={(project) => shouldRenderProject(project)} tabIndex={getIndex("cover_image")} diff --git a/web/core/components/dropdowns/project.tsx b/web/core/components/dropdowns/project.tsx index 3f973cd1618..f94014eb8b8 100644 --- a/web/core/components/dropdowns/project.tsx +++ b/web/core/components/dropdowns/project.tsx @@ -1,4 +1,4 @@ -import { Fragment, ReactNode, useRef, useState } from "react"; +import { ReactNode, useRef, useState } from "react"; import { observer } from "mobx-react"; import { usePopper } from "react-popper"; import { Check, ChevronDown, Search } from "lucide-react"; @@ -25,12 +25,21 @@ type Props = TDropdownProps & { button?: ReactNode; dropdownArrow?: boolean; dropdownArrowClassName?: string; - onChange: (val: string) => void; onClose?: () => void; renderCondition?: (project: TProject) => boolean; - value: string | null; renderByDefault?: boolean; -}; +} & ( + | { + multiple: false; + onChange: (val: string) => void; + value: string | null; + } + | { + multiple: true; + onChange: (val: string[]) => void; + value: string[]; + } + ); export const ProjectDropdown: React.FC = observer((props) => { const { @@ -43,6 +52,7 @@ export const ProjectDropdown: React.FC = observer((props) => { dropdownArrow = false, dropdownArrowClassName = "", hideIcon = false, + multiple, onChange, onClose, placeholder = "Project", @@ -99,8 +109,6 @@ export const ProjectDropdown: React.FC = observer((props) => { const filteredOptions = query === "" ? options : options?.filter((o) => o?.query.toLowerCase().includes(query.toLowerCase())); - const selectedProject = value ? getProjectById(value) : null; - const { handleClose, handleKeyDown, handleOnClick, searchInputKeyDown } = useDropdown({ dropdownRef, inputRef, @@ -111,9 +119,40 @@ export const ProjectDropdown: React.FC = observer((props) => { setQuery, }); - const dropdownOnChange = (val: string) => { + const dropdownOnChange = (val: string & string[]) => { onChange(val); - handleClose(); + if (!multiple) handleClose(); + }; + + const getDisplayName = (value: string | string[] | null, placeholder: string = "") => { + if (Array.isArray(value)) { + const firstProject = getProjectById(value[0]); + return value.length ? (value.length === 1 ? firstProject?.name : `${value.length} projects`) : placeholder; + } else { + return value ? (getProjectById(value)?.name ?? placeholder) : placeholder; + } + }; + + const getProjectIcon = (value: string | string[] | null) => { + const renderIcon = (projectDetails: TProject) => ( + + + + ); + + if (Array.isArray(value)) { + return ( +
    + {value.map((projectId) => { + const projectDetails = getProjectById(projectId); + return projectDetails ? renderIcon(projectDetails) : null; + })} +
    + ); + } else { + const projectDetails = getProjectById(value); + return projectDetails ? renderIcon(projectDetails) : null; + } }; const comboButton = ( @@ -147,18 +186,14 @@ export const ProjectDropdown: React.FC = observer((props) => { className={buttonClassName} isActive={isOpen} tooltipHeading="Project" - tooltipContent={selectedProject?.name ?? placeholder} + tooltipContent={value?.length ? `${value.length} project${value.length !== 1 ? "s" : ""}` : placeholder} showTooltip={showTooltip} variant={buttonVariant} renderToolTipByDefault={renderByDefault} > - {!hideIcon && selectedProject && ( - - - - )} + {!hideIcon && getProjectIcon(value)} {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && ( - {selectedProject?.name ?? placeholder} + {getDisplayName(value, placeholder)} )} {dropdownArrow && (