From 134644fdf12a5b0d7598df61fb2b40c86a99a351 Mon Sep 17 00:00:00 2001 From: Vamsi Krishna <46787868+mathalav55@users.noreply.github.com> Date: Wed, 11 Dec 2024 13:41:19 +0530 Subject: [PATCH 001/355] [WEB-2382]chore:notification files restructuring (#6181) * chore: adjusted increment/decrement for unread count * chore: improved param handling for unread notification count function * chore:file restructuring * fix:notification types * chore:file restructuring * chore:modified notfication types * chore: modified types for notification * chore:removed redundant checks for id --- .../types/src/workspace-notifications.d.ts | 2 +- .../(projects)/notifications/layout.tsx | 2 +- .../workspace-notifications/index.ts | 2 +- .../notification-card/root.tsx | 0 .../workspace-notifications/index.ts | 1 + .../workspace-notifications/root.tsx | 3 ++- .../sidebar/notification-card/index.ts | 1 - web/core/store/notifications/notification.ts | 18 ++---------------- 8 files changed, 8 insertions(+), 21 deletions(-) rename web/{core/components/workspace-notifications/sidebar => ce/components/workspace-notifications}/notification-card/root.tsx (100%) rename web/{ce => core}/components/workspace-notifications/root.tsx (97%) diff --git a/packages/types/src/workspace-notifications.d.ts b/packages/types/src/workspace-notifications.d.ts index 7d960015b..0e0e15af1 100644 --- a/packages/types/src/workspace-notifications.d.ts +++ b/packages/types/src/workspace-notifications.d.ts @@ -35,7 +35,7 @@ export type TNotificationData = { }; export type TNotification = { - id: string | undefined; + id: string; title: string | undefined; data: TNotificationData | undefined; entity_identifier: string | undefined; diff --git a/web/app/[workspaceSlug]/(projects)/notifications/layout.tsx b/web/app/[workspaceSlug]/(projects)/notifications/layout.tsx index 7d71948d8..e3d730363 100644 --- a/web/app/[workspaceSlug]/(projects)/notifications/layout.tsx +++ b/web/app/[workspaceSlug]/(projects)/notifications/layout.tsx @@ -1,7 +1,7 @@ "use client"; // components -import { NotificationsSidebarRoot } from "@/plane-web/components/workspace-notifications"; +import { NotificationsSidebarRoot } from "@/components/workspace-notifications"; export default function ProjectInboxIssuesLayout({ children }: { children: React.ReactNode }) { return ( diff --git a/web/ce/components/workspace-notifications/index.ts b/web/ce/components/workspace-notifications/index.ts index c8711b96a..18c4afa96 100644 --- a/web/ce/components/workspace-notifications/index.ts +++ b/web/ce/components/workspace-notifications/index.ts @@ -1 +1 @@ -export * from './root' \ No newline at end of file +export * from "./notification-card/root"; diff --git a/web/core/components/workspace-notifications/sidebar/notification-card/root.tsx b/web/ce/components/workspace-notifications/notification-card/root.tsx similarity index 100% rename from web/core/components/workspace-notifications/sidebar/notification-card/root.tsx rename to web/ce/components/workspace-notifications/notification-card/root.tsx diff --git a/web/core/components/workspace-notifications/index.ts b/web/core/components/workspace-notifications/index.ts index 2682c9114..8bc361a7c 100644 --- a/web/core/components/workspace-notifications/index.ts +++ b/web/core/components/workspace-notifications/index.ts @@ -1,2 +1,3 @@ export * from "./notification-app-sidebar-option"; export * from "./sidebar"; +export * from "./root"; diff --git a/web/ce/components/workspace-notifications/root.tsx b/web/core/components/workspace-notifications/root.tsx similarity index 97% rename from web/ce/components/workspace-notifications/root.tsx rename to web/core/components/workspace-notifications/root.tsx index 35c61263d..fa17060d5 100644 --- a/web/ce/components/workspace-notifications/root.tsx +++ b/web/core/components/workspace-notifications/root.tsx @@ -11,7 +11,6 @@ import { NotificationEmptyState, NotificationSidebarHeader, AppliedFilters, - NotificationCardListRoot, } from "@/components/workspace-notifications"; // constants import { NOTIFICATION_TABS, TNotificationTab } from "@/constants/notification"; @@ -21,6 +20,8 @@ import { getNumberCount } from "@/helpers/string.helper"; // hooks import { useWorkspace, useWorkspaceNotifications } from "@/hooks/store"; +import { NotificationCardListRoot } from "@/plane-web/components/workspace-notifications"; + export const NotificationsSidebarRoot: FC = observer(() => { const { workspaceSlug } = useParams(); // hooks diff --git a/web/core/components/workspace-notifications/sidebar/notification-card/index.ts b/web/core/components/workspace-notifications/sidebar/notification-card/index.ts index d4000aa9e..8c086a5a8 100644 --- a/web/core/components/workspace-notifications/sidebar/notification-card/index.ts +++ b/web/core/components/workspace-notifications/sidebar/notification-card/index.ts @@ -1,3 +1,2 @@ -export * from "./root"; export * from "./item"; export * from "./options"; diff --git a/web/core/store/notifications/notification.ts b/web/core/store/notifications/notification.ts index dff2755fd..74c0bf38c 100644 --- a/web/core/store/notifications/notification.ts +++ b/web/core/store/notifications/notification.ts @@ -26,7 +26,7 @@ export interface INotification extends TNotification { export class Notification implements INotification { // observables - id: string | undefined = undefined; + id: string; title: string | undefined = undefined; data: TNotificationData | undefined = undefined; entity_identifier: string | undefined = undefined; @@ -54,6 +54,7 @@ export class Notification implements INotification { private store: CoreRootStore, private notification: TNotification ) { + this.id = this.notification.id; makeObservable(this, { // observables id: observable.ref, @@ -90,7 +91,6 @@ export class Notification implements INotification { snoozeNotification: action, unSnoozeNotification: action, }); - this.id = this.notification.id; this.title = this.notification.title; this.data = this.notification.data; this.entity_identifier = this.notification.entity_identifier; @@ -169,8 +169,6 @@ export class Notification implements INotification { workspaceSlug: string, payload: Partial ): Promise => { - if (!this.id) return undefined; - try { const notification = await workspaceNotificationService.updateNotificationById(workspaceSlug, this.id, payload); if (notification) { @@ -188,8 +186,6 @@ export class Notification implements INotification { * @returns { TNotification | undefined } */ markNotificationAsRead = async (workspaceSlug: string): Promise => { - if (!this.id) return undefined; - const currentNotificationReadAt = this.read_at; try { const payload: Partial = { @@ -215,8 +211,6 @@ export class Notification implements INotification { * @returns { TNotification | undefined } */ markNotificationAsUnRead = async (workspaceSlug: string): Promise => { - if (!this.id) return undefined; - const currentNotificationReadAt = this.read_at; try { const payload: Partial = { @@ -242,8 +236,6 @@ export class Notification implements INotification { * @returns { TNotification | undefined } */ archiveNotification = async (workspaceSlug: string): Promise => { - if (!this.id) return undefined; - const currentNotificationArchivedAt = this.archived_at; try { const payload: Partial = { @@ -267,8 +259,6 @@ export class Notification implements INotification { * @returns { TNotification | undefined } */ unArchiveNotification = async (workspaceSlug: string): Promise => { - if (!this.id) return undefined; - const currentNotificationArchivedAt = this.archived_at; try { const payload: Partial = { @@ -293,8 +283,6 @@ export class Notification implements INotification { * @returns { TNotification | undefined } */ snoozeNotification = async (workspaceSlug: string, snoozeTill: Date): Promise => { - if (!this.id) return undefined; - const currentNotificationSnoozeTill = this.snoozed_till; try { const payload: Partial = { @@ -315,8 +303,6 @@ export class Notification implements INotification { * @returns { TNotification | undefined } */ unSnoozeNotification = async (workspaceSlug: string): Promise => { - if (!this.id) return undefined; - const currentNotificationSnoozeTill = this.snoozed_till; try { const payload: Partial = { From 5a9ae666808a586abe37c69c33a23abc5524da7e Mon Sep 17 00:00:00 2001 From: rahulramesha <71900764+rahulramesha@users.noreply.github.com> Date: Wed, 11 Dec 2024 13:43:48 +0530 Subject: [PATCH 002/355] chore: Remove shouldIgnoreDependencies flags while dragging in timeline view (#6150) * remove shouldEnable dependency flags for timeline view * chore: error handling --------- Co-authored-by: Prateek Shourya --- .../blockResizables/use-gantt-resizable.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/web/core/components/gantt-chart/helpers/blockResizables/use-gantt-resizable.ts b/web/core/components/gantt-chart/helpers/blockResizables/use-gantt-resizable.ts index e6a1a887e..c96a25725 100644 --- a/web/core/components/gantt-chart/helpers/blockResizables/use-gantt-resizable.ts +++ b/web/core/components/gantt-chart/helpers/blockResizables/use-gantt-resizable.ts @@ -1,6 +1,6 @@ import { useRef, useState } from "react"; // Plane -import { setToast } from "@plane/ui"; +import { setToast, TOAST_TYPE } from "@plane/ui"; // hooks import { useTimeLineChartStore } from "@/hooks/use-timeline-chart"; // @@ -103,7 +103,7 @@ export const useGanttResizable = ( const deltaWidth = Math.round((width - (block.position?.width ?? 0)) / dayWidth) * dayWidth; // call update blockPosition - if (deltaWidth || deltaLeft) updateBlockPosition(block.id, deltaLeft, deltaWidth, dragDirection !== "move"); + if (deltaWidth || deltaLeft) updateBlockPosition(block.id, deltaLeft, deltaWidth); }; // remove event listeners and call updateBlockDates @@ -119,10 +119,14 @@ export const useGanttResizable = ( (dragDirection === "left" && !block.start_date) || (dragDirection === "right" && !block.target_date); try { - const blockUpdates = getUpdatedPositionAfterDrag(block.id, shouldUpdateHalfBlock, dragDirection !== "move"); - updateBlockDates && updateBlockDates(blockUpdates); - } catch (e) { - setToast; + const blockUpdates = getUpdatedPositionAfterDrag(block.id, shouldUpdateHalfBlock); + if (updateBlockDates) updateBlockDates(blockUpdates); + } catch { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error", + message: "Something went wrong while updating block dates", + }); } setIsDragging(false); From 0ac68f27311c212c9cd4603fd362b165370e9b71 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Wed, 11 Dec 2024 15:14:15 +0530 Subject: [PATCH 003/355] improvement: refactored issue grouping logic to access MobX store directly (#6134) * improvement: refactored issue grouping logic to access MobX store directly * chore: minor updates --- packages/types/src/issues.d.ts | 2 +- .../issues/issue-layouts/kanban/default.tsx | 30 +-- .../issues/issue-layouts/kanban/swimlanes.tsx | 44 +--- .../issues/issue-layouts/list/default.tsx | 25 +- .../components/issues/issue-layouts/utils.tsx | 228 +++++++++--------- web/core/store/cycle.store.ts | 96 ++------ web/core/store/module.store.ts | 22 +- 7 files changed, 172 insertions(+), 275 deletions(-) diff --git a/packages/types/src/issues.d.ts b/packages/types/src/issues.d.ts index eff81f857..9bbfa36b1 100644 --- a/packages/types/src/issues.d.ts +++ b/packages/types/src/issues.d.ts @@ -216,7 +216,7 @@ export type GroupByColumnTypes = export interface IGroupByColumn { id: string; name: string; - icon: ReactElement | undefined; + icon?: ReactElement | undefined; payload: Partial; isDropDisabled?: boolean; dropErrorMessage?: string; diff --git a/web/core/components/issues/issue-layouts/kanban/default.tsx b/web/core/components/issues/issue-layouts/kanban/default.tsx index d1144995c..f1107c8fb 100644 --- a/web/core/components/issues/issue-layouts/kanban/default.tsx +++ b/web/core/components/issues/issue-layouts/kanban/default.tsx @@ -18,7 +18,7 @@ import { ContentWrapper } from "@plane/ui"; import RenderIfVisible from "@/components/core/render-if-visible-HOC"; import { KanbanColumnLoader } from "@/components/ui"; // hooks -import { useCycle, useKanbanView, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store"; +import { useKanbanView } from "@/hooks/store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; // types // parent components @@ -87,30 +87,16 @@ export const KanBan: React.FC = observer((props) => { dropErrorMessage, subGroupIndex = 0, } = props; - + // store hooks const storeType = useIssueStoreType(); - - const member = useMember(); - const project = useProject(); - const label = useLabel(); - const cycle = useCycle(); - const moduleInfo = useModule(); - const projectState = useProjectState(); const issueKanBanView = useKanbanView(); - + // derived values const isDragDisabled = !issueKanBanView?.getCanUserDragDrop(group_by, sub_group_by); - - const list = getGroupByColumns( - group_by as GroupByColumnTypes, - project, - cycle, - moduleInfo, - label, - projectState, - member, - true, - isWorkspaceLevel(storeType) - ); + const list = getGroupByColumns({ + groupBy: group_by as GroupByColumnTypes, + includeNone: true, + isWorkspaceLevel: isWorkspaceLevel(storeType), + }); if (!list) return null; diff --git a/web/core/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/core/components/issues/issue-layouts/kanban/swimlanes.tsx index ba1dce5c1..e2d52e7ca 100644 --- a/web/core/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/core/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -15,7 +15,6 @@ import { // UI import { Row } from "@plane/ui"; // hooks -import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; // components import { TRenderQuickActions } from "../list/list-view-types"; @@ -262,38 +261,19 @@ export const KanBanSwimLanes: React.FC = observer((props) => { quickAddCallback, scrollableContainerRef, } = props; - + // store hooks const storeType = useIssueStoreType(); - - const member = useMember(); - const project = useProject(); - const label = useLabel(); - const cycle = useCycle(); - const projectModule = useModule(); - const projectState = useProjectState(); - - const groupByList = getGroupByColumns( - group_by as GroupByColumnTypes, - project, - cycle, - projectModule, - label, - projectState, - member, - true, - isWorkspaceLevel(storeType) - ); - const subGroupByList = getGroupByColumns( - sub_group_by as GroupByColumnTypes, - project, - cycle, - projectModule, - label, - projectState, - member, - true, - isWorkspaceLevel(storeType) - ); + // derived values + const groupByList = getGroupByColumns({ + groupBy: group_by as GroupByColumnTypes, + includeNone: true, + isWorkspaceLevel: isWorkspaceLevel(storeType), + }); + const subGroupByList = getGroupByColumns({ + groupBy: sub_group_by as GroupByColumnTypes, + includeNone: true, + isWorkspaceLevel: isWorkspaceLevel(storeType), + }); if (!groupByList || !subGroupByList) return null; diff --git a/web/core/components/issues/issue-layouts/list/default.tsx b/web/core/components/issues/issue-layouts/list/default.tsx index fca8d68eb..01a81cc61 100644 --- a/web/core/components/issues/issue-layouts/list/default.tsx +++ b/web/core/components/issues/issue-layouts/list/default.tsx @@ -18,9 +18,7 @@ import { } from "@plane/types"; // components import { MultipleSelectGroup } from "@/components/core"; - // hooks -import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; // plane web components import { IssueBulkOperationsRoot } from "@/plane-web/components/issues"; @@ -75,29 +73,16 @@ export const List: React.FC = observer((props) => { } = props; const storeType = useIssueStoreType(); - // store hooks - const member = useMember(); - const project = useProject(); - const label = useLabel(); - const projectState = useProjectState(); - const cycle = useCycle(); - const projectModule = useModule(); // plane web hooks const isBulkOperationsEnabled = useBulkOperationStatus(); const containerRef = useRef(null); - const groups = getGroupByColumns( - group_by as GroupByColumnTypes, - project, - cycle, - projectModule, - label, - projectState, - member, - true, - isWorkspaceLevel(storeType) - ); + const groups = getGroupByColumns({ + groupBy: group_by as GroupByColumnTypes, + includeNone: true, + isWorkspaceLevel: isWorkspaceLevel(storeType), + }); // Enable Auto Scroll for Main Kanban useEffect(() => { diff --git a/web/core/components/issues/issue-layouts/utils.tsx b/web/core/components/issues/issue-layouts/utils.tsx index 21f40a5bc..9cb6fa8f5 100644 --- a/web/core/components/issues/issue-layouts/utils.tsx +++ b/web/core/components/issues/issue-layouts/utils.tsx @@ -36,13 +36,8 @@ import { STATE_GROUPS } from "@/constants/state"; import { renderFormattedDate } from "@/helpers/date-time.helper"; import { getFileURL } from "@/helpers/file.helper"; // store -import { ICycleStore } from "@/store/cycle.store"; +import { store } from "@/lib/store-context"; import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/base-issues.store"; -import { ILabelStore } from "@/store/label.store"; -import { IMemberRootStore } from "@/store/member"; -import { IModuleStore } from "@/store/module.store"; -import { IProjectStore } from "@/store/project/project.store"; -import { IStateStore } from "@/store/state.store"; export const HIGHLIGHT_CLASS = "highlight"; export const HIGHLIGHT_WITH_LINE = "highlight-with-line"; @@ -65,51 +60,61 @@ export type IssueUpdates = { export const isWorkspaceLevel = (type: EIssuesStoreType) => [EIssuesStoreType.PROFILE, EIssuesStoreType.GLOBAL].includes(type) ? true : false; -export const getGroupByColumns = ( - groupBy: GroupByColumnTypes | null, - project: IProjectStore, - cycle: ICycleStore, - module: IModuleStore, - label: ILabelStore, - projectState: IStateStore, - member: IMemberRootStore, - includeNone?: boolean, - isWorkspaceLevel?: boolean -): IGroupByColumn[] | undefined => { - switch (groupBy) { - case "project": - return getProjectColumns(project); - case "cycle": - return getCycleColumns(project, cycle); - case "module": - return getModuleColumns(project, module); - case "state": - return getStateColumns(projectState); - case "state_detail.group": - return getStateGroupColumns(); - case "priority": - return getPriorityColumns(); - case "labels": - return getLabelsColumns(label, isWorkspaceLevel) as any; - case "assignees": - return getAssigneeColumns(member) as any; - case "created_by": - return getCreatedByColumns(member) as any; - default: - if (includeNone) return [{ id: `All Issues`, name: `All Issues`, payload: {}, icon: undefined }]; - } +type TGetGroupByColumns = { + groupBy: GroupByColumnTypes | null; + includeNone: boolean; + isWorkspaceLevel: boolean; }; -const getProjectColumns = (project: IProjectStore): IGroupByColumn[] | undefined => { - const { workspaceProjectIds: projectIds, projectMap } = project; +// NOTE: Type of groupBy is different compared to what's being passed from the components. +// We are using `as` to typecast it to the expected type. +// It can break the includeNone logic if not handled properly. +export const getGroupByColumns = ({ + groupBy, + includeNone, + isWorkspaceLevel, +}: TGetGroupByColumns): IGroupByColumn[] | undefined => { + // If no groupBy is specified and includeNone is true, return "All Issues" group + if (!groupBy && includeNone) { + return [ + { + id: "All Issues", + name: "All Issues", + payload: {}, + icon: undefined, + }, + ]; + } + // Return undefined if no valid groupBy + if (!groupBy) return undefined; + + // Map of group by options to their corresponding column getter functions + const groupByColumnMap: Record IGroupByColumn[] | undefined> = { + project: getProjectColumns, + cycle: getCycleColumns, + module: getModuleColumns, + state: getStateColumns, + "state_detail.group": getStateGroupColumns, + priority: getPriorityColumns, + labels: () => getLabelsColumns(isWorkspaceLevel), + assignees: getAssigneeColumns, + created_by: getCreatedByColumns, + }; + + // Get and return the columns for the specified group by option + return groupByColumnMap[groupBy]?.(); +}; + +const getProjectColumns = (): IGroupByColumn[] | undefined => { + const { joinedProjectIds: projectIds, projectMap } = store.projectRoot.project; + // Return undefined if no project ids if (!projectIds) return; - + // Map project ids to project columns return projectIds - .filter((projectId) => !!projectMap[projectId]) - .map((projectId) => { + .map((projectId: string) => { const project = projectMap[projectId]; - + if (!project) return; return { id: project.id, name: project.name, @@ -120,78 +125,71 @@ const getProjectColumns = (project: IProjectStore): IGroupByColumn[] | undefined ), payload: { project_id: project.id }, }; - }) as any; + }) + .filter((column) => column !== undefined) as IGroupByColumn[]; }; -const getCycleColumns = (projectStore: IProjectStore, cycleStore: ICycleStore): IGroupByColumn[] | undefined => { - const { currentProjectDetails } = projectStore; - const { getProjectCycleIds, getCycleById } = cycleStore; - +const getCycleColumns = (): IGroupByColumn[] | undefined => { + const { currentProjectDetails } = store.projectRoot.project; + // Check for the current project details if (!currentProjectDetails || !currentProjectDetails?.id) return; - - const cycleIds = currentProjectDetails?.id ? getProjectCycleIds(currentProjectDetails?.id) : undefined; - if (!cycleIds) return; - - const cycles = []; - - cycleIds.map((cycleId) => { - const cycle = getCycleById(cycleId); - if (cycle) { - const cycleStatus = cycle.status ? (cycle.status.toLocaleLowerCase() as TCycleGroups) : "draft"; - const isDropDisabled = cycleStatus === "completed"; - cycles.push({ - id: cycle.id, - name: cycle.name, - icon: , - payload: { cycle_id: cycle.id }, - isDropDisabled, - dropErrorMessage: isDropDisabled ? "Issue cannot be moved to completed cycles" : undefined, - }); - } + const { getProjectCycleDetails } = store.cycle; + // Get the cycle details for the current project + const cycleDetails = currentProjectDetails?.id ? getProjectCycleDetails(currentProjectDetails?.id) : undefined; + // Map the cycle details to the group by columns + const cycles: IGroupByColumn[] = []; + cycleDetails?.map((cycle) => { + const cycleStatus = cycle.status ? (cycle.status.toLocaleLowerCase() as TCycleGroups) : "draft"; + const isDropDisabled = cycleStatus === "completed"; + cycles.push({ + id: cycle.id, + name: cycle.name, + icon: , + payload: { cycle_id: cycle.id }, + isDropDisabled, + dropErrorMessage: isDropDisabled ? "Issue cannot be moved to completed cycles" : undefined, + }); }); cycles.push({ id: "None", name: "None", icon: , + payload: {}, }); - - return cycles as any; + return cycles; }; -const getModuleColumns = (projectStore: IProjectStore, moduleStore: IModuleStore): IGroupByColumn[] | undefined => { - const { currentProjectDetails } = projectStore; - const { getProjectModuleIds, getModuleById } = moduleStore; - +const getModuleColumns = (): IGroupByColumn[] | undefined => { + // get current project details + const { currentProjectDetails } = store.projectRoot.project; if (!currentProjectDetails || !currentProjectDetails?.id) return; - - const moduleIds = currentProjectDetails?.id ? getProjectModuleIds(currentProjectDetails?.id) : undefined; - if (!moduleIds) return; - - const modules = []; - - moduleIds.map((moduleId) => { - const moduleInfo = getModuleById(moduleId); - if (moduleInfo) - modules.push({ - id: moduleInfo.id, - name: moduleInfo.name, - icon: , - payload: { module_ids: [moduleInfo.id] }, - }); - }) as any; + // get project module ids and module details + const { getProjectModuleDetails } = store.module; + // get module details + const moduleDetails = currentProjectDetails?.id ? getProjectModuleDetails(currentProjectDetails?.id) : undefined; + // map module details to group by columns + const modules: IGroupByColumn[] = []; + moduleDetails?.map((module) => { + modules.push({ + id: module.id, + name: module.name, + icon: , + payload: { module_ids: [module.id] }, + }); + }); modules.push({ id: "None", name: "None", icon: , + payload: {}, }); - - return modules as any; + return modules; }; -const getStateColumns = (projectState: IStateStore): IGroupByColumn[] | undefined => { - const { projectStates } = projectState; +const getStateColumns = (): IGroupByColumn[] | undefined => { + const { projectStates } = store.state; if (!projectStates) return; - + // map project states to group by columns return projectStates.map((state) => ({ id: state.id, name: state.name, @@ -201,12 +199,12 @@ const getStateColumns = (projectState: IStateStore): IGroupByColumn[] | undefine ), payload: { state_id: state.id }, - })) as any; + })); }; -const getStateGroupColumns = () => { +const getStateGroupColumns = (): IGroupByColumn[] => { const stateGroups = STATE_GROUPS; - + // map state groups to group by columns return Object.values(stateGroups).map((stateGroup) => ({ id: stateGroup.key, name: stateGroup.label, @@ -219,9 +217,9 @@ const getStateGroupColumns = () => { })); }; -const getPriorityColumns = () => { +const getPriorityColumns = (): IGroupByColumn[] => { const priorities = ISSUE_PRIORITIES; - + // map priorities to group by columns return priorities.map((priority) => ({ id: priority.key, name: priority.title, @@ -230,14 +228,14 @@ const getPriorityColumns = () => { })); }; -const getLabelsColumns = (label: ILabelStore, isWorkspaceLevel: boolean = false) => { - const { workspaceLabels, projectLabels } = label; - +const getLabelsColumns = (isWorkspaceLevel: boolean = false): IGroupByColumn[] => { + const { workspaceLabels, projectLabels } = store.label; + // map labels to group by columns const labels = [ ...(isWorkspaceLevel ? workspaceLabels || [] : projectLabels || []), { id: "None", name: "None", color: "#666" }, ]; - + // map labels to group by columns return labels.map((label) => ({ id: label.id, name: label.name, @@ -248,15 +246,14 @@ const getLabelsColumns = (label: ILabelStore, isWorkspaceLevel: boolean = false) })); }; -const getAssigneeColumns = (member: IMemberRootStore) => { +const getAssigneeColumns = (): IGroupByColumn[] | undefined => { const { project: { projectMemberIds }, getUserDetails, - } = member; - + } = store.memberRoot; if (!projectMemberIds) return; - - const assigneeColumns: any = projectMemberIds.map((memberId) => { + // Map project member ids to group by assignee columns + const assigneeColumns: IGroupByColumn[] = projectMemberIds.map((memberId) => { const member = getUserDetails(memberId); return { id: memberId, @@ -265,20 +262,17 @@ const getAssigneeColumns = (member: IMemberRootStore) => { payload: { assignee_ids: [memberId] }, }; }); - assigneeColumns.push({ id: "None", name: "None", icon: , payload: {} }); - return assigneeColumns; }; -const getCreatedByColumns = (member: IMemberRootStore) => { +const getCreatedByColumns = (): IGroupByColumn[] | undefined => { const { project: { projectMemberIds }, getUserDetails, - } = member; - + } = store.memberRoot; if (!projectMemberIds) return; - + // Map project member ids to group by created by columns return projectMemberIds.map((memberId) => { const member = getUserDetails(memberId); return { diff --git a/web/core/store/cycle.store.ts b/web/core/store/cycle.store.ts index dd90cfe1d..4e56e56c1 100644 --- a/web/core/store/cycle.store.ts +++ b/web/core/store/cycle.store.ts @@ -1,4 +1,4 @@ -import { isFuture, isPast, isToday } from "date-fns"; +import { isPast, isToday } from "date-fns"; import isEmpty from "lodash/isEmpty"; import set from "lodash/set"; import sortBy from "lodash/sortBy"; @@ -7,16 +7,14 @@ import { computedFn } from "mobx-utils"; // types import { ICycle, - CycleDateCheckData, TCyclePlotType, TProgressSnapshot, TCycleEstimateDistribution, TCycleDistribution, TCycleEstimateType, - TCycleProgress, } from "@plane/types"; // helpers -import { orderCycles, shouldFilterCycle, formatActiveCycle } from "@/helpers/cycle.helper"; +import { orderCycles, shouldFilterCycle } from "@/helpers/cycle.helper"; import { getDate } from "@/helpers/date-time.helper"; import { DistributionUpdates, updateDistribution } from "@/helpers/distribution-update.helper"; // services @@ -42,21 +40,18 @@ export interface ICycleStore { // computed currentProjectCycleIds: string[] | null; currentProjectCompletedCycleIds: string[] | null; - currentProjectUpcomingCycleIds: string[] | null; currentProjectIncompleteCycleIds: string[] | null; - currentProjectDraftCycleIds: string[] | null; currentProjectActiveCycleId: string | null; currentProjectArchivedCycleIds: string[] | null; currentProjectActiveCycle: ICycle | null; // computed actions - getActiveCycleProgress: (cycleId?: string) => { cycle: ICycle; isBurnDown: boolean; isTypeIssue: boolean } | null; getFilteredCycleIds: (projectId: string, sortByManual: boolean) => string[] | null; getFilteredCompletedCycleIds: (projectId: string) => string[] | null; getFilteredArchivedCycleIds: (projectId: string) => string[] | null; getCycleById: (cycleId: string) => ICycle | null; getCycleNameById: (cycleId: string) => string | undefined; - getActiveCycleById: (cycleId: string) => ICycle | null; + getProjectCycleDetails: (projectId: string) => ICycle[] | null; getProjectCycleIds: (projectId: string) => string[] | null; getPlotTypeByCycleId: (cycleId: string) => TCyclePlotType; getEstimateTypeByCycleId: (cycleId: string) => TCycleEstimateType; @@ -64,7 +59,6 @@ export interface ICycleStore { // actions updateCycleDistribution: (distributionUpdates: DistributionUpdates, cycleId: string) => void; - validateDate: (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => Promise; setPlotType: (cycleId: string, plotType: TCyclePlotType) => void; setEstimateType: (cycleId: string, estimateType: TCycleEstimateType) => void; // fetch @@ -130,15 +124,12 @@ export class CycleStore implements ICycleStore { // computed currentProjectCycleIds: computed, currentProjectCompletedCycleIds: computed, - currentProjectUpcomingCycleIds: computed, currentProjectIncompleteCycleIds: computed, - currentProjectDraftCycleIds: computed, currentProjectActiveCycleId: computed, currentProjectArchivedCycleIds: computed, currentProjectActiveCycle: computed, // actions - setPlotType: action, setEstimateType: action, fetchWorkspaceCycles: action, fetchAllCycles: action, @@ -195,22 +186,6 @@ export class CycleStore implements ICycleStore { return completedCycleIds; } - /** - * returns all upcoming cycle ids for a project - */ - get currentProjectUpcomingCycleIds() { - const projectId = this.rootStore.router.projectId; - if (!projectId || !this.fetchedMap[projectId]) return null; - let upcomingCycles = Object.values(this.cycleMap ?? {}).filter((c) => { - const startDate = getDate(c.start_date); - const isStartDateUpcoming = startDate && isFuture(startDate); - return c.project_id === projectId && isStartDateUpcoming && !c?.archived_at; - }); - upcomingCycles = sortBy(upcomingCycles, [(c) => c.sort_order]); - const upcomingCycleIds = upcomingCycles.map((c) => c.id); - return upcomingCycleIds; - } - /** * returns all incomplete cycle ids for a project */ @@ -227,20 +202,6 @@ export class CycleStore implements ICycleStore { return incompleteCycleIds; } - /** - * returns all draft cycle ids for a project - */ - get currentProjectDraftCycleIds() { - const projectId = this.rootStore.router.projectId; - if (!projectId || !this.fetchedMap[projectId]) return null; - let draftCycles = Object.values(this.cycleMap ?? {}).filter( - (c) => c.project_id === projectId && !c.start_date && !c.end_date && !c?.archived_at - ); - draftCycles = sortBy(draftCycles, [(c) => c.sort_order]); - const draftCycleIds = draftCycles.map((c) => c.id); - return draftCycleIds; - } - /** * returns active cycle id for a project */ @@ -285,19 +246,6 @@ export class CycleStore implements ICycleStore { } else return false; }); - /** - * returns active cycle progress for a project - */ - getActiveCycleProgress = computedFn((cycleId?: string) => { - const cycle = cycleId ? this.cycleMap[cycleId] : this.currentProjectActiveCycle; - if (!cycle) return null; - - const isTypeIssue = this.getEstimateTypeByCycleId(cycle.id) === "issues"; - const isBurnDown = this.getPlotTypeByCycleId(cycle.id) === "burndown"; - - return { cycle, isTypeIssue, isBurnDown }; - }); - /** * @description returns filtered cycle ids based on display filters and filters * @param {TCycleDisplayFilters} displayFilters @@ -379,37 +327,28 @@ export class CycleStore implements ICycleStore { getCycleNameById = computedFn((cycleId: string): string => this.cycleMap?.[cycleId]?.name); /** - * @description returns active cycle details by cycle id - * @param cycleId - * @returns + * @description returns list of cycle details of the project id passed as argument + * @param projectId */ - getActiveCycleById = computedFn((cycleId: string): ICycle | null => - this.activeCycleIdMap?.[cycleId] && this.cycleMap?.[cycleId] ? this.cycleMap?.[cycleId] : null - ); + getProjectCycleDetails = computedFn((projectId: string): ICycle[] | null => { + if (!this.fetchedMap[projectId]) return null; + + let cycles = Object.values(this.cycleMap ?? {}).filter((c) => c.project_id === projectId && !c?.archived_at); + cycles = sortBy(cycles, [(c) => c.sort_order]); + return cycles || null; + }); /** * @description returns list of cycle ids of the project id passed as argument * @param projectId */ getProjectCycleIds = computedFn((projectId: string): string[] | null => { - if (!this.fetchedMap[projectId]) return null; - - let cycles = Object.values(this.cycleMap ?? {}).filter((c) => c.project_id === projectId && !c?.archived_at); - cycles = sortBy(cycles, [(c) => c.sort_order]); + const cycles = this.getProjectCycleDetails(projectId); + if (!cycles) return null; const cycleIds = cycles.map((c) => c.id); return cycleIds || null; }); - /** - * @description validates cycle dates - * @param workspaceSlug - * @param projectId - * @param payload - * @returns - */ - validateDate = async (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => - await this.cycleService.cycleDateCheck(workspaceSlug, projectId, payload); - /** * @description gets the plot type for the cycle store * @param {TCyclePlotType} plotType @@ -473,14 +412,16 @@ export class CycleStore implements ICycleStore { runInAction(() => { response.forEach((cycle) => { set(this.cycleMap, [cycle.id], cycle); - cycle.status?.toLowerCase() === "current" && set(this.activeCycleIdMap, [cycle.id], true); + if (cycle.status?.toLowerCase() === "current") { + set(this.activeCycleIdMap, [cycle.id], true); + } }); set(this.fetchedMap, projectId, true); this.loader = false; }); return response; }); - } catch (error) { + } catch { this.loader = false; return undefined; } @@ -553,6 +494,7 @@ export class CycleStore implements ICycleStore { * @param cycleId * @returns */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars fetchActiveCycleProgressPro = action(async (workspaceSlug: string, projectId: string, cycleId: string) => {}); /** diff --git a/web/core/store/module.store.ts b/web/core/store/module.store.ts index 787b8b091..06a08b92c 100644 --- a/web/core/store/module.store.ts +++ b/web/core/store/module.store.ts @@ -32,6 +32,7 @@ export interface IModuleStore { getFilteredArchivedModuleIds: (projectId: string) => string[] | null; getModuleById: (moduleId: string) => IModule | null; getModuleNameById: (moduleId: string) => string; + getProjectModuleDetails: (projectId: string) => IModule[] | null; getProjectModuleIds: (projectId: string) => string[] | null; getPlotTypeByModuleId: (moduleId: string) => TModulePlotType; // actions @@ -210,15 +211,24 @@ export class ModulesStore implements IModuleStore { */ getModuleNameById = computedFn((moduleId: string) => this.moduleMap?.[moduleId]?.name); + /** + * @description returns list of module details of the project id passed as argument + * @param projectId + */ + getProjectModuleDetails = computedFn((projectId: string) => { + if (!this.fetchedMap[projectId]) return null; + let projectModules = Object.values(this.moduleMap).filter((m) => m.project_id === projectId && !m.archived_at); + projectModules = sortBy(projectModules, [(m) => m.sort_order]); + return projectModules; + }); + /** * @description returns list of module ids of the project id passed as argument * @param projectId */ getProjectModuleIds = computedFn((projectId: string) => { - if (!this.fetchedMap[projectId]) return null; - - let projectModules = Object.values(this.moduleMap).filter((m) => m.project_id === projectId && !m.archived_at); - projectModules = sortBy(projectModules, [(m) => m.sort_order]); + const projectModules = this.getProjectModuleDetails(projectId); + if (!projectModules) return null; const projectModuleIds = projectModules.map((m) => m.id); return projectModuleIds; }); @@ -282,7 +292,7 @@ export class ModulesStore implements IModuleStore { }); return response; }); - } catch (error) { + } catch { this.loader = false; return undefined; } @@ -308,7 +318,7 @@ export class ModulesStore implements IModuleStore { }); return projectModules; }); - } catch (error) { + } catch { this.loader = false; return undefined; } From 7fca7fd86cef2df01e132bf578761695bab5e724 Mon Sep 17 00:00:00 2001 From: Vamsi Krishna <46787868+mathalav55@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:29:39 +0530 Subject: [PATCH 004/355] [WEB-2774] fix:favorites reorder (#6179) * fix:favorites reorder * chore: added error handling Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../sidebar/favorites/favorite-folder.tsx | 11 ++++-- .../sidebar/favorites/favorite-items/root.tsx | 2 +- .../sidebar/favorites/favorites-menu.tsx | 20 ++++------- web/core/store/favorite.store.ts | 34 +++++-------------- 4 files changed, 25 insertions(+), 42 deletions(-) diff --git a/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx b/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx index 6fc2c1b52..8806b2d08 100644 --- a/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx @@ -27,6 +27,7 @@ import { CustomMenu, Tooltip, DropIndicator, FavoriteFolderIcon, DragHandle } fr import { cn } from "@/helpers/common.helper"; // hooks import { useAppTheme } from "@/hooks/store"; +import { useFavorite } from "@/hooks/store/use-favorite"; import { usePlatformOS } from "@/hooks/use-platform-os"; // constants import { FavoriteRoot } from "./favorite-items"; @@ -45,7 +46,7 @@ export const FavoriteFolder: React.FC = (props) => { const { favorite, handleRemoveFromFavorites, isLastChild, handleDrop } = props; // store hooks const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme(); - + const { getGroupedFavorites } = useFavorite(); const { isMobile } = usePlatformOS(); const { workspaceSlug } = useParams(); // states @@ -58,6 +59,12 @@ export const FavoriteFolder: React.FC = (props) => { const actionSectionRef = useRef(null); const elementRef = useRef(null); + useEffect(() => { + if (favorite.children === undefined && workspaceSlug) { + getGroupedFavorites(workspaceSlug.toString(), favorite.id); + } + }, [favorite.id, favorite.children, workspaceSlug, getGroupedFavorites]); + useEffect(() => { const element = elementRef.current; @@ -123,7 +130,7 @@ export const FavoriteFolder: React.FC = (props) => { }) ); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isDragging, favorite.id]); + }, [isDragging, favorite.id, isLastChild, favorite.id]); useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false)); diff --git a/web/core/components/workspace/sidebar/favorites/favorite-items/root.tsx b/web/core/components/workspace/sidebar/favorites/favorite-items/root.tsx index 2ae16528a..49931802e 100644 --- a/web/core/components/workspace/sidebar/favorites/favorite-items/root.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorite-items/root.tsx @@ -131,7 +131,7 @@ export const FavoriteRoot: FC = observer((props) => { }) ); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [elementRef?.current, isDragging]); + }, [elementRef?.current, isDragging, isLastChild, favorite.id]); useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false)); diff --git a/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx b/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx index 8aea968ec..6773e3a64 100644 --- a/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx @@ -87,33 +87,27 @@ export const SidebarFavoritesMenu = observer(() => { const sourceData = source.data as TargetData; if (!sourceData.id) return; - if (isFolder) { // handle move to a new parent folder if dropped on a folder if (parentId && parentId !== sourceData.parentId) { - handleMoveToFolder(sourceData.id, parentId); + handleMoveToFolder(sourceData.id, parentId); /**parent id */ } - //handle remove from folder if dropped outside of the folder - if (parentId && parentId !== sourceData.parentId && sourceData.isChild) { - handleRemoveFromFavoritesFolder(sourceData.id); - } - // handle reordering at root level if (droppedFavId) { if (instruction != "make-child") { - handleReorder(sourceData.id, droppedFavId, instruction); + handleReorder(sourceData.id, droppedFavId, instruction); /** sequence */ } } } else { //handling reordering for favorites if (droppedFavId) { - handleReorder(sourceData.id, droppedFavId, instruction); + handleReorder(sourceData.id, droppedFavId, instruction); /** sequence */ } + } - // handle removal from folder if dropped outside a folder - if (!parentId && sourceData.isChild) { - handleRemoveFromFavoritesFolder(sourceData.id); - } + /**remove if dropped outside and source is a child */ + if (!parentId && sourceData.isChild) { + handleRemoveFromFavoritesFolder(sourceData.id); /**parent null */ } }; diff --git a/web/core/store/favorite.store.ts b/web/core/store/favorite.store.ts index 3f4f636d3..a588f2af3 100644 --- a/web/core/store/favorite.store.ts +++ b/web/core/store/favorite.store.ts @@ -174,23 +174,14 @@ export class FavoriteStore implements IFavoriteStore { * @returns Promise */ moveFavoriteToFolder = async (workspaceSlug: string, favoriteId: string, data: Partial) => { - const oldParent = this.favoriteMap[favoriteId].parent; try { + await this.favoriteService.updateFavorite(workspaceSlug, favoriteId, data); runInAction(() => { // add parent of the favorite set(this.favoriteMap, [favoriteId, "parent"], data.parent); }); - await this.favoriteService.updateFavorite(workspaceSlug, favoriteId, data); } catch (error) { - console.error("Failed to move favorite from favorite store"); - - // revert the changes - runInAction(() => { - if (!data.parent) return; - - // revert the parent - set(this.favoriteMap, [favoriteId, "parent"], oldParent); - }); + console.error("Failed to move favorite to folder", error); throw error; } }; @@ -201,7 +192,6 @@ export class FavoriteStore implements IFavoriteStore { destinationId: string, edge: string | undefined ) => { - const initialSequence = this.favoriteMap[favoriteId].sequence; try { let resultSequence = 10000; if (edge) { @@ -221,35 +211,27 @@ export class FavoriteStore implements IFavoriteStore { } } } + + await this.favoriteService.updateFavorite(workspaceSlug, favoriteId, { sequence: resultSequence }); + runInAction(() => { set(this.favoriteMap, [favoriteId, "sequence"], resultSequence); }); - - await this.favoriteService.updateFavorite(workspaceSlug, favoriteId, { sequence: resultSequence }); } catch (error) { console.error("Failed to move favorite folder"); - runInAction(() => { - set(this.favoriteMap, [favoriteId, "sequence"], initialSequence); - throw error; - }); + throw error; } }; removeFromFavoriteFolder = async (workspaceSlug: string, favoriteId: string) => { - const parent = this.favoriteMap[favoriteId].parent; try { + await this.favoriteService.updateFavorite(workspaceSlug, favoriteId, { parent: null }); runInAction(() => { //remove parent set(this.favoriteMap, [favoriteId, "parent"], null); }); - await this.favoriteService.updateFavorite(workspaceSlug, favoriteId, { parent: null }); } catch (error) { console.error("Failed to move favorite"); - runInAction(() => { - set(this.favoriteMap, [favoriteId, "parent"], parent); - - throw error; - }); throw error; } }; @@ -384,7 +366,7 @@ export class FavoriteStore implements IFavoriteStore { set(this.favoriteMap, [favorite.id], favorite); this.favoriteIds.push(favorite.id); this.favoriteIds = uniqBy(this.favoriteIds, (id) => id); - favorite.entity_identifier && set(this.entityMap, [favorite.entity_identifier], favorite); + if (favorite.entity_identifier) set(this.entityMap, [favorite.entity_identifier], favorite); }); }); From ca0d50b22909d0d5a0f414e2f551952378ee4823 Mon Sep 17 00:00:00 2001 From: Vamsi Krishna <46787868+mathalav55@users.noreply.github.com> Date: Wed, 11 Dec 2024 17:57:27 +0530 Subject: [PATCH 005/355] fix: no activity while moving inbox issues (#6185) --- .../issue-detail/issue-activity/activity/activity-list.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/core/components/issues/issue-detail/issue-activity/activity/activity-list.tsx b/web/core/components/issues/issue-detail/issue-activity/activity/activity-list.tsx index 148cf1f26..d8d0b59dd 100644 --- a/web/core/components/issues/issue-detail/issue-activity/activity/activity-list.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/activity/activity-list.tsx @@ -82,6 +82,7 @@ export const IssueActivityItem: FC = observer((props) => { return ; case "archived_at": return ; + case "intake": case "inbox": return ; case "type": From a9bd2e243af33a5f8bc92c9e533e0328a2c15731 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Wed, 11 Dec 2024 18:02:58 +0530 Subject: [PATCH 006/355] refactor: enhance command palette modularity (#6139) * refactor: enhance command palette modularity * chore: minor updates to command palette store --- packages/types/src/command-palette.d.ts | 15 ++ packages/types/src/index.d.ts | 1 + .../command-palette/modals/index.ts | 3 + .../command-palette/modals/issue-level.tsx | 73 +++++++ .../command-palette/modals/project-level.tsx | 59 +++++ .../modals/workspace-level.tsx | 25 +++ web/ce/helpers/command-palette.ts | 95 ++++++++ web/ce/store/command-palette.store.ts | 12 + .../command-palette/command-palette.tsx | 206 +++--------------- .../shortcuts-modal/commands-list.tsx | 30 ++- web/core/hooks/store/use-command-palette.ts | 2 +- ...store.ts => base-command-palette.store.ts} | 28 ++- web/core/store/root.store.ts | 2 +- .../command-palette/modals/index.ts | 1 + web/ee/store/command-palette.store.ts | 1 + web/helpers/command-palette.ts | 1 + 16 files changed, 342 insertions(+), 212 deletions(-) create mode 100644 packages/types/src/command-palette.d.ts create mode 100644 web/ce/components/command-palette/modals/index.ts create mode 100644 web/ce/components/command-palette/modals/issue-level.tsx create mode 100644 web/ce/components/command-palette/modals/project-level.tsx create mode 100644 web/ce/components/command-palette/modals/workspace-level.tsx create mode 100644 web/ce/helpers/command-palette.ts create mode 100644 web/ce/store/command-palette.store.ts rename web/core/store/{command-palette.store.ts => base-command-palette.store.ts} (92%) create mode 100644 web/ee/components/command-palette/modals/index.ts create mode 100644 web/ee/store/command-palette.store.ts create mode 100644 web/helpers/command-palette.ts diff --git a/packages/types/src/command-palette.d.ts b/packages/types/src/command-palette.d.ts new file mode 100644 index 000000000..6e072ab8f --- /dev/null +++ b/packages/types/src/command-palette.d.ts @@ -0,0 +1,15 @@ +export type TCommandPaletteActionList = Record< + string, + { title: string; description: string; action: () => void } +>; + +export type TCommandPaletteShortcutList = { + key: string; + title: string; + shortcuts: TCommandPaletteShortcut[]; +}; + +export type TCommandPaletteShortcut = { + keys: string; // comma separated keys + description: string; +}; diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index 10e519700..9c66c629a 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -32,3 +32,4 @@ export * from "./workspace-notifications"; export * from "./favorite"; export * from "./file"; export * from "./workspace-draft-issues/base"; +export * from "./command-palette"; diff --git a/web/ce/components/command-palette/modals/index.ts b/web/ce/components/command-palette/modals/index.ts new file mode 100644 index 000000000..a4fac4b91 --- /dev/null +++ b/web/ce/components/command-palette/modals/index.ts @@ -0,0 +1,3 @@ +export * from "./workspace-level"; +export * from "./project-level"; +export * from "./issue-level"; diff --git a/web/ce/components/command-palette/modals/issue-level.tsx b/web/ce/components/command-palette/modals/issue-level.tsx new file mode 100644 index 000000000..84a7dddc6 --- /dev/null +++ b/web/ce/components/command-palette/modals/issue-level.tsx @@ -0,0 +1,73 @@ +import { observer } from "mobx-react"; +import { useParams, usePathname } from "next/navigation"; +import useSWR from "swr"; +// components +import { BulkDeleteIssuesModal } from "@/components/core"; +import { CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues"; +// constants +import { ISSUE_DETAILS } from "@/constants/fetch-keys"; +// hooks +import { useCommandPalette, useUser } from "@/hooks/store"; +import { useAppRouter } from "@/hooks/use-app-router"; +import { useIssuesStore } from "@/hooks/use-issue-layout-store"; +// services +import { IssueService } from "@/services/issue"; + +// services +const issueService = new IssueService(); + +export const IssueLevelModals = observer(() => { + // router + const pathname = usePathname(); + const { workspaceSlug, projectId, issueId, cycleId, moduleId } = useParams(); + const router = useAppRouter(); + // store hooks + const { data: currentUser } = useUser(); + const { + issues: { removeIssue }, + } = useIssuesStore(); + const { + isCreateIssueModalOpen, + toggleCreateIssueModal, + isDeleteIssueModalOpen, + toggleDeleteIssueModal, + isBulkDeleteIssueModalOpen, + toggleBulkDeleteIssueModal, + } = useCommandPalette(); + // derived values + const isDraftIssue = pathname?.includes("draft-issues") || false; + + const { data: issueDetails } = useSWR( + workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null, + workspaceSlug && projectId && issueId + ? () => issueService.retrieve(workspaceSlug as string, projectId as string, issueId as string) + : null + ); + + return ( + <> + toggleCreateIssueModal(false)} + data={cycleId ? { cycle_id: cycleId.toString() } : moduleId ? { module_ids: [moduleId.toString()] } : undefined} + isDraft={isDraftIssue} + /> + {workspaceSlug && projectId && issueId && issueDetails && ( + toggleDeleteIssueModal(false)} + isOpen={isDeleteIssueModalOpen} + data={issueDetails} + onSubmit={async () => { + await removeIssue(workspaceSlug.toString(), projectId.toString(), issueId.toString()); + router.push(`/${workspaceSlug}/projects/${projectId}/issues`); + }} + /> + )} + toggleBulkDeleteIssueModal(false)} + user={currentUser} + /> + + ); +}); diff --git a/web/ce/components/command-palette/modals/project-level.tsx b/web/ce/components/command-palette/modals/project-level.tsx new file mode 100644 index 000000000..324af8d48 --- /dev/null +++ b/web/ce/components/command-palette/modals/project-level.tsx @@ -0,0 +1,59 @@ +import { observer } from "mobx-react"; +// components +import { CycleCreateUpdateModal } from "@/components/cycles"; +import { CreateUpdateModuleModal } from "@/components/modules"; +import { CreatePageModal } from "@/components/pages"; +import { CreateUpdateProjectViewModal } from "@/components/views"; +// hooks +import { useCommandPalette } from "@/hooks/store"; + +export type TProjectLevelModalsProps = { + workspaceSlug: string; + projectId: string; +}; + +export const ProjectLevelModals = observer((props: TProjectLevelModalsProps) => { + const { workspaceSlug, projectId } = props; + // store hooks + const { + isCreateCycleModalOpen, + toggleCreateCycleModal, + isCreateModuleModalOpen, + toggleCreateModuleModal, + isCreateViewModalOpen, + toggleCreateViewModal, + createPageModal, + toggleCreatePageModal, + } = useCommandPalette(); + + return ( + <> + toggleCreateCycleModal(false)} + workspaceSlug={workspaceSlug.toString()} + projectId={projectId.toString()} + /> + toggleCreateModuleModal(false)} + workspaceSlug={workspaceSlug.toString()} + projectId={projectId.toString()} + /> + toggleCreateViewModal(false)} + workspaceSlug={workspaceSlug.toString()} + projectId={projectId.toString()} + /> + toggleCreatePageModal({ isOpen: false })} + redirectionEnabled + /> + + ); +}); diff --git a/web/ce/components/command-palette/modals/workspace-level.tsx b/web/ce/components/command-palette/modals/workspace-level.tsx new file mode 100644 index 000000000..64d22493e --- /dev/null +++ b/web/ce/components/command-palette/modals/workspace-level.tsx @@ -0,0 +1,25 @@ +import { observer } from "mobx-react"; +// components +import { CreateProjectModal } from "@/components/project"; +// hooks +import { useCommandPalette } from "@/hooks/store"; + +export type TWorkspaceLevelModalsProps = { + workspaceSlug: string; +}; + +export const WorkspaceLevelModals = observer((props: TWorkspaceLevelModalsProps) => { + const { workspaceSlug } = props; + // store hooks + const { isCreateProjectModalOpen, toggleCreateProjectModal } = useCommandPalette(); + + return ( + <> + toggleCreateProjectModal(false)} + workspaceSlug={workspaceSlug.toString()} + /> + + ); +}); diff --git a/web/ce/helpers/command-palette.ts b/web/ce/helpers/command-palette.ts new file mode 100644 index 000000000..fccfcbaa4 --- /dev/null +++ b/web/ce/helpers/command-palette.ts @@ -0,0 +1,95 @@ +// types +import { TCommandPaletteActionList, TCommandPaletteShortcut, TCommandPaletteShortcutList } from "@plane/types"; +// store +import { store } from "@/lib/store-context"; + +export const getGlobalShortcutsList: () => TCommandPaletteActionList = () => { + const { toggleCreateIssueModal } = store.commandPalette; + + return { + c: { + title: "Create a new issue", + description: "Create a new issue in the current project", + action: () => toggleCreateIssueModal(true), + }, + }; +}; + +export const getWorkspaceShortcutsList: () => TCommandPaletteActionList = () => { + const { toggleCreateProjectModal } = store.commandPalette; + + return { + p: { + title: "Create a new project", + description: "Create a new project in the current workspace", + action: () => toggleCreateProjectModal(true), + }, + }; +}; + +export const getProjectShortcutsList: () => TCommandPaletteActionList = () => { + const { + toggleCreatePageModal, + toggleCreateModuleModal, + toggleCreateCycleModal, + toggleCreateViewModal, + toggleBulkDeleteIssueModal, + } = store.commandPalette; + + return { + d: { + title: "Create a new page", + description: "Create a new page in the current project", + action: () => toggleCreatePageModal({ isOpen: true }), + }, + m: { + title: "Create a new module", + description: "Create a new module in the current project", + action: () => toggleCreateModuleModal(true), + }, + q: { + title: "Create a new cycle", + description: "Create a new cycle in the current project", + action: () => toggleCreateCycleModal(true), + }, + v: { + title: "Create a new view", + description: "Create a new view in the current project", + action: () => toggleCreateViewModal(true), + }, + backspace: { + title: "Bulk delete issues", + description: "Bulk delete issues in the current project", + action: () => toggleBulkDeleteIssueModal(true), + }, + delete: { + title: "Bulk delete issues", + description: "Bulk delete issues in the current project", + action: () => toggleBulkDeleteIssueModal(true), + }, + }; +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const handleAdditionalKeyDownEvents = (e: KeyboardEvent) => null; + +export const getNavigationShortcutsList = (): TCommandPaletteShortcut[] => [ + { keys: "Ctrl,K", description: "Open command menu" }, +]; + +export const getCommonShortcutsList = (platform: string): TCommandPaletteShortcut[] => [ + { keys: "P", description: "Create project" }, + { keys: "C", description: "Create issue" }, + { keys: "Q", description: "Create cycle" }, + { keys: "M", description: "Create module" }, + { keys: "V", description: "Create view" }, + { keys: "D", description: "Create page" }, + { keys: "Delete", description: "Bulk delete issues" }, + { keys: "Shift,/", description: "Open shortcuts guide" }, + { + keys: platform === "MacOS" ? "Ctrl,control,C" : "Ctrl,Alt,C", + description: "Copy issue URL from the issue details page", + }, +]; + +export const getAdditionalShortcutsList = (): TCommandPaletteShortcutList[] => []; diff --git a/web/ce/store/command-palette.store.ts b/web/ce/store/command-palette.store.ts new file mode 100644 index 000000000..47c9280cd --- /dev/null +++ b/web/ce/store/command-palette.store.ts @@ -0,0 +1,12 @@ +import { makeObservable } from "mobx"; +// types / constants +import { BaseCommandPaletteStore, IBaseCommandPaletteStore } from "@/store/base-command-palette.store"; + +export type ICommandPaletteStore = IBaseCommandPaletteStore; + +export class CommandPaletteStore extends BaseCommandPaletteStore implements ICommandPaletteStore { + constructor() { + super(); + makeObservable(this, {}); + } +} diff --git a/web/core/components/command-palette/command-palette.tsx b/web/core/components/command-palette/command-palette.tsx index c38266c40..4e59234cd 100644 --- a/web/core/components/command-palette/command-palette.tsx +++ b/web/core/components/command-palette/command-palette.tsx @@ -2,87 +2,43 @@ import React, { useCallback, useEffect, FC, useMemo } from "react"; import { observer } from "mobx-react"; -import { useParams, usePathname } from "next/navigation"; -import useSWR from "swr"; +import { useParams } from "next/navigation"; // ui import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { CommandModal, ShortcutsModal } from "@/components/command-palette"; -import { BulkDeleteIssuesModal } from "@/components/core"; -import { CycleCreateUpdateModal } from "@/components/cycles"; -import { CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues"; -import { CreateUpdateModuleModal } from "@/components/modules"; -import { CreatePageModal } from "@/components/pages"; -import { CreateProjectModal } from "@/components/project"; -import { CreateUpdateProjectViewModal } from "@/components/views"; -// constants -import { ISSUE_DETAILS } from "@/constants/fetch-keys"; // helpers import { copyTextToClipboard } from "@/helpers/string.helper"; // hooks import { useEventTracker, useUser, useAppTheme, useCommandPalette, useUserPermissions } from "@/hooks/store"; -import { useAppRouter } from "@/hooks/use-app-router"; -import { useIssuesStore } from "@/hooks/use-issue-layout-store"; import { usePlatformOS } from "@/hooks/use-platform-os"; +// plane web components +import { + IssueLevelModals, + ProjectLevelModals, + WorkspaceLevelModals, +} from "@/plane-web/components/command-palette/modals"; +// plane web constants import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; -// services -import { IssueService } from "@/services/issue"; - -// services -const issueService = new IssueService(); +// plane web helpers +import { + getGlobalShortcutsList, + getProjectShortcutsList, + getWorkspaceShortcutsList, + handleAdditionalKeyDownEvents, +} from "@/plane-web/helpers/command-palette"; export const CommandPalette: FC = observer(() => { - // router - const router = useAppRouter(); // router params - const { workspaceSlug, projectId, issueId, cycleId, moduleId } = useParams(); - // pathname - const pathname = usePathname(); + const { workspaceSlug, projectId, issueId } = useParams(); // store hooks const { toggleSidebar } = useAppTheme(); const { setTrackElement } = useEventTracker(); const { platform } = usePlatformOS(); - const { - data: currentUser, - // canPerformProjectMemberActions, - // canPerformWorkspaceMemberActions, - canPerformAnyCreateAction, - // canPerformProjectAdminActions, - } = useUser(); - const { - issues: { removeIssue }, - } = useIssuesStore(); - const { - toggleCommandPaletteModal, - isCreateIssueModalOpen, - toggleCreateIssueModal, - isCreateCycleModalOpen, - toggleCreateCycleModal, - createPageModal, - toggleCreatePageModal, - isCreateProjectModalOpen, - toggleCreateProjectModal, - isCreateModuleModalOpen, - toggleCreateModuleModal, - isCreateViewModalOpen, - toggleCreateViewModal, - isShortcutModalOpen, - toggleShortcutModal, - isBulkDeleteIssueModalOpen, - toggleBulkDeleteIssueModal, - isDeleteIssueModalOpen, - toggleDeleteIssueModal, - isAnyModalOpen, - } = useCommandPalette(); + const { data: currentUser, canPerformAnyCreateAction } = useUser(); + const { toggleCommandPaletteModal, isShortcutModalOpen, toggleShortcutModal, isAnyModalOpen } = useCommandPalette(); const { allowPermissions } = useUserPermissions(); - const { data: issueDetails } = useSWR( - workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null, - workspaceSlug && projectId && issueId - ? () => issueService.retrieve(workspaceSlug as string, projectId as string, issueId as string) - : null - ); - // derived values const canPerformWorkspaceMemberActions = allowPermissions( [EUserPermissions.ADMIN, EUserPermissions.MEMBER], @@ -170,62 +126,11 @@ export const CommandPalette: FC = observer(() => { project: Record void }>; } = useMemo( () => ({ - global: { - c: { - title: "Create a new issue", - description: "Create a new issue in the current project", - action: () => toggleCreateIssueModal(true), - }, - }, - workspace: { - p: { - title: "Create a new project", - description: "Create a new project in the current workspace", - action: () => toggleCreateProjectModal(true), - }, - }, - project: { - d: { - title: "Create a new page", - description: "Create a new page in the current project", - action: () => toggleCreatePageModal({ isOpen: true }), - }, - m: { - title: "Create a new module", - description: "Create a new module in the current project", - action: () => toggleCreateModuleModal(true), - }, - q: { - title: "Create a new cycle", - description: "Create a new cycle in the current project", - action: () => toggleCreateCycleModal(true), - }, - v: { - title: "Create a new view", - description: "Create a new view in the current project", - action: () => toggleCreateViewModal(true), - }, - backspace: { - title: "Bulk delete issues", - description: "Bulk delete issues in the current project", - action: () => toggleBulkDeleteIssueModal(true), - }, - delete: { - title: "Bulk delete issues", - description: "Bulk delete issues in the current project", - action: () => toggleBulkDeleteIssueModal(true), - }, - }, + global: getGlobalShortcutsList(), + workspace: getWorkspaceShortcutsList(), + project: getProjectShortcutsList(), }), - [ - toggleBulkDeleteIssueModal, - toggleCreateCycleModal, - toggleCreateIssueModal, - toggleCreateModuleModal, - toggleCreatePageModal, - toggleCreateProjectModal, - toggleCreateViewModal, - ] + [] ); const handleKeyDown = useCallback( @@ -296,6 +201,8 @@ export const CommandPalette: FC = observer(() => { shortcutsList.project[keyPressed].action(); } } + // Additional keydown events + handleAdditionalKeyDownEvents(e); }, [ copyIssueUrlToClipboard, @@ -320,75 +227,16 @@ export const CommandPalette: FC = observer(() => { return () => document.removeEventListener("keydown", handleKeyDown); }, [handleKeyDown]); - const isDraftIssue = pathname?.includes("draft-issues") || false; - if (!currentUser) return null; return ( <> toggleShortcutModal(false)} /> - {workspaceSlug && ( - toggleCreateProjectModal(false)} - workspaceSlug={workspaceSlug.toString()} - /> - )} + {workspaceSlug && } {workspaceSlug && projectId && ( - <> - toggleCreateCycleModal(false)} - workspaceSlug={workspaceSlug.toString()} - projectId={projectId.toString()} - /> - toggleCreateModuleModal(false)} - workspaceSlug={workspaceSlug.toString()} - projectId={projectId.toString()} - /> - toggleCreateViewModal(false)} - workspaceSlug={workspaceSlug.toString()} - projectId={projectId.toString()} - /> - toggleCreatePageModal({ isOpen: false })} - redirectionEnabled - /> - + )} - - toggleCreateIssueModal(false)} - data={cycleId ? { cycle_id: cycleId.toString() } : moduleId ? { module_ids: [moduleId.toString()] } : undefined} - isDraft={isDraftIssue} - /> - - {workspaceSlug && projectId && issueId && issueDetails && ( - toggleDeleteIssueModal(false)} - isOpen={isDeleteIssueModalOpen} - data={issueDetails} - onSubmit={async () => { - await removeIssue(workspaceSlug.toString(), projectId.toString(), issueId.toString()); - router.push(`/${workspaceSlug}/projects/${projectId}/issues`); - }} - /> - )} - - toggleBulkDeleteIssueModal(false)} - user={currentUser} - /> + ); diff --git a/web/core/components/command-palette/shortcuts-modal/commands-list.tsx b/web/core/components/command-palette/shortcuts-modal/commands-list.tsx index e72e92e0b..570cb02fa 100644 --- a/web/core/components/command-palette/shortcuts-modal/commands-list.tsx +++ b/web/core/components/command-palette/shortcuts-modal/commands-list.tsx @@ -3,6 +3,12 @@ import { Command } from "lucide-react"; import { substringMatch } from "@/helpers/string.helper"; // hooks import { usePlatformOS } from "@/hooks/use-platform-os"; +// plane web helpers +import { + getAdditionalShortcutsList, + getCommonShortcutsList, + getNavigationShortcutsList, +} from "@/plane-web/helpers/command-palette"; type Props = { searchQuery: string; @@ -16,26 +22,14 @@ export const ShortcutCommandsList: React.FC = (props) => { { key: "navigation", title: "Navigation", - shortcuts: [{ keys: "Ctrl,K", description: "Open command menu" }], + shortcuts: getNavigationShortcutsList(), }, { key: "common", title: "Common", - shortcuts: [ - { keys: "P", description: "Create project" }, - { keys: "C", description: "Create issue" }, - { keys: "Q", description: "Create cycle" }, - { keys: "M", description: "Create module" }, - { keys: "V", description: "Create view" }, - { keys: "D", description: "Create page" }, - { keys: "Delete", description: "Bulk delete issues" }, - { keys: "Shift,/", description: "Open shortcuts guide" }, - { - keys: platform === "MacOS" ? "Ctrl,control,C" : "Ctrl,Alt,C", - description: "Copy issue URL from the issue details page", - }, - ], + shortcuts: getCommonShortcutsList(platform), }, + ...getAdditionalShortcutsList(), ]; const filteredShortcuts = KEYBOARD_SHORTCUTS.map((category) => { @@ -69,7 +63,11 @@ export const ShortcutCommandsList: React.FC = (props) => {
{key === "Ctrl" ? (
- { platform === "MacOS" ? : 'Ctrl'} + {platform === "MacOS" ? ( + + ) : ( + "Ctrl" + )}
) : ( diff --git a/web/core/hooks/store/use-command-palette.ts b/web/core/hooks/store/use-command-palette.ts index 7b399387b..cdf6a09ec 100644 --- a/web/core/hooks/store/use-command-palette.ts +++ b/web/core/hooks/store/use-command-palette.ts @@ -2,7 +2,7 @@ import { useContext } from "react"; // mobx store import { StoreContext } from "@/lib/store-context"; // types -import { ICommandPaletteStore } from "@/store/command-palette.store"; +import { ICommandPaletteStore } from "@/plane-web/store/command-palette.store"; export const useCommandPalette = (): ICommandPaletteStore => { const context = useContext(StoreContext); diff --git a/web/core/store/command-palette.store.ts b/web/core/store/base-command-palette.store.ts similarity index 92% rename from web/core/store/command-palette.store.ts rename to web/core/store/base-command-palette.store.ts index 4cede3434..9b0ff52b5 100644 --- a/web/core/store/command-palette.store.ts +++ b/web/core/store/base-command-palette.store.ts @@ -9,9 +9,8 @@ export interface ModalData { viewId: string; } -export interface ICommandPaletteStore { +export interface IBaseCommandPaletteStore { // observables - isCommandPaletteOpen: boolean; isShortcutModalOpen: boolean; isCreateProjectModalOpen: boolean; @@ -22,6 +21,7 @@ export interface ICommandPaletteStore { isCreateIssueModalOpen: boolean; isDeleteIssueModalOpen: boolean; isBulkDeleteIssueModalOpen: boolean; + createIssueStoreType: TCreateModalStoreTypes; // computed isAnyModalOpen: boolean; // toggle actions @@ -35,11 +35,9 @@ export interface ICommandPaletteStore { toggleCreateModuleModal: (value?: boolean) => void; toggleDeleteIssueModal: (value?: boolean) => void; toggleBulkDeleteIssueModal: (value?: boolean) => void; - - createIssueStoreType: TCreateModalStoreTypes; } -export class CommandPaletteStore implements ICommandPaletteStore { +export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStore { // observables isCommandPaletteOpen: boolean = false; isShortcutModalOpen: boolean = false; @@ -51,7 +49,6 @@ export class CommandPaletteStore implements ICommandPaletteStore { isDeleteIssueModalOpen: boolean = false; isBulkDeleteIssueModalOpen: boolean = false; createPageModal: TCreatePageModal = DEFAULT_CREATE_PAGE_MODAL_DATA; - createIssueStoreType: TCreateModalStoreTypes = EIssuesStoreType.PROJECT; constructor() { @@ -67,6 +64,7 @@ export class CommandPaletteStore implements ICommandPaletteStore { isDeleteIssueModalOpen: observable.ref, isBulkDeleteIssueModalOpen: observable.ref, createPageModal: observable, + createIssueStoreType: observable, // computed isAnyModalOpen: computed, // projectPages: computed, @@ -85,20 +83,20 @@ export class CommandPaletteStore implements ICommandPaletteStore { } /** - * Checks whether any modal is open or not. + * Checks whether any modal is open or not in the base command palette. * @returns boolean */ get isAnyModalOpen() { return Boolean( this.isCreateIssueModalOpen || - this.isCreateCycleModalOpen || - this.isCreateProjectModalOpen || - this.isCreateModuleModalOpen || - this.isCreateViewModalOpen || - this.isShortcutModalOpen || - this.isBulkDeleteIssueModalOpen || - this.isDeleteIssueModalOpen || - this.createPageModal.isOpen + this.isCreateCycleModalOpen || + this.isCreateProjectModalOpen || + this.isCreateModuleModalOpen || + this.isCreateViewModalOpen || + this.isShortcutModalOpen || + this.isBulkDeleteIssueModalOpen || + this.isDeleteIssueModalOpen || + this.createPageModal.isOpen ); } diff --git a/web/core/store/root.store.ts b/web/core/store/root.store.ts index 33e4e7e92..621f5f808 100644 --- a/web/core/store/root.store.ts +++ b/web/core/store/root.store.ts @@ -1,9 +1,9 @@ import { enableStaticRendering } from "mobx-react"; // plane web store +import { CommandPaletteStore, ICommandPaletteStore } from "@/plane-web/store/command-palette.store"; import { RootStore } from "@/plane-web/store/root.store"; import { IStateStore, StateStore } from "@/plane-web/store/state.store"; // stores -import { CommandPaletteStore, ICommandPaletteStore } from "./command-palette.store"; import { CycleStore, ICycleStore } from "./cycle.store"; import { CycleFilterStore, ICycleFilterStore } from "./cycle_filter.store"; import { DashboardStore, IDashboardStore } from "./dashboard.store"; diff --git a/web/ee/components/command-palette/modals/index.ts b/web/ee/components/command-palette/modals/index.ts new file mode 100644 index 000000000..fabf334b9 --- /dev/null +++ b/web/ee/components/command-palette/modals/index.ts @@ -0,0 +1 @@ +export * from "ce/components/command-palette/modals"; diff --git a/web/ee/store/command-palette.store.ts b/web/ee/store/command-palette.store.ts new file mode 100644 index 000000000..b191f9f2d --- /dev/null +++ b/web/ee/store/command-palette.store.ts @@ -0,0 +1 @@ +export * from "ce/store/command-palette.store"; diff --git a/web/helpers/command-palette.ts b/web/helpers/command-palette.ts new file mode 100644 index 000000000..7a4518321 --- /dev/null +++ b/web/helpers/command-palette.ts @@ -0,0 +1 @@ +export * from "ce/helpers/command-palette"; From 38e8a5c807bfd87f4d366f170427c077d7c980f0 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Wed, 11 Dec 2024 18:19:09 +0530 Subject: [PATCH 007/355] fix: command palette build (#6186) --- web/ee/helpers/command-palette.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 web/ee/helpers/command-palette.ts diff --git a/web/ee/helpers/command-palette.ts b/web/ee/helpers/command-palette.ts new file mode 100644 index 000000000..7a4518321 --- /dev/null +++ b/web/ee/helpers/command-palette.ts @@ -0,0 +1 @@ +export * from "ce/helpers/command-palette"; From 9ad8b43408986523af0a73462561f2d4ad1936d4 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Thu, 12 Dec 2024 14:11:12 +0530 Subject: [PATCH 008/355] chore: handled the cycle date time using project timezone (#6187) * chore: handled the cycle date time using project timezone * chore: reverted the frontend commit --- apiserver/plane/app/serializers/cycle.py | 12 +++ apiserver/plane/app/views/cycle/base.py | 55 +++++++++- apiserver/plane/app/views/issue/base.py | 2 +- apiserver/plane/app/views/issue/sub_issue.py | 2 +- apiserver/plane/app/views/module/archive.py | 2 +- apiserver/plane/app/views/module/base.py | 2 +- apiserver/plane/utils/timezone_converter.py | 100 ++++++++++++++++++ .../plane/utils/user_timezone_converter.py | 26 ----- web/core/components/project/form.tsx | 38 +++---- 9 files changed, 185 insertions(+), 54 deletions(-) create mode 100644 apiserver/plane/utils/timezone_converter.py delete mode 100644 apiserver/plane/utils/user_timezone_converter.py diff --git a/apiserver/plane/app/serializers/cycle.py b/apiserver/plane/app/serializers/cycle.py index bf08de4fe..171494f03 100644 --- a/apiserver/plane/app/serializers/cycle.py +++ b/apiserver/plane/app/serializers/cycle.py @@ -5,6 +5,7 @@ from rest_framework import serializers from .base import BaseSerializer from .issue import IssueStateSerializer from plane.db.models import Cycle, CycleIssue, CycleUserProperties +from plane.utils.timezone_converter import convert_to_utc class CycleWriteSerializer(BaseSerializer): @@ -15,6 +16,17 @@ class CycleWriteSerializer(BaseSerializer): and data.get("start_date", None) > data.get("end_date", None) ): raise serializers.ValidationError("Start date cannot exceed end date") + if ( + data.get("start_date", None) is not None + and data.get("end_date", None) is not None + ): + project_id = self.initial_data.get("project_id") or self.instance.project_id + data["start_date"] = convert_to_utc( + str(data.get("start_date").date()), project_id + ) + data["end_date"] = convert_to_utc( + str(data.get("end_date", None).date()), project_id, is_end_date=True + ) return data class Meta: diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index 61ea9eed4..1addc5bec 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -1,5 +1,7 @@ # Python imports import json +import pytz + # Django imports from django.contrib.postgres.aggregates import ArrayAgg @@ -52,6 +54,11 @@ from plane.bgtasks.recent_visited_task import recent_visited_task # Module imports from .. import BaseAPIView, BaseViewSet from plane.bgtasks.webhook_task import model_activity +from plane.utils.timezone_converter import ( + convert_utc_to_project_timezone, + convert_to_utc, + user_timezone_converter, +) class CycleViewSet(BaseViewSet): @@ -67,6 +74,19 @@ class CycleViewSet(BaseViewSet): project_id=self.kwargs.get("project_id"), workspace__slug=self.kwargs.get("slug"), ) + + project = Project.objects.get(id=self.kwargs.get("project_id")) + + # Fetch project for the specific record or pass project_id dynamically + project_timezone = project.timezone + + # Convert the current time (timezone.now()) to the project's timezone + local_tz = pytz.timezone(project_timezone) + current_time_in_project_tz = timezone.now().astimezone(local_tz) + + # Convert project local time back to UTC for comparison (start_date is stored in UTC) + current_time_in_utc = current_time_in_project_tz.astimezone(pytz.utc) + return self.filter_queryset( super() .get_queryset() @@ -119,12 +139,15 @@ class CycleViewSet(BaseViewSet): .annotate( status=Case( When( - Q(start_date__lte=timezone.now()) - & Q(end_date__gte=timezone.now()), + Q(start_date__lte=current_time_in_utc) + & Q(end_date__gte=current_time_in_utc), then=Value("CURRENT"), ), - When(start_date__gt=timezone.now(), then=Value("UPCOMING")), - When(end_date__lt=timezone.now(), then=Value("COMPLETED")), + When( + start_date__gt=current_time_in_utc, + then=Value("UPCOMING"), + ), + When(end_date__lt=current_time_in_utc, then=Value("COMPLETED")), When( Q(start_date__isnull=True) & Q(end_date__isnull=True), then=Value("DRAFT"), @@ -160,10 +183,22 @@ class CycleViewSet(BaseViewSet): # Update the order by queryset = queryset.order_by("-is_favorite", "-created_at") + project = Project.objects.get(id=self.kwargs.get("project_id")) + + # Fetch project for the specific record or pass project_id dynamically + project_timezone = project.timezone + + # Convert the current time (timezone.now()) to the project's timezone + local_tz = pytz.timezone(project_timezone) + current_time_in_project_tz = timezone.now().astimezone(local_tz) + + # Convert project local time back to UTC for comparison (start_date is stored in UTC) + current_time_in_utc = current_time_in_project_tz.astimezone(pytz.utc) + # Current Cycle if cycle_view == "current": queryset = queryset.filter( - start_date__lte=timezone.now(), end_date__gte=timezone.now() + start_date__lte=current_time_in_utc, end_date__gte=current_time_in_utc ) data = queryset.values( @@ -191,6 +226,8 @@ class CycleViewSet(BaseViewSet): "version", "created_by", ) + datetime_fields = ["start_date", "end_date"] + data = user_timezone_converter(data, datetime_fields, project_timezone) if data: return Response(data, status=status.HTTP_200_OK) @@ -221,6 +258,8 @@ class CycleViewSet(BaseViewSet): "version", "created_by", ) + datetime_fields = ["start_date", "end_date"] + data = user_timezone_converter(data, datetime_fields, project_timezone) return Response(data, status=status.HTTP_200_OK) @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) @@ -365,6 +404,7 @@ class CycleViewSet(BaseViewSet): @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def retrieve(self, request, slug, project_id, pk): + project = Project.objects.get(id=project_id) queryset = self.get_queryset().filter(archived_at__isnull=True).filter(pk=pk) data = ( self.get_queryset() @@ -417,6 +457,8 @@ class CycleViewSet(BaseViewSet): ) queryset = queryset.first() + datetime_fields = ["start_date", "end_date"] + data = user_timezone_converter(data, datetime_fields, project.timezone) recent_visited_task.delay( slug=slug, @@ -492,6 +534,9 @@ class CycleDateCheckEndpoint(BaseAPIView): status=status.HTTP_400_BAD_REQUEST, ) + start_date = convert_to_utc(str(start_date), project_id) + end_date = convert_to_utc(str(end_date), project_id, is_end_date=True) + # Check if any cycle intersects in the given interval cycles = Cycle.objects.filter( Q(workspace__slug=slug) diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py index d0c614368..e087a3659 100644 --- a/apiserver/plane/app/views/issue/base.py +++ b/apiserver/plane/app/views/issue/base.py @@ -54,7 +54,7 @@ from plane.utils.issue_filters import issue_filters from plane.utils.order_queryset import order_issue_queryset from plane.utils.paginator import GroupedOffsetPaginator, SubGroupedOffsetPaginator from .. import BaseAPIView, BaseViewSet -from plane.utils.user_timezone_converter import user_timezone_converter +from plane.utils.timezone_converter import user_timezone_converter from plane.bgtasks.recent_visited_task import recent_visited_task from plane.utils.global_paginator import paginate from plane.bgtasks.webhook_task import model_activity diff --git a/apiserver/plane/app/views/issue/sub_issue.py b/apiserver/plane/app/views/issue/sub_issue.py index e461917fb..19e2522d2 100644 --- a/apiserver/plane/app/views/issue/sub_issue.py +++ b/apiserver/plane/app/views/issue/sub_issue.py @@ -20,7 +20,7 @@ from plane.app.serializers import IssueSerializer from plane.app.permissions import ProjectEntityPermission from plane.db.models import Issue, IssueLink, FileAsset, CycleIssue from plane.bgtasks.issue_activities_task import issue_activity -from plane.utils.user_timezone_converter import user_timezone_converter +from plane.utils.timezone_converter import user_timezone_converter from collections import defaultdict diff --git a/apiserver/plane/app/views/module/archive.py b/apiserver/plane/app/views/module/archive.py index 82c1d47eb..d5c632f96 100644 --- a/apiserver/plane/app/views/module/archive.py +++ b/apiserver/plane/app/views/module/archive.py @@ -28,7 +28,7 @@ from plane.app.permissions import ProjectEntityPermission from plane.app.serializers import ModuleDetailSerializer from plane.db.models import Issue, Module, ModuleLink, UserFavorite, Project from plane.utils.analytics_plot import burndown_plot -from plane.utils.user_timezone_converter import user_timezone_converter +from plane.utils.timezone_converter import user_timezone_converter # Module imports diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py index 8f9839b71..3e3a4c2db 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -56,7 +56,7 @@ from plane.db.models import ( Project, ) from plane.utils.analytics_plot import burndown_plot -from plane.utils.user_timezone_converter import user_timezone_converter +from plane.utils.timezone_converter import user_timezone_converter from plane.bgtasks.webhook_task import model_activity from .. import BaseAPIView, BaseViewSet from plane.bgtasks.recent_visited_task import recent_visited_task diff --git a/apiserver/plane/utils/timezone_converter.py b/apiserver/plane/utils/timezone_converter.py new file mode 100644 index 000000000..dc8e20b8c --- /dev/null +++ b/apiserver/plane/utils/timezone_converter.py @@ -0,0 +1,100 @@ +import pytz +from plane.db.models import Project +from datetime import datetime, time +from datetime import timedelta + +def user_timezone_converter(queryset, datetime_fields, user_timezone): + # Create a timezone object for the user's timezone + user_tz = pytz.timezone(user_timezone) + + # Check if queryset is a dictionary (single item) or a list of dictionaries + if isinstance(queryset, dict): + queryset_values = [queryset] + else: + queryset_values = list(queryset) + + # Iterate over the dictionaries in the list + for item in queryset_values: + # Iterate over the datetime fields + for field in datetime_fields: + # Convert the datetime field to the user's timezone + if field in item and item[field]: + item[field] = item[field].astimezone(user_tz) + + # If queryset was a single item, return a single item + if isinstance(queryset, dict): + return queryset_values[0] + else: + return queryset_values + + +def convert_to_utc(date, project_id, is_end_date=False): + """ + Converts a start date string to the project's local timezone at 12:00 AM + and then converts it to UTC for storage. + + Args: + date (str): The date string in "YYYY-MM-DD" format. + project_id (int): The project's ID to fetch the associated timezone. + + Returns: + datetime: The UTC datetime. + """ + # Retrieve the project's timezone using the project ID + project = Project.objects.get(id=project_id) + project_timezone = project.timezone + if not date or not project_timezone: + raise ValueError("Both date and timezone must be provided.") + + # Parse the string into a date object + start_date = datetime.strptime(date, "%Y-%m-%d").date() + + # Get the project's timezone + local_tz = pytz.timezone(project_timezone) + + # Combine the date with 12:00 AM time + local_datetime = datetime.combine(start_date, time.min) + + # Localize the datetime to the project's timezone + localized_datetime = local_tz.localize(local_datetime) + + # If it's an end date, subtract one minute + if is_end_date: + localized_datetime -= timedelta(minutes=1) + + # Convert the localized datetime to UTC + utc_datetime = localized_datetime.astimezone(pytz.utc) + + # Return the UTC datetime for storage + return utc_datetime + + +def convert_utc_to_project_timezone(utc_datetime, project_id): + """ + Converts a UTC datetime (stored in the database) to the project's local timezone. + + Args: + utc_datetime (datetime): The UTC datetime to be converted. + project_id (int): The project's ID to fetch the associated timezone. + + Returns: + datetime: The datetime in the project's local timezone. + """ + # Retrieve the project's timezone using the project ID + project = Project.objects.get(id=project_id) + project_timezone = project.timezone + if not project_timezone: + raise ValueError("Project timezone must be provided.") + + # Get the timezone object for the project's timezone + local_tz = pytz.timezone(project_timezone) + + # Convert the UTC datetime to the project's local timezone + if utc_datetime.tzinfo is None: + # Localize UTC datetime if it's naive (i.e., without timezone info) + utc_datetime = pytz.utc.localize(utc_datetime) + + # Convert to the project's local timezone + local_datetime = utc_datetime.astimezone(local_tz) + + return local_datetime diff --git a/apiserver/plane/utils/user_timezone_converter.py b/apiserver/plane/utils/user_timezone_converter.py deleted file mode 100644 index 550abfe99..000000000 --- a/apiserver/plane/utils/user_timezone_converter.py +++ /dev/null @@ -1,26 +0,0 @@ -import pytz - - -def user_timezone_converter(queryset, datetime_fields, user_timezone): - # Create a timezone object for the user's timezone - user_tz = pytz.timezone(user_timezone) - - # Check if queryset is a dictionary (single item) or a list of dictionaries - if isinstance(queryset, dict): - queryset_values = [queryset] - else: - queryset_values = list(queryset) - - # Iterate over the dictionaries in the list - for item in queryset_values: - # Iterate over the datetime fields - for field in datetime_fields: - # Convert the datetime field to the user's timezone - if field in item and item[field]: - item[field] = item[field].astimezone(user_tz) - - # If queryset was a single item, return a single item - if isinstance(queryset, dict): - return queryset_values[0] - else: - return queryset_values diff --git a/web/core/components/project/form.tsx b/web/core/components/project/form.tsx index 86da4a2f7..855c52aaf 100644 --- a/web/core/components/project/form.tsx +++ b/web/core/components/project/form.tsx @@ -16,7 +16,7 @@ import { CustomEmojiIconPicker, EmojiIconPickerTypes, Tooltip, - // CustomSearchSelect, + CustomSearchSelect, } from "@plane/ui"; // components import { Logo } from "@/components/common"; @@ -25,7 +25,7 @@ import { ImagePickerPopover } from "@/components/core"; import { PROJECT_UPDATED } from "@/constants/event-tracker"; import { NETWORK_CHOICES } from "@/constants/project"; // helpers -// import { TTimezone, TIME_ZONES } from "@/constants/timezones"; +import { TTimezone, TIME_ZONES } from "@/constants/timezones"; import { renderFormattedDate } from "@/helpers/date-time.helper"; import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper"; import { getFileURL } from "@/helpers/file.helper"; @@ -68,20 +68,20 @@ export const ProjectDetailsForm: FC = (props) => { }); // derived values const currentNetwork = NETWORK_CHOICES.find((n) => n.key === project?.network); - // const getTimeZoneLabel = (timezone: TTimezone | undefined) => { - // if (!timezone) return undefined; - // return ( - //
- // {timezone.gmtOffset} - // {timezone.name} - //
- // ); - // }; - // const timeZoneOptions = TIME_ZONES.map((timeZone) => ({ - // value: timeZone.value, - // query: timeZone.name + " " + timeZone.gmtOffset + " " + timeZone.value, - // content: getTimeZoneLabel(timeZone), - // })); + const getTimeZoneLabel = (timezone: TTimezone | undefined) => { + if (!timezone) return undefined; + return ( +
+ {timezone.gmtOffset} + {timezone.name} +
+ ); + }; + const timeZoneOptions = TIME_ZONES.map((timeZone) => ({ + value: timeZone.value, + query: timeZone.name + " " + timeZone.gmtOffset + " " + timeZone.value, + content: getTimeZoneLabel(timeZone), + })); const coverImage = watch("cover_image_url"); useEffect(() => { @@ -146,7 +146,7 @@ export const ProjectDetailsForm: FC = (props) => { description: formData.description, logo_props: formData.logo_props, - // timezone: formData.timezone, + timezone: formData.timezone, }; // if unsplash or a pre-defined image is uploaded, delete the old uploaded asset if (formData.cover_image_url?.startsWith("http")) { @@ -386,7 +386,7 @@ export const ProjectDetailsForm: FC = (props) => { }} />
- {/*
+

Project Timezone

= (props) => { )} /> {errors.timezone && {errors.timezone.message}} -
*/} +
<> From 54f828cbfaf4175313edce13b28233ad2e209448 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Thu, 12 Dec 2024 21:40:57 +0530 Subject: [PATCH 009/355] 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 --- packages/constants/src/issue.ts | 1 + packages/types/src/common.d.ts | 2 + packages/types/src/enums.ts | 1 + packages/types/src/issues.d.ts | 3 +- packages/types/src/view-props.d.ts | 3 + packages/ui/src/breadcrumbs/index.ts | 1 + .../src/breadcrumbs/navigation-dropdown.tsx | 96 ++++++++++ packages/ui/src/icons/index.ts | 3 + packages/ui/src/icons/lead-icon.tsx | 26 +++ packages/ui/src/icons/teams.tsx | 19 ++ packages/ui/src/tabs/tabs.tsx | 32 +++- space/core/store/helpers/base-issues.store.ts | 2 +- .../cycles/(detail)/[cycleId]/page.tsx | 7 +- .../[workspaceSlug]/(projects)/sidebar.tsx | 17 +- .../components/cycles/active-cycle/root.tsx | 117 ++++++------ web/ce/components/issues/filters/index.ts | 1 + .../issues/filters/team-project.tsx | 12 ++ .../components/issues/issue-layouts/utils.tsx | 4 + .../workspace/sidebar/teams-sidebar-list.tsx | 1 + web/ce/constants/dashboard.ts | 58 +++++- web/ce/constants/issues.ts | 8 +- web/ce/helpers/dashboard.helper.ts | 5 +- web/ce/helpers/issue-action-helper.ts | 15 ++ web/ce/store/command-palette.store.ts | 20 +- 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/ce/types/dashboard.ts | 2 + .../cycles/analytics-sidebar/root.tsx | 27 ++- .../components/cycles/cycle-peek-overview.tsx | 24 ++- web/core/components/cycles/form.tsx | 1 + .../cycles/list/cycle-list-item-action.tsx | 13 +- .../list/cycle-list-project-group-header.tsx | 44 +++++ .../cycles/list/cycles-list-item.tsx | 6 +- web/core/components/cycles/list/index.ts | 1 + web/core/components/dropdowns/layout.tsx | 12 +- web/core/components/dropdowns/project.tsx | 66 +++++-- .../rich-text-editor/rich-text-editor.tsx | 15 +- .../inbox/content/inbox-issue-header.tsx | 7 +- .../content/inbox-issue-mobile-header.tsx | 7 +- .../components/inbox/content/issue-root.tsx | 6 +- web/core/components/inbox/content/root.tsx | 3 +- .../modals/create-modal/issue-description.tsx | 8 +- .../components/issues/description-input.tsx | 13 +- .../issues/issue-detail/main-content.tsx | 7 +- .../components/issues/issue-layouts/utils.tsx | 6 +- .../components/description-editor.tsx | 8 +- .../issue-modal/components/project-select.tsx | 1 + .../components/issues/issue-update-status.tsx | 8 +- .../issues/peek-overview/header.tsx | 8 +- web/core/components/modules/form.tsx | 1 + web/core/components/project/card-list.tsx | 18 +- web/core/components/project/header.tsx | 80 +------- web/core/components/project/index.ts | 2 + .../components/project/multi-select-modal.tsx | 175 ++++++++++++++++++ .../components/project/search-projects.tsx | 78 ++++++++ .../workspace/sidebar/workspace-menu.tsx | 21 ++- web/core/constants/dashboard.ts | 48 ----- 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/base-command-palette.store.ts | 12 +- .../store/issue/helpers/base-issues.store.ts | 2 + .../helpers/issue-filter-helper.store.ts | 2 + web/core/store/issue/root.store.ts | 38 +++- web/core/store/pages/page.ts | 12 +- web/core/store/router.store.ts | 10 + web/ee/components/issues/filters/index.ts | 1 + .../issues/filters/team-project.tsx | 1 + .../components/issues/issue-layouts/utils.tsx | 1 + .../workspace/sidebar/teams-sidebar-list.tsx | 1 + web/ee/constants/dashboard.ts | 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 + web/helpers/issue.helper.ts | 2 +- 82 files changed, 1044 insertions(+), 320 deletions(-) create mode 100644 packages/ui/src/breadcrumbs/navigation-dropdown.tsx create mode 100644 packages/ui/src/icons/lead-icon.tsx create mode 100644 packages/ui/src/icons/teams.tsx 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/ce/components/workspace/sidebar/teams-sidebar-list.tsx 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/core/components/cycles/list/cycle-list-project-group-header.tsx create mode 100644 web/core/components/project/multi-select-modal.tsx create mode 100644 web/core/components/project/search-projects.tsx create mode 100644 web/ee/components/issues/filters/team-project.tsx create mode 100644 web/ee/components/issues/issue-layouts/utils.tsx create mode 100644 web/ee/components/workspace/sidebar/teams-sidebar-list.tsx create mode 100644 web/ee/constants/dashboard.ts 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/packages/constants/src/issue.ts b/packages/constants/src/issue.ts index 5db398c76..9f6a1a2e2 100644 --- a/packages/constants/src/issue.ts +++ b/packages/constants/src/issue.ts @@ -11,6 +11,7 @@ export enum EIssueGroupByToServerOptions { "target_date" = "target_date", "project" = "project_id", "created_by" = "created_by", + "team_project" = "project_id", } export enum EIssueGroupBYServerToProperty { diff --git a/packages/types/src/common.d.ts b/packages/types/src/common.d.ts index 5fe31ad00..7e755fcc2 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 df6a462b0..e37e2f4a5 100644 --- a/packages/types/src/enums.ts +++ b/packages/types/src/enums.ts @@ -59,4 +59,5 @@ export enum EFileAssetType { USER_AVATAR = "USER_AVATAR", USER_COVER = "USER_COVER", WORKSPACE_LOGO = "WORKSPACE_LOGO", + TEAM_SPACE_DESCRIPTION = "TEAM_SPACE_DESCRIPTION", } diff --git a/packages/types/src/issues.d.ts b/packages/types/src/issues.d.ts index 9bbfa36b1..b6d32bdf8 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 57baa4cfd..aa1c75cdb 100644 --- a/packages/types/src/view-props.d.ts +++ b/packages/types/src/view-props.d.ts @@ -18,6 +18,7 @@ export type TIssueGroupByOptions = | "cycle" | "module" | "target_date" + | "team_project" | null; export type TIssueOrderByOptions = @@ -69,6 +70,7 @@ export type TIssueParams = | "start_date" | "target_date" | "project" + | "team_project" | "group_by" | "sub_group_by" | "order_by" @@ -92,6 +94,7 @@ export interface IIssueFilterOptions { cycle?: string[] | null; module?: string[] | null; project?: string[] | null; + team_project?: string[] | null; start_date?: string[] | null; state?: string[] | null; state_group?: string[] | null; diff --git a/packages/ui/src/breadcrumbs/index.ts b/packages/ui/src/breadcrumbs/index.ts index 669f55757..05a8bdbf1 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 000000000..a716ca65e --- /dev/null +++ b/packages/ui/src/breadcrumbs/navigation-dropdown.tsx @@ -0,0 +1,96 @@ +"use client"; + +import * as React from "react"; +import { CheckIcon, ChevronDownIcon } from "lucide-react"; +// ui +import { CustomMenu, TContextMenuItem } from "../dropdowns"; +// helpers +import { cn } from "../../helpers"; + +type TBreadcrumbNavigationDropdownProps = { + selectedItemKey: string; + navigationItems: TContextMenuItem[]; + navigationDisabled?: boolean; +}; + +export const BreadcrumbNavigationDropdown = (props: TBreadcrumbNavigationDropdownProps) => { + const { selectedItemKey, navigationItems, navigationDisabled = false } = props; + // derived values + const selectedItem = navigationItems.find((item) => item.key === selectedItemKey); + const selectedItemIcon = selectedItem?.icon ? ( + + ) : undefined; + + // if no selected item, return null + if (!selectedItem) return null; + + const NavigationButton = ({ className }: { className?: string }) => ( +
  • + {selectedItemIcon && ( +
    {selectedItemIcon}
    + )} +
    {selectedItem.title}
    +
  • + ); + + if (navigationDisabled) { + return ; + } + + return ( + + + +
    + } + placement="bottom-start" + closeOnSelect + > + {navigationItems.map((item) => { + if (item.shouldRender === false) return null; + return ( + { + e.preventDefault(); + e.stopPropagation(); + if (item.key === selectedItemKey) return; + item.action(); + }} + className={cn( + "flex items-center gap-2", + { + "text-custom-text-400": item.disabled, + }, + item.className + )} + disabled={item.disabled} + > + {item.icon && } +
    +
    {item.title}
    + {item.description && ( +

    + {item.description} +

    + )} +
    + {item.key === selectedItemKey && } +
    + ); + })} + + ); +}; diff --git a/packages/ui/src/icons/index.ts b/packages/ui/src/icons/index.ts index f8a2b1c84..573efd99f 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 000000000..75575d35e --- /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 000000000..b73055598 --- /dev/null +++ b/packages/ui/src/icons/teams.tsx @@ -0,0 +1,19 @@ +import * as React from "react"; + +import { ISvgIcons } from "./type"; + +export const TeamsIcon: React.FC = ({ className = "text-current", ...rest }) => ( + + + + +); diff --git a/packages/ui/src/tabs/tabs.tsx b/packages/ui/src/tabs/tabs.tsx index a323d9721..92bc3ad72 100644 --- a/packages/ui/src/tabs/tabs.tsx +++ b/packages/ui/src/tabs/tabs.tsx @@ -1,4 +1,4 @@ -import React, { FC, Fragment } from "react"; +import React, { FC, Fragment, useEffect, useState } from "react"; import { Tab } from "@headlessui/react"; import { LucideProps } from "lucide-react"; // helpers @@ -11,11 +11,12 @@ type TabItem = { label?: React.ReactNode; content: React.ReactNode; disabled?: boolean; + onClick?: () => void; }; type TTabsProps = { tabs: TabItem[]; - storageKey: string; + storageKey?: string; actions?: React.ReactNode; defaultTab?: string; containerClassName?: string; @@ -23,6 +24,8 @@ type TTabsProps = { tabListClassName?: string; tabClassName?: string; tabPanelClassName?: string; + size?: "sm" | "md" | "lg"; + storeInLocalStorage?: boolean; }; export const Tabs: FC = (props: TTabsProps) => { @@ -36,15 +39,28 @@ export const Tabs: FC = (props: TTabsProps) => { tabListClassName = "", tabClassName = "", tabPanelClassName = "", + size = "md", + storeInLocalStorage = true, } = props; // local storage - const { storedValue, setValue } = useLocalStorage(`tab-${storageKey}`, defaultTab); + const { storedValue, setValue } = useLocalStorage( + storeInLocalStorage && storageKey ? `tab-${storageKey}` : `tab-${tabs[0]?.key}`, + defaultTab + ); + // state + const [selectedTab, setSelectedTab] = useState(storedValue ?? defaultTab); + + useEffect(() => { + if (storeInLocalStorage) { + setValue(selectedTab); + } + }, [selectedTab, setValue, storeInLocalStorage, storageKey]); const currentTabIndex = (tabKey: string): number => tabs.findIndex((tab) => tab.key === tabKey); return (
    - +
    = (props: TTabsProps) => { : tab.disabled ? "text-custom-text-400 cursor-not-allowed" : "text-custom-text-400 hover:text-custom-text-300 hover:bg-custom-background-80/60", + { + "text-xs": size === "sm", + "text-sm": size === "md", + "text-base": size === "lg", + }, tabClassName ) } key={tab.key} onClick={() => { - if (!tab.disabled) setValue(tab.key); + if (!tab.disabled) setSelectedTab(tab.key); + tab.onClick?.(); }} disabled={tab.disabled} > diff --git a/space/core/store/helpers/base-issues.store.ts b/space/core/store/helpers/base-issues.store.ts index 004aa06c6..7abfa324a 100644 --- a/space/core/store/helpers/base-issues.store.ts +++ b/space/core/store/helpers/base-issues.store.ts @@ -26,7 +26,7 @@ import { CoreRootStore } from "../root.store"; // constants // helpers -export type TIssueDisplayFilterOptions = Exclude | "target_date"; +export type TIssueDisplayFilterOptions = Exclude | "target_date"; export enum EIssueGroupedAction { ADD = "ADD", diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx index a1f7071a4..7a29f0553 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx @@ -77,7 +77,12 @@ const CycleDetailPage = observer(() => { "0px 1px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 4px 0px rgba(16, 24, 40, 0.06), 0px 1px 8px -1px rgba(16, 24, 40, 0.06)", }} > - +
    )}
    diff --git a/web/app/[workspaceSlug]/(projects)/sidebar.tsx b/web/app/[workspaceSlug]/(projects)/sidebar.tsx index ac55fdec8..11d23e35f 100644 --- a/web/app/[workspaceSlug]/(projects)/sidebar.tsx +++ b/web/app/[workspaceSlug]/(projects)/sidebar.tsx @@ -21,6 +21,7 @@ import { useFavorite } from "@/hooks/store/use-favorite"; import useSize from "@/hooks/use-window-size"; // plane web components import { SidebarAppSwitcher } from "@/plane-web/components/sidebar"; +import { SidebarTeamsList } from "@/plane-web/components/workspace/sidebar/teams-sidebar-list"; import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; export const AppSidebar: FC = observer(() => { @@ -47,7 +48,7 @@ export const AppSidebar: FC = observer(() => { }); useEffect(() => { - if (windowSize[0] < 768) !sidebarCollapsed && toggleSidebar(); + if (windowSize[0] < 768 && !sidebarCollapsed) toggleSidebar(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [windowSize]); @@ -73,9 +74,12 @@ export const AppSidebar: FC = observer(() => { "px-4": !sidebarCollapsed, })} > + {/* Workspace switcher and settings */}
    - + {/* App switcher */} + {canPerformWorkspaceMemberActions && } + {/* Quick actions */}

    { "vertical-scrollbar px-4": !sidebarCollapsed, })} > + {/* User Menu */} - + {/* Workspace Menu */}
    + {/* Favorites Menu */} {canPerformWorkspaceMemberActions && !isFavoriteEmpty && } - + {/* Teams List */} + + {/* Projects List */}
    + {/* Help Section */} diff --git a/web/ce/components/cycles/active-cycle/root.tsx b/web/ce/components/cycles/active-cycle/root.tsx index a173cfda0..5ebddc63f 100644 --- a/web/ce/components/cycles/active-cycle/root.tsx +++ b/web/ce/components/cycles/active-cycle/root.tsx @@ -1,5 +1,6 @@ "use client"; +import { useMemo } from "react"; import { observer } from "mobx-react"; import { Disclosure } from "@headlessui/react"; // ui @@ -22,68 +23,80 @@ import { ActiveCycleIssueDetails } from "@/store/issue/cycle"; interface IActiveCycleDetails { workspaceSlug: string; projectId: string; + cycleId?: string; + showHeader?: boolean; } export const ActiveCycleRoot: React.FC = observer((props) => { - const { workspaceSlug, projectId } = props; - const { currentProjectActiveCycle, currentProjectActiveCycleId } = useCycle(); + const { workspaceSlug, projectId, cycleId: propsCycleId, showHeader = true } = props; + const { currentProjectActiveCycleId } = useCycle(); + // derived values + const cycleId = propsCycleId ?? currentProjectActiveCycleId; + // fetch cycle details const { handleFiltersUpdate, cycle: activeCycle, cycleIssueDetails, - } = useCyclesDetails({ workspaceSlug, projectId, cycleId: currentProjectActiveCycleId }); + } = useCyclesDetails({ workspaceSlug, projectId, cycleId }); + + const ActiveCyclesComponent = useMemo( + () => ( + <> + {!cycleId || !activeCycle ? ( + + ) : ( +
    + {cycleId && ( + + )} + +
    + + + +
    +
    +
    + )} + + ), + [cycleId, activeCycle, workspaceSlug, projectId, handleFiltersUpdate, cycleIssueDetails] + ); return ( <> - - {({ open }) => ( - <> - - - - - {!currentProjectActiveCycle ? ( - - ) : ( -
    - {currentProjectActiveCycleId && ( - - )} - -
    - - - -
    -
    -
    - )} -
    - - )} -
    + {showHeader ? ( + + {({ open }) => ( + <> + + + + {ActiveCyclesComponent} + + )} + + ) : ( + <>{ActiveCyclesComponent} + )} ); }); diff --git a/web/ce/components/issues/filters/index.ts b/web/ce/components/issues/filters/index.ts index 2cd80e3a7..f0f36b6c9 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 000000000..4f4787fef --- /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 000000000..48dca43bd --- /dev/null +++ b/web/ce/components/issues/issue-layouts/utils.tsx @@ -0,0 +1,4 @@ +// types +import { IGroupByColumn } from "@plane/types"; + +export const getTeamProjectColumns = (): IGroupByColumn[] | undefined => undefined; diff --git a/web/ce/components/workspace/sidebar/teams-sidebar-list.tsx b/web/ce/components/workspace/sidebar/teams-sidebar-list.tsx new file mode 100644 index 000000000..92cbdfc5f --- /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 8872982fc..0df2719a7 100644 --- a/web/ce/constants/dashboard.ts +++ b/web/ce/constants/dashboard.ts @@ -1,17 +1,19 @@ "use client"; // icons -import { Home, Inbox, PenSquare } from "lucide-react"; +import { Briefcase, Home, Inbox, Layers, PenSquare, BarChart2 } from "lucide-react"; // ui -import { UserActivityIcon } from "@plane/ui"; +import { UserActivityIcon, ContrastIcon } from "@plane/ui"; import { Props } from "@/components/icons/types"; +// constants import { TLinkOptions } from "@/constants/dashboard"; +// plane web constants import { EUserPermissions } from "@/plane-web/constants/user-permissions"; // plane web types -import { TSidebarUserMenuItemKeys } from "@/plane-web/types/dashboard"; +import { TSidebarUserMenuItemKeys, TSidebarWorkspaceMenuItemKeys } from "@/plane-web/types/dashboard"; -export type TSidebarUserMenuItems = { - key: TSidebarUserMenuItemKeys; +export type TSidebarMenuItems = { + key: T; label: string; href: string; access: EUserPermissions[]; @@ -19,6 +21,8 @@ export type TSidebarUserMenuItems = { Icon: React.FC; }; +export type TSidebarUserMenuItems = TSidebarMenuItems; + export const SIDEBAR_USER_MENU_ITEMS: TSidebarUserMenuItems[] = [ { key: "home", @@ -54,3 +58,47 @@ export const SIDEBAR_USER_MENU_ITEMS: TSidebarUserMenuItems[] = [ Icon: PenSquare, }, ]; + +export type TSidebarWorkspaceMenuItems = TSidebarMenuItems; + +export const SIDEBAR_WORKSPACE_MENU: Partial> = { + projects: { + key: "projects", + label: "Projects", + href: `/projects`, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/projects/`, + Icon: Briefcase, + }, + "all-issues": { + key: "all-issues", + label: "Views", + href: `/workspace-views/all-issues`, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], + highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/workspace-views/`), + Icon: Layers, + }, + "active-cycles": { + key: "active-cycles", + label: "Cycles", + href: `/active-cycles`, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/active-cycles/`, + Icon: ContrastIcon, + }, + analytics: { + key: "analytics", + label: "Analytics", + href: `/analytics`, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/analytics/`), + Icon: BarChart2, + }, +}; + +export const SIDEBAR_WORKSPACE_MENU_ITEMS: TSidebarWorkspaceMenuItems[] = [ + SIDEBAR_WORKSPACE_MENU?.projects, + SIDEBAR_WORKSPACE_MENU?.["all-issues"], + SIDEBAR_WORKSPACE_MENU?.["active-cycles"], + SIDEBAR_WORKSPACE_MENU?.analytics, +].filter((item): item is TSidebarWorkspaceMenuItems => item !== undefined); diff --git a/web/ce/constants/issues.ts b/web/ce/constants/issues.ts index dc6ffbcb8..99b8ef90d 100644 --- a/web/ce/constants/issues.ts +++ b/web/ce/constants/issues.ts @@ -1,4 +1,6 @@ import { TIssueActivityComment } from "@plane/types"; +// constants +import { ILayoutDisplayFiltersOptions } from "@/constants/issue"; export enum EActivityFilterType { ACTIVITY = "ACTIVITY", @@ -19,7 +21,7 @@ export const ACTIVITY_FILTER_TYPE_OPTIONS: Record void; @@ -32,3 +34,7 @@ export const filterActivityOnSelectedFilters = ( activity.filter((activity) => filter.includes(activity.activity_type as TActivityFilters)); export const ENABLE_ISSUE_DEPENDENCIES = false; + +export const ADDITIONAL_ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { + [pageType: string]: { [layoutType: string]: ILayoutDisplayFiltersOptions }; +} = {}; diff --git a/web/ce/helpers/dashboard.helper.ts b/web/ce/helpers/dashboard.helper.ts index b2fba63ad..c96c818a1 100644 --- a/web/ce/helpers/dashboard.helper.ts +++ b/web/ce/helpers/dashboard.helper.ts @@ -1,5 +1,8 @@ // plane web types -import { TSidebarUserMenuItemKeys } from "@/plane-web/types/dashboard"; +import { TSidebarUserMenuItemKeys, TSidebarWorkspaceMenuItemKeys } from "@/plane-web/types/dashboard"; // eslint-disable-next-line @typescript-eslint/no-unused-vars export const isUserFeatureEnabled = (featureKey: TSidebarUserMenuItemKeys) => true; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const isWorkspaceFeatureEnabled = (featureKey: TSidebarWorkspaceMenuItemKeys, workspaceSlug: string) => true; diff --git a/web/ce/helpers/issue-action-helper.ts b/web/ce/helpers/issue-action-helper.ts new file mode 100644 index 000000000..b1644e2aa --- /dev/null +++ b/web/ce/helpers/issue-action-helper.ts @@ -0,0 +1,15 @@ +import { IssueActions } from "@/hooks/use-issues-actions"; + +export const useTeamIssueActions: () => IssueActions = () => ({ + fetchIssues: () => Promise.resolve(undefined), + fetchNextIssues: () => Promise.resolve(undefined), + removeIssue: () => Promise.resolve(undefined), + updateFilters: () => Promise.resolve(undefined), +}); + +export const useTeamViewIssueActions: () => IssueActions = () => ({ + fetchIssues: () => Promise.resolve(undefined), + fetchNextIssues: () => Promise.resolve(undefined), + removeIssue: () => Promise.resolve(undefined), + updateFilters: () => Promise.resolve(undefined), +}); diff --git a/web/ce/store/command-palette.store.ts b/web/ce/store/command-palette.store.ts index 47c9280cd..1b6fabf18 100644 --- a/web/ce/store/command-palette.store.ts +++ b/web/ce/store/command-palette.store.ts @@ -1,12 +1,26 @@ -import { makeObservable } from "mobx"; +import { computed, makeObservable } from "mobx"; // types / constants import { BaseCommandPaletteStore, IBaseCommandPaletteStore } from "@/store/base-command-palette.store"; -export type ICommandPaletteStore = IBaseCommandPaletteStore; +export interface ICommandPaletteStore extends IBaseCommandPaletteStore { + // computed + isAnyModalOpen: boolean; +} export class CommandPaletteStore extends BaseCommandPaletteStore implements ICommandPaletteStore { constructor() { super(); - makeObservable(this, {}); + makeObservable(this, { + // computed + isAnyModalOpen: computed, + }); + } + + /** + * Checks whether any modal is open or not in the base command palette. + * @returns boolean + */ + get isAnyModalOpen(): boolean { + return Boolean(super.getCoreModalsState()); } } diff --git a/web/ce/store/issue/team-views/filter.store.ts b/web/ce/store/issue/team-views/filter.store.ts new file mode 100644 index 000000000..9c33f9405 --- /dev/null +++ b/web/ce/store/issue/team-views/filter.store.ts @@ -0,0 +1,12 @@ +import { IProjectViewIssuesFilter, ProjectViewIssuesFilter } from "@/store/issue/project-views"; +import { IIssueRootStore } from "@/store/issue/root.store"; + +// @ts-nocheck - This class will never be used, extending similar class to avoid type errors +export type ITeamViewIssuesFilter = IProjectViewIssuesFilter; + +// @ts-nocheck - This class will never be used, extending similar class to avoid type errors +export class TeamViewIssuesFilter extends ProjectViewIssuesFilter implements IProjectViewIssuesFilter { + constructor(_rootStore: IIssueRootStore) { + super(_rootStore); + } +} diff --git a/web/ce/store/issue/team-views/index.ts b/web/ce/store/issue/team-views/index.ts new file mode 100644 index 000000000..0fe6c946b --- /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 000000000..328370f85 --- /dev/null +++ b/web/ce/store/issue/team-views/issue.store.ts @@ -0,0 +1,13 @@ +import { IProjectViewIssues, ProjectViewIssues } from "@/store/issue/project-views"; +import { IIssueRootStore } from "@/store/issue/root.store"; +import { ITeamViewIssuesFilter } from "./filter.store"; + +// @ts-nocheck - This class will never be used, extending similar class to avoid type errors +export type ITeamViewIssues = IProjectViewIssues; + +// @ts-nocheck - This class will never be used, extending similar class to avoid type errors +export class TeamViewIssues extends ProjectViewIssues implements IProjectViewIssues { + constructor(_rootStore: IIssueRootStore, teamViewFilterStore: ITeamViewIssuesFilter) { + super(_rootStore, teamViewFilterStore); + } +} diff --git a/web/ce/store/issue/team/filter.store.ts b/web/ce/store/issue/team/filter.store.ts new file mode 100644 index 000000000..42b2d5dd2 --- /dev/null +++ b/web/ce/store/issue/team/filter.store.ts @@ -0,0 +1,12 @@ +import { IProjectIssuesFilter, ProjectIssuesFilter } from "@/store/issue/project"; +import { IIssueRootStore } from "@/store/issue/root.store"; + +// @ts-nocheck - This class will never be used, extending similar class to avoid type errors +export type ITeamIssuesFilter = IProjectIssuesFilter; + +// @ts-nocheck - This class will never be used, extending similar class to avoid type errors +export class TeamIssuesFilter extends ProjectIssuesFilter implements IProjectIssuesFilter { + constructor(_rootStore: IIssueRootStore) { + super(_rootStore); + } +} diff --git a/web/ce/store/issue/team/index.ts b/web/ce/store/issue/team/index.ts new file mode 100644 index 000000000..0fe6c946b --- /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 000000000..2e3979436 --- /dev/null +++ b/web/ce/store/issue/team/issue.store.ts @@ -0,0 +1,13 @@ +import { IProjectIssues, ProjectIssues } from "@/store/issue/project"; +import { IIssueRootStore } from "@/store/issue/root.store"; +import { ITeamIssuesFilter } from "./filter.store"; + +// @ts-nocheck - This class will never be used, extending similar class to avoid type errors +export type ITeamIssues = IProjectIssues; + +// @ts-nocheck - This class will never be used, extending similar class to avoid type errors +export class TeamIssues extends ProjectIssues implements IProjectIssues { + constructor(_rootStore: IIssueRootStore, teamIssueFilterStore: ITeamIssuesFilter) { + super(_rootStore, teamIssueFilterStore); + } +} diff --git a/web/ce/types/dashboard.ts b/web/ce/types/dashboard.ts index d615ac4af..de35f60c6 100644 --- a/web/ce/types/dashboard.ts +++ b/web/ce/types/dashboard.ts @@ -1 +1,3 @@ export type TSidebarUserMenuItemKeys = "home" | "your-work" | "notifications" | "drafts"; + +export type TSidebarWorkspaceMenuItemKeys = "projects" | "all-issues" | "active-cycles" | "analytics"; diff --git a/web/core/components/cycles/analytics-sidebar/root.tsx b/web/core/components/cycles/analytics-sidebar/root.tsx index fd8c984a6..b709c0e63 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 759569cfa..187425b8d 100644 --- a/web/core/components/cycles/cycle-peek-overview.tsx +++ b/web/core/components/cycles/cycle-peek-overview.tsx @@ -9,12 +9,13 @@ import { useAppRouter } from "@/hooks/use-app-router"; import { CycleDetailsSidebar } from "./"; type Props = { - projectId: string; + projectId?: string; workspaceSlug: string; isArchived?: boolean; }; -export const CyclePeekOverview: React.FC = observer(({ projectId, workspaceSlug, isArchived = false }) => { +export const CyclePeekOverview: React.FC = observer((props) => { + const { projectId: propsProjectId, workspaceSlug, isArchived } = props; // router const router = useAppRouter(); const pathname = usePathname(); @@ -23,22 +24,25 @@ export const CyclePeekOverview: React.FC = observer(({ projectId, workspa // refs const ref = React.useRef(null); // store hooks - const { fetchCycleDetails, fetchArchivedCycleDetails } = useCycle(); + const { getCycleById, fetchCycleDetails, fetchArchivedCycleDetails } = useCycle(); + // derived values + const cycleDetails = peekCycle ? getCycleById(peekCycle.toString()) : undefined; + const projectId = propsProjectId || cycleDetails?.project_id; const handleClose = () => { const query = generateQueryParams(searchParams, ["peekCycle"]); - router.push(`${pathname}?${query}`); + router.push(`${pathname}?${query}`, {}, { showProgressBar: false }); }; useEffect(() => { - if (!peekCycle) return; + if (!peekCycle || !projectId) return; if (isArchived) fetchArchivedCycleDetails(workspaceSlug, projectId, peekCycle.toString()); else fetchCycleDetails(workspaceSlug, projectId, peekCycle.toString()); }, [fetchArchivedCycleDetails, fetchCycleDetails, isArchived, peekCycle, projectId, workspaceSlug]); return ( <> - {peekCycle && ( + {peekCycle && projectId && (
    = observer(({ projectId, workspa "0px 1px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 4px 0px rgba(16, 24, 40, 0.06), 0px 1px 8px -1px rgba(16, 24, 40, 0.06)", }} > - +
    )} diff --git a/web/core/components/cycles/form.tsx b/web/core/components/cycles/form.tsx index 660d33cdb..7651c5d44 100644 --- a/web/core/components/cycles/form.tsx +++ b/web/core/components/cycles/form.tsx @@ -75,6 +75,7 @@ export const CycleForm: React.FC = (props) => { onChange(val); setActiveProject(val); }} + multiple={false} buttonVariant="border-with-text" renderCondition={(project) => shouldRenderProject(project)} tabIndex={getIndex("cover_image")} diff --git a/web/core/components/cycles/list/cycle-list-item-action.tsx b/web/core/components/cycles/list/cycle-list-item-action.tsx index 989e0436e..73dca345d 100644 --- a/web/core/components/cycles/list/cycle-list-item-action.tsx +++ b/web/core/components/cycles/list/cycle-list-item-action.tsx @@ -14,10 +14,9 @@ import { CycleQuickActions } from "@/components/cycles"; import { DateRangeDropdown } from "@/components/dropdowns"; import { ButtonAvatars } from "@/components/dropdowns/member/avatar"; // constants -import { CYCLE_STATUS } from "@/constants/cycle"; import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "@/constants/event-tracker"; // helpers -import { findHowManyDaysLeft, getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; +import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { getFileURL } from "@/helpers/file.helper"; // hooks import { generateQueryParams } from "@/helpers/router.helper"; @@ -69,11 +68,11 @@ export const CycleListItemAction: FC = observer((props) => { const cycleStatus = cycleDetails.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft"; const isEditingAllowed = allowPermissions( [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.PROJECT + EUserPermissionsLevel.PROJECT, + workspaceSlug, + projectId ); const renderIcon = Boolean(cycleDetails.start_date) || Boolean(cycleDetails.end_date); - const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus); - const daysLeft = findHowManyDaysLeft(cycleDetails.end_date) ?? 0; // handlers const handleAddToFavorites = (e: MouseEvent) => { @@ -201,9 +200,9 @@ export const CycleListItemAction: FC = observer((props) => { const query = generateQueryParams(searchParams, ["peekCycle"]); if (searchParams.has("peekCycle") && searchParams.get("peekCycle") === cycleId) { - router.push(`${pathname}?${query}`); + router.push(`${pathname}?${query}`, {}, { showProgressBar: false }); } else { - router.push(`${pathname}?${query && `${query}&`}peekCycle=${cycleId}`); + router.push(`${pathname}?${query && `${query}&`}peekCycle=${cycleId}`, {}, { showProgressBar: false }); } }; diff --git a/web/core/components/cycles/list/cycle-list-project-group-header.tsx b/web/core/components/cycles/list/cycle-list-project-group-header.tsx new file mode 100644 index 000000000..d663eca0d --- /dev/null +++ b/web/core/components/cycles/list/cycle-list-project-group-header.tsx @@ -0,0 +1,44 @@ +"use client"; + +import React, { FC } from "react"; +import { observer } from "mobx-react"; +import { ChevronRight } from "lucide-react"; +// icons +import { Row, Logo } from "@plane/ui"; +// helpers +import { cn } from "@/helpers/common.helper"; +import { useProject } from "@/hooks/store/use-project"; + +type Props = { + projectId: string; + count?: number; + showCount?: boolean; + isExpanded?: boolean; +}; + +export const CycleListProjectGroupHeader: FC = observer((props) => { + const { projectId, count, showCount = false, isExpanded = false } = props; + // store hooks + const { getProjectById } = useProject(); + // derived values + const project = getProjectById(projectId); + + if (!project) return null; + return ( + + +
    + +
    +
    +
    {project.name}
    + {showCount &&
    {`${count ?? "0"}`}
    } +
    +
    + ); +}); diff --git a/web/core/components/cycles/list/cycles-list-item.tsx b/web/core/components/cycles/list/cycles-list-item.tsx index 8d531216a..5954a0a7f 100644 --- a/web/core/components/cycles/list/cycles-list-item.tsx +++ b/web/core/components/cycles/list/cycles-list-item.tsx @@ -4,7 +4,7 @@ import { FC, MouseEvent, useRef } from "react"; import { observer } from "mobx-react"; import { usePathname, useSearchParams } from "next/navigation"; // icons -import { Check, Info } from "lucide-react"; +import { Check } from "lucide-react"; // types import type { TCycleGroups } from "@plane/types"; // ui @@ -72,9 +72,9 @@ export const CyclesListItem: FC = observer((props) => { const query = generateQueryParams(searchParams, ["peekCycle"]); if (searchParams.has("peekCycle") && searchParams.get("peekCycle") === cycleId) { - router.push(`${pathname}?${query}`); + router.push(`${pathname}?${query}`, {}, { showProgressBar: false }); } else { - router.push(`${pathname}?${query && `${query}&`}peekCycle=${cycleId}`); + router.push(`${pathname}?${query && `${query}&`}peekCycle=${cycleId}`, {}, { showProgressBar: false }); } }; diff --git a/web/core/components/cycles/list/index.ts b/web/core/components/cycles/list/index.ts index 4eebc5779..25419a056 100644 --- a/web/core/components/cycles/list/index.ts +++ b/web/core/components/cycles/list/index.ts @@ -3,3 +3,4 @@ export * from "./cycles-list-map"; export * from "./root"; export * from "./cycle-list-item-action"; export * from "./cycle-list-group-header"; +export * from "./cycle-list-project-group-header"; diff --git a/web/core/components/dropdowns/layout.tsx b/web/core/components/dropdowns/layout.tsx index 2557e57a2..7864d1849 100644 --- a/web/core/components/dropdowns/layout.tsx +++ b/web/core/components/dropdowns/layout.tsx @@ -10,18 +10,24 @@ import { EIssueLayoutTypes, ISSUE_LAYOUT_MAP } from "@/constants/issue"; type TLayoutDropDown = { onChange: (value: EIssueLayoutTypes) => void; value: EIssueLayoutTypes; + disabledLayouts?: EIssueLayoutTypes[]; }; export const LayoutDropDown = observer((props: TLayoutDropDown) => { - const { onChange, value = EIssueLayoutTypes.LIST } = props; + const { onChange, value = EIssueLayoutTypes.LIST, disabledLayouts = [] } = props; + // derived values + const availableLayouts = useMemo( + () => Object.values(ISSUE_LAYOUT_MAP).filter((layout) => !disabledLayouts.includes(layout.key)), + [disabledLayouts] + ); const options = useMemo( () => - Object.values(ISSUE_LAYOUT_MAP).map((issueLayout) => ({ + availableLayouts.map((issueLayout) => ({ data: issueLayout.key, value: issueLayout.key, })), - [] + [availableLayouts] ); const buttonContent = useCallback((isOpen: boolean, buttonValue: string | string[] | undefined) => { diff --git a/web/core/components/dropdowns/project.tsx b/web/core/components/dropdowns/project.tsx index 3f973cd16..f94014eb8 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 && (