From 898cf98be36a8c8a6002381713245c1cb59f7915 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Wed, 13 Mar 2024 16:59:12 +0530 Subject: [PATCH 01/22] [WEB-749] fix: rendering empty states with "showEmptyGroup" filter in issue grouping (#3954) * fix: Fixed show empty states in groupBy and subGroupBy in kanban * lint: lint issue resolved --- .../issues/issue-layouts/kanban/default.tsx | 49 ++++-- .../issue-layouts/kanban/kanban-group.tsx | 2 +- .../issues/issue-layouts/kanban/swimlanes.tsx | 161 ++++++++++++------ 3 files changed, 141 insertions(+), 71 deletions(-) diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index 3cbab589f..5bb46f143 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -58,6 +58,7 @@ export interface IGroupByKanBan { scrollableContainerRef?: MutableRefObject; isDragStarted?: boolean; showEmptyGroup?: boolean; + subGroupIssueHeaderCount?: (listId: string) => number; } const GroupByKanBan: React.FC = observer((props) => { @@ -83,6 +84,7 @@ const GroupByKanBan: React.FC = observer((props) => { scrollableContainerRef, isDragStarted, showEmptyGroup = true, + subGroupIssueHeaderCount, } = props; const member = useMember(); @@ -97,17 +99,27 @@ const GroupByKanBan: React.FC = observer((props) => { if (!list) return null; - const groupWithIssues = list.filter((_list) => (issueIds as TGroupedIssues)?.[_list.id]?.length > 0); - - const groupList = showEmptyGroup ? list : groupWithIssues; - - const visibilityGroupBy = (_list: IGroupByColumn) => { + const visibilityGroupBy = (_list: IGroupByColumn): { showGroup: boolean; showIssues: boolean } => { if (sub_group_by) { - if (kanbanFilters?.sub_group_by.includes(_list.id)) return true; - return false; + const groupVisibility = { + showGroup: true, + showIssues: true, + }; + if (!showEmptyGroup) { + groupVisibility.showGroup = subGroupIssueHeaderCount ? subGroupIssueHeaderCount(_list.id) > 0 : true; + } + return groupVisibility; } else { - if (kanbanFilters?.group_by.includes(_list.id)) return true; - return false; + const groupVisibility = { + showGroup: true, + showIssues: true, + }; + if (!showEmptyGroup) { + if ((issueIds as TGroupedIssues)?.[_list.id]?.length > 0) groupVisibility.showGroup = true; + else groupVisibility.showGroup = false; + } + if (kanbanFilters?.group_by.includes(_list.id)) groupVisibility.showIssues = false; + return groupVisibility; } }; @@ -115,13 +127,18 @@ const GroupByKanBan: React.FC = observer((props) => { return (
- {groupList && - groupList.length > 0 && - groupList.map((_list: IGroupByColumn) => { + {list && + list.length > 0 && + list.map((_list: IGroupByColumn) => { const groupByVisibilityToggle = visibilityGroupBy(_list); + if (groupByVisibilityToggle.showGroup === false) return <>; return ( -
+
{sub_group_by === null && (
= observer((props) => {
)} - {!groupByVisibilityToggle && ( + {groupByVisibilityToggle.showIssues && ( = observer((props) => { viewId={viewId} disableIssueCreation={disableIssueCreation} canEditProperties={canEditProperties} - groupByVisibilityToggle={groupByVisibilityToggle} scrollableContainerRef={scrollableContainerRef} isDragStarted={isDragStarted} /> @@ -197,6 +213,7 @@ export interface IKanBan { canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; isDragStarted?: boolean; + subGroupIssueHeaderCount?: (listId: string) => number; } export const KanBan: React.FC = observer((props) => { @@ -221,6 +238,7 @@ export const KanBan: React.FC = observer((props) => { scrollableContainerRef, isDragStarted, showEmptyGroup, + subGroupIssueHeaderCount, } = props; const issueKanBanView = useKanbanView(); @@ -248,6 +266,7 @@ export const KanBan: React.FC = observer((props) => { scrollableContainerRef={scrollableContainerRef} isDragStarted={isDragStarted} showEmptyGroup={showEmptyGroup} + subGroupIssueHeaderCount={subGroupIssueHeaderCount} /> ); }); diff --git a/web/components/issues/issue-layouts/kanban/kanban-group.tsx b/web/components/issues/issue-layouts/kanban/kanban-group.tsx index a05fb1791..5a46324bd 100644 --- a/web/components/issues/issue-layouts/kanban/kanban-group.tsx +++ b/web/components/issues/issue-layouts/kanban/kanban-group.tsx @@ -37,7 +37,7 @@ interface IKanbanGroup { viewId?: string; disableIssueCreation?: boolean; canEditProperties: (projectId: string | undefined) => boolean; - groupByVisibilityToggle: boolean; + groupByVisibilityToggle?: boolean; scrollableContainerRef?: MutableRefObject; isDragStarted?: boolean; } diff --git a/web/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/components/issues/issue-layouts/kanban/swimlanes.tsx index d60e3b618..11f5304b9 100644 --- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -29,6 +29,7 @@ interface ISubGroupSwimlaneHeader { list: IGroupByColumn[]; kanbanFilters: TIssueKanbanFilters; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; + showEmptyGroup: boolean; } const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: string) => { @@ -39,6 +40,22 @@ const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: st return headerCount; }; +const visibilitySubGroupByGroupCount = ( + issueIds: TSubGroupedIssues, + _list: IGroupByColumn, + showEmptyGroup: boolean +): boolean => { + let subGroupHeaderVisibility = true; + + if (showEmptyGroup) subGroupHeaderVisibility = true; + else { + if (getSubGroupHeaderIssuesCount(issueIds, _list.id) > 0) subGroupHeaderVisibility = true; + else subGroupHeaderVisibility = false; + } + + return subGroupHeaderVisibility; +}; + const SubGroupSwimlaneHeader: React.FC = ({ issueIds, sub_group_by, @@ -46,25 +63,36 @@ const SubGroupSwimlaneHeader: React.FC = ({ list, kanbanFilters, handleKanbanFilters, + showEmptyGroup, }) => (
{list && list.length > 0 && - list.map((_list: IGroupByColumn) => ( -
- -
- ))} + list.map((_list: IGroupByColumn) => { + const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount( + issueIds as TSubGroupedIssues, + _list, + showEmptyGroup + ); + + if (subGroupByVisibilityToggle === false) return <>; + + return ( +
+ +
+ ); + })}
); @@ -124,52 +152,74 @@ const SubGroupSwimlane: React.FC = observer((props) => { return issueCount; }; + const visibilitySubGroupBy = (_list: IGroupByColumn): { showGroup: boolean; showIssues: boolean } => { + const subGroupVisibility = { + showGroup: true, + showIssues: true, + }; + if (showEmptyGroup) subGroupVisibility.showGroup = true; + else { + if (calculateIssueCount(_list.id) > 0) subGroupVisibility.showGroup = true; + else subGroupVisibility.showGroup = false; + } + if (kanbanFilters?.sub_group_by.includes(_list.id)) subGroupVisibility.showIssues = false; + return subGroupVisibility; + }; + return (
{list && list.length > 0 && - list.map((_list: any) => ( -
-
-
- -
-
-
+ list.map((_list: any) => { + const subGroupByVisibilityToggle = visibilitySubGroupBy(_list); + if (subGroupByVisibilityToggle.showGroup === false) return <>; - {!kanbanFilters?.sub_group_by.includes(_list.id) && ( -
- + return ( +
+
+
+ +
+
- )} -
- ))} + + {subGroupByVisibilityToggle.showIssues && ( +
+ + getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, groupByListId) + } + /> +
+ )} +
+ ); + })}
); }); @@ -261,6 +311,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { kanbanFilters={kanbanFilters} handleKanbanFilters={handleKanbanFilters} list={groupByList} + showEmptyGroup={showEmptyGroup} />
From 1363ef0b2dc50ec7cd3f508994ccdacd331f214d Mon Sep 17 00:00:00 2001 From: P B Date: Wed, 13 Mar 2024 17:13:11 +0530 Subject: [PATCH 02/22] Updated one-click deploy's readme for readability and presentation (#3962) Changed title, first paragraph, and bullets. --- deploy/1-click/README.md | 93 +++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 49 deletions(-) diff --git a/deploy/1-click/README.md b/deploy/1-click/README.md index 88ea66c4c..fbaac930c 100644 --- a/deploy/1-click/README.md +++ b/deploy/1-click/README.md @@ -1,78 +1,73 @@ -# 1-Click Self-Hosting +# One-click deploy -In this guide, we will walk you through the process of setting up a 1-click self-hosted environment. Self-hosting allows you to have full control over your applications and data. It's a great way to ensure privacy, control, and customization. +Deployment methods for Plane have improved significantly to make self-managing super-easy. One of those is a single-line-command installation of Plane. -Let's get started! +This short guide will guide you through the process, the background tasks that run with the command for the Community, One, and Entprise editions, and the post-deployment configuration options available to you. -## Installing Plane +### Requirements -Installing Plane is a very easy and minimal step process. - -### Prerequisite - -- Operating System (latest): Debian / Ubuntu / Centos -- Supported CPU Architechture: AMD64 / ARM64 / x86_64 / aarch64 - -### Downloading Latest Stable Release +- Operating systems: Debian, Ubuntu, CentOS +- Supported CPU architechtures: AMD64, ARM64, x86_64, aarch64 +### Download the latest stable release +Run ↓ on any CLI. ``` curl -fsSL https://raw.githubusercontent.com/makeplane/plane/master/deploy/1-click/install.sh | sh - ``` -
- Downloading Preview Release +### Download the Preview release +`Preview` builds do not support ARM64/AARCH64 CPU architecture + +Run ↓ on any CLI. ``` export BRANCH=preview curl -fsSL https://raw.githubusercontent.com/makeplane/plane/preview/deploy/1-click/install.sh | sh - ``` - -NOTE: `Preview` builds do not support ARM64/AARCH64 CPU architecture -
- --- - - -Expect this after a successful install +--- +### Successful installation +You should see ↓ if there are no hitches. That output will also list the IP address you can use to access your Plane instance. ![Install Output](images/install.png) -Access the application on a browser via http://server-ip-address - --- +### Manage your Plane instance -### Get Control of your Plane Server Setup - -Plane App is available via the command `plane-app`. Running the command `plane-app --help` helps you to manage Plane +Use `plane-app` [OPERATOR] to manage your Plane instance easily. Get a list of all operators with `plane-app ---help`. ![Plane Help](images/help.png) -Basic Operations: -1. Start Server using `plane-app start` -1. Stop Server using `plane-app stop` -1. Restart Server using `plane-app restart` +1. Basic operators + 1. `plane-app start` starts the Plane server. + 2. `plane-app restart` restarts the Plane server. + 3. `plane-app stop` stops the Plane server. -Advanced Operations: -1. Configure Plane using `plane-app --configure`. This will give you options to modify - - NGINX Port (default 80) - - Domain Name (default is the local server public IP address) - - File Upload Size (default 5MB) - - External Postgres DB Url (optional - default empty) - - External Redis URL (optional - default empty) - - AWS S3 Bucket (optional - to be configured only in case the user wants to use an S3 Bucket) +2. Advanced operations + 1. `plane-app --configure` will show advanced configurators. + - Change your proxy or listening port +
Default: 80 + - Change your domain name +
Default: Deployed server's public IP address + - File upload size +
Default: 5MB + - Specify external database address when using an external database +
Default: `Empty` +
`Default folder: /opt/plane/data/postgres` + - Specify external Redis URL when using external Redis +
Default: `Empty` +
`Default folder: /opt/plane/data/redis` + - Configure AWS S3 bucket +
Use only when you or your users want to use S3 +
`Default folder: /opt/plane/data/minio` -1. Upgrade Plane using `plane-app --upgrade`. This will get the latest stable version of Plane files (docker-compose.yaml, .env, and docker images) +3. Version operators + 1. `plane-app --upgrade` gets the latest stable version of docker-compose.yaml, .env, and Docker images -1. Updating Plane App installer using `plane-app --update-installer` will update the `plane-app` utility. + 2. `plane-app --update-installer` updates the installer and the `plane-app` utility. -1. Uninstall Plane using `plane-app --uninstall`. This will uninstall the Plane application from the server and all docker containers but do not remove the data stored in Postgres, Redis, and Minio. - -1. Plane App can be reinstalled using `plane-app --install`. - -Application Data is stored in the mentioned folders: -1. DB Data: /opt/plane/data/postgres -1. Redis Data: /opt/plane/data/redis -1. Minio Data: /opt/plane/data/minio \ No newline at end of file + 3. `plane-app --uninstall` uninstalls the Plane application and all Docker containers from the server but leaves the data stored in + Postgres, Redis, and Minio alone. + 4. `plane-app --install` installs the Plane app again. From 552c66457a2408cb3295f8c92a012fb1b6a12fd3 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Wed, 13 Mar 2024 17:17:40 +0530 Subject: [PATCH 03/22] chore: 1click docs update --- deploy/1-click/README.md | 62 +++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/deploy/1-click/README.md b/deploy/1-click/README.md index fbaac930c..783f88371 100644 --- a/deploy/1-click/README.md +++ b/deploy/1-click/README.md @@ -10,30 +10,37 @@ This short guide will guide you through the process, the background tasks that r - Supported CPU architechtures: AMD64, ARM64, x86_64, aarch64 ### Download the latest stable release + Run ↓ on any CLI. + ``` curl -fsSL https://raw.githubusercontent.com/makeplane/plane/master/deploy/1-click/install.sh | sh - ``` - ### Download the Preview release + `Preview` builds do not support ARM64/AARCH64 CPU architecture Run ↓ on any CLI. + ``` export BRANCH=preview curl -fsSL https://raw.githubusercontent.com/makeplane/plane/preview/deploy/1-click/install.sh | sh - ``` + --- + ### Successful installation + You should see ↓ if there are no hitches. That output will also list the IP address you can use to access your Plane instance. ![Install Output](images/install.png) --- + ### Manage your Plane instance Use `plane-app` [OPERATOR] to manage your Plane instance easily. Get a list of all operators with `plane-app ---help`. @@ -41,33 +48,36 @@ Use `plane-app` [OPERATOR] to manage your Plane instance easily. Get a list of a ![Plane Help](images/help.png) 1. Basic operators - 1. `plane-app start` starts the Plane server. - 2. `plane-app restart` restarts the Plane server. - 3. `plane-app stop` stops the Plane server. -2. Advanced operations - 1. `plane-app --configure` will show advanced configurators. - - Change your proxy or listening port -
Default: 80 - - Change your domain name -
Default: Deployed server's public IP address - - File upload size -
Default: 5MB - - Specify external database address when using an external database -
Default: `Empty` -
`Default folder: /opt/plane/data/postgres` - - Specify external Redis URL when using external Redis -
Default: `Empty` -
`Default folder: /opt/plane/data/redis` - - Configure AWS S3 bucket -
Use only when you or your users want to use S3 -
`Default folder: /opt/plane/data/minio` + 1. `plane-app start` starts the Plane server. + 2. `plane-app restart` restarts the Plane server. + 3. `plane-app stop` stops the Plane server. + +2. Advanced operators + `plane-app --configure` will show advanced configurators. + + - Change your proxy or listening port +
Default: 80 + - Change your domain name +
Default: Deployed server's public IP address + - File upload size +
Default: 5MB + - Specify external database address when using an external database +
Default: `Empty` +
`Default folder: /opt/plane/data/postgres` + - Specify external Redis URL when using external Redis +
Default: `Empty` +
`Default folder: /opt/plane/data/redis` + - Configure AWS S3 bucket +
Use only when you or your users want to use S3 +
`Default folder: /opt/plane/data/minio` 3. Version operators - 1. `plane-app --upgrade` gets the latest stable version of docker-compose.yaml, .env, and Docker images - 2. `plane-app --update-installer` updates the installer and the `plane-app` utility. + 1. `plane-app --upgrade` gets the latest stable version of docker-compose.yaml, .env, and Docker images - 3. `plane-app --uninstall` uninstalls the Plane application and all Docker containers from the server but leaves the data stored in - Postgres, Redis, and Minio alone. - 4. `plane-app --install` installs the Plane app again. + 2. `plane-app --update-installer` updates the installer and the `plane-app` utility. + + 3. `plane-app --uninstall` uninstalls the Plane application and all Docker containers from the server but leaves the data stored in + Postgres, Redis, and Minio alone. + 4. `plane-app --install` installs the Plane app again. From 884856b02103011c8c374887231174e1dbc0ae84 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Wed, 13 Mar 2024 17:18:11 +0530 Subject: [PATCH 04/22] chore: 1click docs update --- deploy/1-click/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy/1-click/README.md b/deploy/1-click/README.md index 783f88371..e586d700c 100644 --- a/deploy/1-click/README.md +++ b/deploy/1-click/README.md @@ -54,6 +54,7 @@ Use `plane-app` [OPERATOR] to manage your Plane instance easily. Get a list of a 3. `plane-app stop` stops the Plane server. 2. Advanced operators + `plane-app --configure` will show advanced configurators. - Change your proxy or listening port From 4ccb505f368eaa15a5c0fa54c7bc03368b0d4c39 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Wed, 13 Mar 2024 17:24:37 +0530 Subject: [PATCH 05/22] chore: 1click docs changes --- deploy/1-click/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/deploy/1-click/README.md b/deploy/1-click/README.md index e586d700c..e114b19f3 100644 --- a/deploy/1-click/README.md +++ b/deploy/1-click/README.md @@ -2,12 +2,12 @@ Deployment methods for Plane have improved significantly to make self-managing super-easy. One of those is a single-line-command installation of Plane. -This short guide will guide you through the process, the background tasks that run with the command for the Community, One, and Entprise editions, and the post-deployment configuration options available to you. +This short guide will guide you through the process, the background tasks that run with the command for the Community, One, and Enterprise editions, and the post-deployment configuration options available to you. ### Requirements - Operating systems: Debian, Ubuntu, CentOS -- Supported CPU architechtures: AMD64, ARM64, x86_64, aarch64 +- Supported CPU architectures: AMD64, ARM64, x86_64, AArch64 ### Download the latest stable release @@ -20,7 +20,7 @@ curl -fsSL https://raw.githubusercontent.com/makeplane/plane/master/deploy/1-cli ### Download the Preview release -`Preview` builds do not support ARM64/AARCH64 CPU architecture +`Preview` builds do not support ARM64, AArch64 CPU architectures Run ↓ on any CLI. @@ -75,7 +75,7 @@ Use `plane-app` [OPERATOR] to manage your Plane instance easily. Get a list of a 3. Version operators - 1. `plane-app --upgrade` gets the latest stable version of docker-compose.yaml, .env, and Docker images + 1. `plane-app --upgrade` gets the latest stable version of `docker-compose.yaml`, `.env`, and Docker images 2. `plane-app --update-installer` updates the installer and the `plane-app` utility. From 8d9adf4d87c736d60eeef97a312a9150de101256 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Wed, 13 Mar 2024 17:26:04 +0530 Subject: [PATCH 06/22] chore: 1click docs readme update --- deploy/1-click/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/deploy/1-click/README.md b/deploy/1-click/README.md index e114b19f3..17d9eefee 100644 --- a/deploy/1-click/README.md +++ b/deploy/1-click/README.md @@ -76,9 +76,7 @@ Use `plane-app` [OPERATOR] to manage your Plane instance easily. Get a list of a 3. Version operators 1. `plane-app --upgrade` gets the latest stable version of `docker-compose.yaml`, `.env`, and Docker images - 2. `plane-app --update-installer` updates the installer and the `plane-app` utility. - 3. `plane-app --uninstall` uninstalls the Plane application and all Docker containers from the server but leaves the data stored in Postgres, Redis, and Minio alone. 4. `plane-app --install` installs the Plane app again. From 9c13dbd95796a724e098911a0b24b7d2be73309d Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Wed, 13 Mar 2024 17:26:52 +0530 Subject: [PATCH 07/22] chore: 1click docs readme update --- deploy/1-click/README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/deploy/1-click/README.md b/deploy/1-click/README.md index 17d9eefee..9ed2323de 100644 --- a/deploy/1-click/README.md +++ b/deploy/1-click/README.md @@ -15,7 +15,6 @@ Run ↓ on any CLI. ``` curl -fsSL https://raw.githubusercontent.com/makeplane/plane/master/deploy/1-click/install.sh | sh - - ``` ### Download the Preview release @@ -26,9 +25,7 @@ Run ↓ on any CLI. ``` export BRANCH=preview - curl -fsSL https://raw.githubusercontent.com/makeplane/plane/preview/deploy/1-click/install.sh | sh - - ``` --- From 6bc133e3b16db8a3b1b1f593b30c09ab1ce5eabc Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Thu, 14 Mar 2024 20:02:18 +0530 Subject: [PATCH 08/22] chore: swapped dates and status properties in the cycle list item (#3957) --- web/components/cycles/list/cycles-list-item.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/web/components/cycles/list/cycles-list-item.tsx b/web/components/cycles/list/cycles-list-item.tsx index 52eb3f732..8a5cee535 100644 --- a/web/components/cycles/list/cycles-list-item.tsx +++ b/web/components/cycles/list/cycles-list-item.tsx @@ -178,7 +178,11 @@ export const CyclesListItem: FC = observer((props) => {
- +
+ {renderDate && `${renderFormattedDate(startDate) ?? `_ _`} - ${renderFormattedDate(endDate) ?? `_ _`}`} +
+
+
{currentCycle && (
= observer((props) => { : `${currentCycle.label}`}
)} -
-
-
- {renderDate && `${renderFormattedDate(startDate) ?? `_ _`} - ${renderFormattedDate(endDate) ?? `_ _`}`} -
From d25fcfdd88d1d8ce5a5c064f0e6793e711e89ce0 Mon Sep 17 00:00:00 2001 From: Ramesh Kumar Chandra <31303617+rameshkumarchandra@users.noreply.github.com> Date: Fri, 15 Mar 2024 17:25:57 +0530 Subject: [PATCH 09/22] [WEB-715] chore: closing custom dropdown on button click (#3950) --- packages/ui/src/dropdowns/custom-menu.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/dropdowns/custom-menu.tsx b/packages/ui/src/dropdowns/custom-menu.tsx index d1623dddf..b94faf436 100644 --- a/packages/ui/src/dropdowns/custom-menu.tsx +++ b/packages/ui/src/dropdowns/custom-menu.tsx @@ -114,7 +114,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => { type="button" onClick={(e) => { e.stopPropagation(); - openDropdown(); + isOpen ? closeDropdown() : openDropdown(); if (menuButtonOnClick) menuButtonOnClick(); }} className={customButtonClassName} @@ -132,7 +132,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => { type="button" onClick={(e) => { e.stopPropagation(); - openDropdown(); + isOpen ? closeDropdown() : openDropdown(); if (menuButtonOnClick) menuButtonOnClick(); }} disabled={disabled} @@ -158,7 +158,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => { } ${buttonClassName}`} onClick={(e) => { e.stopPropagation(); - openDropdown(); + isOpen ? closeDropdown() : openDropdown(); if (menuButtonOnClick) menuButtonOnClick(); }} tabIndex={customButtonTabIndex} From cbe5d9a047149687ee299176beb766be7aa3de59 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 15 Mar 2024 17:26:38 +0530 Subject: [PATCH 10/22] chore: issue reaction response updated (#3951) --- packages/types/src/issues/issue_reaction.d.ts | 4 ++-- web/components/issues/issue-detail/reactions/issue.tsx | 2 +- web/store/issue/issue-details/reaction.store.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/types/src/issues/issue_reaction.d.ts b/packages/types/src/issues/issue_reaction.d.ts index a4eaee0a8..7fba8cd9c 100644 --- a/packages/types/src/issues/issue_reaction.d.ts +++ b/packages/types/src/issues/issue_reaction.d.ts @@ -1,7 +1,7 @@ export type TIssueReaction = { - actor_id: string; + actor: string; id: string; - issue_id: string; + issue: string; reaction: string; }; diff --git a/web/components/issues/issue-detail/reactions/issue.tsx b/web/components/issues/issue-detail/reactions/issue.tsx index c21f92139..6fcd35ac4 100644 --- a/web/components/issues/issue-detail/reactions/issue.tsx +++ b/web/components/issues/issue-detail/reactions/issue.tsx @@ -79,7 +79,7 @@ export const IssueReaction: FC = observer((props) => { const reactionUsers = (reactionIds?.[reaction] || []) .map((reactionId) => { const reactionDetails = getReactionById(reactionId); - return reactionDetails ? getUserDetails(reactionDetails.actor_id)?.display_name : null; + return reactionDetails ? getUserDetails(reactionDetails.actor)?.display_name : null; }) .filter((displayName): displayName is string => !!displayName); diff --git a/web/store/issue/issue-details/reaction.store.ts b/web/store/issue/issue-details/reaction.store.ts index a32ba6eca..e873e4c8c 100644 --- a/web/store/issue/issue-details/reaction.store.ts +++ b/web/store/issue/issue-details/reaction.store.ts @@ -84,7 +84,7 @@ export class IssueReactionStore implements IIssueReactionStore { if (reactions?.[reaction]) reactions?.[reaction].map((reactionId) => { const currentReaction = this.getReactionById(reactionId); - if (currentReaction && currentReaction.actor_id === userId) _userReactions.push(currentReaction); + if (currentReaction && currentReaction.actor === userId) _userReactions.push(currentReaction); }); }); @@ -151,7 +151,7 @@ export class IssueReactionStore implements IIssueReactionStore { ) => { try { const userReactions = this.reactionsByUser(issueId, userId); - const currentReaction = find(userReactions, { actor_id: userId, reaction: reaction }); + const currentReaction = find(userReactions, { actor: userId, reaction: reaction }); if (currentReaction && currentReaction.id) { runInAction(() => { From ed2e4ad6f745ab20bad4e4d743d514060c301833 Mon Sep 17 00:00:00 2001 From: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com> Date: Fri, 15 Mar 2024 17:28:03 +0530 Subject: [PATCH 11/22] fix: cycle & module peekview scroll (#3953) --- web/components/cycles/cycle-peek-overview.tsx | 2 +- web/components/modules/module-peek-overview.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/cycles/cycle-peek-overview.tsx b/web/components/cycles/cycle-peek-overview.tsx index b7e778c10..b15e89ab2 100644 --- a/web/components/cycles/cycle-peek-overview.tsx +++ b/web/components/cycles/cycle-peek-overview.tsx @@ -38,7 +38,7 @@ export const CyclePeekOverview: React.FC = observer(({ projectId, workspa {peekCycle && (
= observer(({ projectId, worksp {peekModule && (
Date: Fri, 15 Mar 2024 17:28:45 +0530 Subject: [PATCH 12/22] [WEB-504] chore: command k and issue relation modal empty state (#3955) * chore: empty state asset updated * chore: empty state asset updated * chore: empty state config file updated * chore: notification empty state updated * chore: command-k, bulk delete and issue relation modal empty state updated * chore: code refactor * chore: code refactor --- .../command-palette/command-modal.tsx | 25 ++++-- .../core/modals/bulk-delete-issues-modal.tsx | 29 ++++--- .../modals/existing-issues-list-modal.tsx | 25 +++--- web/components/core/modals/index.ts | 1 + .../modals/issue-search-modal-empty-state.tsx | 36 ++++++++ web/components/empty-state/empty-state.tsx | 24 +++++- .../inbox/modals/select-duplicate.tsx | 27 +++--- .../issues/parent-issues-list-modal.tsx | 23 +++--- .../notifications/notification-popover.tsx | 41 +++++---- web/constants/empty-state.ts | 78 ++++++++++++++++-- .../empty-state/search/archive-dark.webp | Bin 0 -> 2292 bytes .../empty-state/search/archive-light.webp | Bin 0 -> 2434 bytes .../empty-state/search/comments-dark.webp | Bin 0 -> 2574 bytes .../empty-state/search/comments-light.webp | Bin 0 -> 2744 bytes web/public/empty-state/search/issue-dark.webp | Bin 0 -> 3118 bytes .../empty-state/search/issues-light.webp | Bin 0 -> 2924 bytes .../empty-state/search/notification-dark.webp | Bin 0 -> 2286 bytes .../search/notification-light.webp | Bin 0 -> 2524 bytes .../empty-state/search/search-dark.webp | Bin 0 -> 2330 bytes .../empty-state/search/search-light.webp | Bin 0 -> 2644 bytes .../empty-state/search/snooze-dark.webp | Bin 0 -> 2464 bytes .../empty-state/search/snooze-light.webp | Bin 0 -> 2790 bytes 22 files changed, 231 insertions(+), 78 deletions(-) create mode 100644 web/components/core/modals/issue-search-modal-empty-state.tsx create mode 100644 web/public/empty-state/search/archive-dark.webp create mode 100644 web/public/empty-state/search/archive-light.webp create mode 100644 web/public/empty-state/search/comments-dark.webp create mode 100644 web/public/empty-state/search/comments-light.webp create mode 100644 web/public/empty-state/search/issue-dark.webp create mode 100644 web/public/empty-state/search/issues-light.webp create mode 100644 web/public/empty-state/search/notification-dark.webp create mode 100644 web/public/empty-state/search/notification-light.webp create mode 100644 web/public/empty-state/search/search-dark.webp create mode 100644 web/public/empty-state/search/search-light.webp create mode 100644 web/public/empty-state/search/snooze-dark.webp create mode 100644 web/public/empty-state/search/snooze-light.webp diff --git a/web/components/command-palette/command-modal.tsx b/web/components/command-palette/command-modal.tsx index cffd3ff11..60c4fcc04 100644 --- a/web/components/command-palette/command-modal.tsx +++ b/web/components/command-palette/command-modal.tsx @@ -4,9 +4,19 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; import { Dialog, Transition } from "@headlessui/react"; +// icons import { FolderPlus, Search, Settings } from "lucide-react"; // hooks +import { useApplication, useEventTracker, useProject } from "hooks/store"; +import { usePlatformOS } from "hooks/use-platform-os"; +import useDebounce from "hooks/use-debounce"; +// services +import { IssueService } from "services/issue"; +import { WorkspaceService } from "services/workspace.service"; +// ui import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui"; +// components +import { EmptyState } from "components/empty-state"; import { CommandPaletteThemeActions, ChangeIssueAssignee, @@ -18,18 +28,13 @@ import { CommandPaletteWorkspaceSettingsActions, CommandPaletteSearchResults, } from "components/command-palette"; -import { ISSUE_DETAILS } from "constants/fetch-keys"; -import { useApplication, useEventTracker, useProject } from "hooks/store"; -import { usePlatformOS } from "hooks/use-platform-os"; -// services -import useDebounce from "hooks/use-debounce"; -import { IssueService } from "services/issue"; -import { WorkspaceService } from "services/workspace.service"; // types import { IWorkspaceSearchResults } from "@plane/types"; // fetch-keys +// constants +import { EmptyStateType } from "constants/empty-state"; +import { ISSUE_DETAILS } from "constants/fetch-keys"; -// services const workspaceService = new WorkspaceService(); const issueService = new IssueService(); @@ -244,7 +249,9 @@ export const CommandModal: React.FC = observer(() => { )} {!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && ( -
No results found.
+
+ +
)} {(isLoading || isSearching) && ( diff --git a/web/components/core/modals/bulk-delete-issues-modal.tsx b/web/components/core/modals/bulk-delete-issues-modal.tsx index 94d665fa7..05b98176c 100644 --- a/web/components/core/modals/bulk-delete-issues-modal.tsx +++ b/web/components/core/modals/bulk-delete-issues-modal.tsx @@ -5,22 +5,22 @@ import { SubmitHandler, useForm } from "react-hook-form"; import useSWR from "swr"; import { Combobox, Dialog, Transition } from "@headlessui/react"; // services -import { Search } from "lucide-react"; -import { Button, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui"; - -import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; -import { EIssuesStoreType } from "constants/issue"; -import { useIssues, useProject } from "hooks/store"; import { IssueService } from "services/issue"; // ui +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // icons +import { Search } from "lucide-react"; // types import { IUser, TIssue } from "@plane/types"; -// fetch keys // store hooks +import { useIssues, useProject } from "hooks/store"; // components import { BulkDeleteIssuesModalItem } from "./bulk-delete-issues-modal-item"; +import { EmptyState } from "components/empty-state"; // constants +import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; +import { EIssuesStoreType } from "constants/issue"; +import { EmptyStateType } from "constants/empty-state"; type FormInput = { delete_issue_ids: string[]; @@ -178,12 +178,15 @@ export const BulkDeleteIssuesModal: React.FC = observer((props) => { ) : ( -
- -

- No issues found. Create a new issue with{" "} -
C
. -

+
+
)} diff --git a/web/components/core/modals/existing-issues-list-modal.tsx b/web/components/core/modals/existing-issues-list-modal.tsx index 79f134b31..b3f81b6ee 100644 --- a/web/components/core/modals/existing-issues-list-modal.tsx +++ b/web/components/core/modals/existing-issues-list-modal.tsx @@ -2,12 +2,14 @@ import React, { useEffect, useState } from "react"; import { Combobox, Dialog, Transition } from "@headlessui/react"; import { Rocket, Search, X } from "lucide-react"; // services -import { Button, LayersIcon, Loader, ToggleSwitch, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; - +import { ProjectService } from "services/project"; +// hooks import useDebounce from "hooks/use-debounce"; import { usePlatformOS } from "hooks/use-platform-os"; -import { ProjectService } from "services/project"; +// components +import { IssueSearchModalEmptyState } from "./issue-search-modal-empty-state"; // ui +import { Button, Loader, ToggleSwitch, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // types import { ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types"; @@ -40,7 +42,7 @@ export const ExistingIssuesListModal: React.FC = (props) => { const [isSearching, setIsSearching] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false); - const { isMobile } = usePlatformOS(); + const { isMobile } = usePlatformOS(); const debouncedSearchTerm: string = useDebounce(searchTerm, 500); const handleClose = () => { @@ -192,15 +194,12 @@ export const ExistingIssuesListModal: React.FC = (props) => { )} - {!isSearching && issues.length === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && ( -
- -

- No issues found. Create a new issue with{" "} -
C
. -

-
- )} + {isSearching ? ( diff --git a/web/components/core/modals/index.ts b/web/components/core/modals/index.ts index cf72365f5..a95c22114 100644 --- a/web/components/core/modals/index.ts +++ b/web/components/core/modals/index.ts @@ -4,3 +4,4 @@ export * from "./gpt-assistant-popover"; export * from "./link-modal"; export * from "./user-image-upload-modal"; export * from "./workspace-image-upload-modal"; +export * from "./issue-search-modal-empty-state"; diff --git a/web/components/core/modals/issue-search-modal-empty-state.tsx b/web/components/core/modals/issue-search-modal-empty-state.tsx new file mode 100644 index 000000000..00dcc03bb --- /dev/null +++ b/web/components/core/modals/issue-search-modal-empty-state.tsx @@ -0,0 +1,36 @@ +import React from "react"; +// components +import { EmptyState } from "components/empty-state"; +// types +import { ISearchIssueResponse } from "@plane/types"; +// constants +import { EmptyStateType } from "constants/empty-state"; + +interface EmptyStateProps { + issues: ISearchIssueResponse[]; + searchTerm: string; + debouncedSearchTerm: string; + isSearching: boolean; +} + +export const IssueSearchModalEmptyState: React.FC = ({ + issues, + searchTerm, + debouncedSearchTerm, + isSearching, +}) => { + const renderEmptyState = (type: EmptyStateType) => ( +
+ +
+ ); + + const emptyState = + issues.length === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && !isSearching + ? renderEmptyState(EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE) + : issues.length === 0 + ? renderEmptyState(EmptyStateType.ISSUE_RELATION_EMPTY_STATE) + : null; + + return emptyState; +}; diff --git a/web/components/empty-state/empty-state.tsx b/web/components/empty-state/empty-state.tsx index e718c065a..783025679 100644 --- a/web/components/empty-state/empty-state.tsx +++ b/web/components/empty-state/empty-state.tsx @@ -16,7 +16,7 @@ import { cn } from "helpers/common.helper"; export type EmptyStateProps = { type: EmptyStateType; size?: "sm" | "md" | "lg"; - layout?: "widget-simple" | "screen-detailed" | "screen-simple"; + layout?: "screen-detailed" | "screen-simple"; additionalPath?: string; primaryButtonOnClick?: () => void; primaryButtonLink?: string; @@ -149,6 +149,28 @@ export const EmptyState: React.FC = (props) => {
)} + {layout === "screen-simple" && ( +
+
+ {key +
+ {description ? ( + <> +

{title}

+

{description}

+ + ) : ( +

{title}

+ )} +
+ )} ); }; diff --git a/web/components/inbox/modals/select-duplicate.tsx b/web/components/inbox/modals/select-duplicate.tsx index 321628f53..b34dd4ee7 100644 --- a/web/components/inbox/modals/select-duplicate.tsx +++ b/web/components/inbox/modals/select-duplicate.tsx @@ -2,15 +2,19 @@ import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; import { Combobox, Dialog, Transition } from "@headlessui/react"; +// hooks +import { useProject, useProjectState } from "hooks/store"; // icons import { Search } from "lucide-react"; +// components +import { EmptyState } from "components/empty-state"; // ui -import { Button, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui"; -// fetch-keys -import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; -import { useProject, useProjectState } from "hooks/store"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // services import { IssueService } from "services/issue"; +// constants +import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; +import { EmptyStateType } from "constants/empty-state"; type Props = { isOpen: boolean; @@ -158,12 +162,15 @@ export const SelectDuplicateInboxIssueModal: React.FC = (props) => { ) : ( -
- -

- No issues found. Create a new issue with{" "} -
C
. -

+
+
)} diff --git a/web/components/issues/parent-issues-list-modal.tsx b/web/components/issues/parent-issues-list-modal.tsx index 7f5f4984b..cdf642711 100644 --- a/web/components/issues/parent-issues-list-modal.tsx +++ b/web/components/issues/parent-issues-list-modal.tsx @@ -3,14 +3,16 @@ import { useRouter } from "next/router"; // headless ui import { Combobox, Dialog, Transition } from "@headlessui/react"; // services -import { Rocket, Search } from "lucide-react"; -import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui"; -import useDebounce from "hooks/use-debounce"; import { ProjectService } from "services/project"; // hooks +import useDebounce from "hooks/use-debounce"; import { usePlatformOS } from "hooks/use-platform-os"; +// components +import { IssueSearchModalEmptyState } from "components/core"; // ui +import { Loader, ToggleSwitch, Tooltip } from "@plane/ui"; // icons +import { Rocket, Search } from "lucide-react"; // types import { ISearchIssueResponse } from "@plane/types"; @@ -151,15 +153,12 @@ export const ParentIssuesListModal: React.FC = ({ )} - {!isSearching && issues.length === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && ( -
- -

- No issues found. Create a new issue with{" "} -
C
. -

-
- )} + {isSearching ? ( diff --git a/web/components/notifications/notification-popover.tsx b/web/components/notifications/notification-popover.tsx index c3e508688..4dc595c0b 100644 --- a/web/components/notifications/notification-popover.tsx +++ b/web/components/notifications/notification-popover.tsx @@ -1,21 +1,22 @@ import React, { Fragment } from "react"; import { observer } from "mobx-react-lite"; import { Popover, Transition } from "@headlessui/react"; -import { Bell } from "lucide-react"; // hooks -import { Tooltip } from "@plane/ui"; -import { EmptyState } from "components/common"; -import { SnoozeNotificationModal, NotificationCard, NotificationHeader } from "components/notifications"; -import { NotificationsLoader } from "components/ui"; -import { getNumberCount } from "helpers/string.helper"; import { useApplication } from "hooks/store"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useUserNotification from "hooks/use-user-notifications"; import { usePlatformOS } from "hooks/use-platform-os"; +// icons +import { Bell } from "lucide-react"; // components -// images -import emptyNotification from "public/empty-state/notification.svg"; +import { Tooltip } from "@plane/ui"; +import { EmptyState } from "components/empty-state"; +import { NotificationsLoader } from "components/ui"; +import { SnoozeNotificationModal, NotificationCard, NotificationHeader } from "components/notifications"; +// constants +import { EmptyStateType } from "constants/empty-state"; // helpers +import { getNumberCount } from "helpers/string.helper"; export const NotificationPopover = observer(() => { // states @@ -59,6 +60,16 @@ export const NotificationPopover = observer(() => { if (selectedNotificationForSnooze === null) setIsActive(false); }); + const currentTabEmptyState = snoozed + ? EmptyStateType.NOTIFICATION_SNOOZED_EMPTY_STATE + : archived + ? EmptyStateType.NOTIFICATION_ARCHIVED_EMPTY_STATE + : selectedTab === "created" + ? EmptyStateType.NOTIFICATION_CREATED_EMPTY_STATE + : selectedTab === "watching" + ? EmptyStateType.NOTIFICATION_SUBSCRIBED_EMPTY_STATE + : EmptyStateType.NOTIFICATION_MY_ISSUE_EMPTY_STATE; + return ( <> { /> <> - +
) : (
- +
) ) : ( diff --git a/web/constants/empty-state.ts b/web/constants/empty-state.ts index dd6d76ef3..587f58cee 100644 --- a/web/constants/empty-state.ts +++ b/web/constants/empty-state.ts @@ -59,7 +59,6 @@ export enum EmptyStateType { PROJECT_DRAFT_NO_ISSUES = "project-draft-no-issues", VIEWS_EMPTY_SEARCH = "views-empty-search", PROJECTS_EMPTY_SEARCH = "projects-empty-search", - COMMANDK_EMPTY_SEARCH = "commandK-empty-search", MEMBERS_EMPTY_SEARCH = "members-empty-search", PROJECT_MODULE_ISSUES = "project-module-issues", PROJECT_MODULE = "project-module", @@ -71,6 +70,18 @@ export enum EmptyStateType { PROJECT_PAGE_SHARED = "project-page-shared", PROJECT_PAGE_ARCHIVED = "project-page-archived", PROJECT_PAGE_RECENT = "project-page-recent", + + COMMAND_K_SEARCH_EMPTY_STATE = "command-k-search-empty-state", + ISSUE_RELATION_SEARCH_EMPTY_STATE = "issue-relation-search-empty-state", + ISSUE_RELATION_EMPTY_STATE = "issue-relation-empty-state", + ISSUE_COMMENT_EMPTY_STATE = "issue-comment-empty-state", + + NOTIFICATION_MY_ISSUE_EMPTY_STATE = "notification-my-issues-empty-state", + NOTIFICATION_CREATED_EMPTY_STATE = "notification-created-empty-state", + NOTIFICATION_SUBSCRIBED_EMPTY_STATE = "notification-subscribed-empty-state", + NOTIFICATION_ARCHIVED_EMPTY_STATE = "notification-archived-empty-state", + NOTIFICATION_SNOOZED_EMPTY_STATE = "notification-snoozed-empty-state", + NOTIFICATION_UNREAD_EMPTY_STATE = "notification-unread-empty-state", } const emptyStateDetails = { @@ -384,11 +395,6 @@ const emptyStateDetails = { description: "No projects detected with the matching criteria. Create a new project instead.", path: "/empty-state/search/project", }, - [EmptyStateType.COMMANDK_EMPTY_SEARCH]: { - key: EmptyStateType.COMMANDK_EMPTY_SEARCH, - title: "No results found. ", - path: "/empty-state/search/search", - }, [EmptyStateType.MEMBERS_EMPTY_SEARCH]: { key: EmptyStateType.MEMBERS_EMPTY_SEARCH, title: "No matching members", @@ -504,6 +510,66 @@ const emptyStateDetails = { accessType: "project", access: EUserProjectRoles.MEMBER, }, + + [EmptyStateType.COMMAND_K_SEARCH_EMPTY_STATE]: { + key: EmptyStateType.COMMAND_K_SEARCH_EMPTY_STATE, + title: "No results found", + path: "/empty-state/search/search", + }, + [EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE]: { + key: EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE, + title: "No maching issues found", + path: "/empty-state/search/search", + }, + [EmptyStateType.ISSUE_RELATION_EMPTY_STATE]: { + key: EmptyStateType.ISSUE_RELATION_EMPTY_STATE, + title: "No issues found", + path: "/empty-state/search/issues", + }, + [EmptyStateType.ISSUE_COMMENT_EMPTY_STATE]: { + key: EmptyStateType.ISSUE_COMMENT_EMPTY_STATE, + title: "No comments yet", + description: "Comments can be used as a discussion and follow-up space for the issues", + path: "/empty-state/search/comments", + }, + + [EmptyStateType.NOTIFICATION_MY_ISSUE_EMPTY_STATE]: { + key: EmptyStateType.NOTIFICATION_MY_ISSUE_EMPTY_STATE, + title: "No issues assigned", + description: "Updates for issues assigned to you can be \n seen here", + path: "/empty-state/search/notification", + }, + + [EmptyStateType.NOTIFICATION_CREATED_EMPTY_STATE]: { + key: EmptyStateType.NOTIFICATION_CREATED_EMPTY_STATE, + title: "No updates to issues", + description: "Updates to issues created by you can be \n seen here", + path: "/empty-state/search/notification", + }, + [EmptyStateType.NOTIFICATION_SUBSCRIBED_EMPTY_STATE]: { + key: EmptyStateType.NOTIFICATION_SUBSCRIBED_EMPTY_STATE, + title: "No updates to issues", + description: "Updates to any issue you are \n subscribed to can be seen here", + path: "/empty-state/search/notification", + }, + [EmptyStateType.NOTIFICATION_UNREAD_EMPTY_STATE]: { + key: EmptyStateType.NOTIFICATION_UNREAD_EMPTY_STATE, + title: "No unread notifications", + description: "Congratulations, you are up-to-date \n with everything happening in the issues \n you care about", + path: "/empty-state/search/notification", + }, + [EmptyStateType.NOTIFICATION_SNOOZED_EMPTY_STATE]: { + key: EmptyStateType.NOTIFICATION_SNOOZED_EMPTY_STATE, + title: "No snoozed notifications yet", + description: "Any notification you snooze for later will \n be available here to act upon", + path: "/empty-state/search/snooze", + }, + [EmptyStateType.NOTIFICATION_ARCHIVED_EMPTY_STATE]: { + key: EmptyStateType.NOTIFICATION_ARCHIVED_EMPTY_STATE, + title: "No archived notifications yet", + description: "Any notification you archive will be \n available here to help you focus", + path: "/empty-state/search/archive", + }, } as const; export const EMPTY_STATE_DETAILS: Record = emptyStateDetails; diff --git a/web/public/empty-state/search/archive-dark.webp b/web/public/empty-state/search/archive-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..d586be888624e9bb074aefbed3a1f103419d083d GIT binary patch literal 2292 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 AUjP6A literal 0 HcmV?d00001 diff --git a/web/public/empty-state/search/comments-dark.webp b/web/public/empty-state/search/comments-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..d06ba7e4288cb6c3ce390a826bc1411b6e0bbc28 GIT binary patch literal 2574 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 literal 0 HcmV?d00001 diff --git a/web/public/empty-state/search/issue-dark.webp b/web/public/empty-state/search/issue-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..dcd00aa94791922af3c32d11ea11ec0c0972dd6d GIT binary patch literal 3118 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 Wv_hkz0E-}dEABtg040iA0002AMP*n3 literal 0 HcmV?d00001 diff --git a/web/public/empty-state/search/notification-dark.webp b/web/public/empty-state/search/notification-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..cb299d112154affd00e5a837b778d6f86fa132f6 GIT binary patch literal 2286 zcmV#yJa}g)t7sQxErVAuSJAF= z&EyU$O}pF*IfCv@JH8ci0p6Q-f6HV%yfwYZmJOFC(F^5DHcL$}w`FtTUG$PUlcn%3 zdf8IRN>ugYD_MxD8nBXesHzbwSqAT-cC2I-yo;KW?>4!ynv=6VhNdQMWDA_stamg- zVl^yhE8wcetz?2fQS)-XK3vtrl`bxc8kzHz;iQJ1u8UYr&2&+JqQ;hTML4O!C%hcm z)Z|PTlSGZq=~DjG@b?Ru)b!I;U{m8ayaZx3Khp&es{@(3H;B4WhR*$pI#F(2`&CE& zcI;POS-N#sXWlz?S9jjK^rsH}?a;5f^tU^|>eS!P4B{0Tx%^q)ow{&W2bT^cb#Xc!v5$mKhOAEB@LZF+c{;={q$J>Q_hu z5x-F*Vf+F~Bna-8H5AAFI2L5LtihP}YSGYkA`Qp1PmPCpAC3pXJ+cPmZI2caZ3ohj z#Lsd}jGt{W(LN##%AZe4RE(c&QPDmi4a=YRN?erpT3ocZNCOk!$&r!YsgY6N;mD9T z1`W;ORf&!AYK@J5uOv3)#h}6Y^DIY4epaKSKHH*$pP+^(KBV|a4<$azLyeF6(Bflz zkoXV)7&JhC08)h907`@ufH^{DfEFSD0JaEW0F6)q3=#bqVq{1$a?3H2#~2ys7@3zC zsVzqS1xedtWaAj&D3KTu{qItV5#bo&DA{6UYcbN+AX#Fh&M`8NF*4>D$+s9uLyQa* zBM1^A030D3z#1V9V2+R(phQRkxJ5_;pa>ZNK;lCH0JivO4@-R1hY}y4Hg|Cx@0DTX}2&iyC6#p?D0Y?9ga7bkDF&y;fy?hjCzH4yU!FO~Vm_0Yb zfsy^jcqB-E%W&+~YZVTT;xneBLGf9JDCg?x0{g~hiv01MnT;y!K8Lo4*T!AZ1Y&A)6t;9Cl2(A`qi_HiZT*O*Z;F^hT z#RQiAe9dWeFj9^M?HfaNxGqXMT z1|}WZ7BV6jl@>$Dh+tS+tWXJzON#|6fq`kcx=LVVTI9N9L@+dEz9tzFtexBSv;-!n zg|9^B1a{BuD%uUf_LR5&wGqLCRJNYAk-(2sTAx}W@FwMj`yw?XICUti^g-4#fOChu zZ0F*d37m|nEKQClTP+E}-9svK+YTYT&gDkndmK_8=i1uFEU$!lYipgyd>aT12v$%y zAPfTl05CEDodGH)0VV-Hkw~9QC8VPvqO{q#uo4MrZsBQ;J^mq#1+z;N{wdf2!~tRd zl?V9K+UBP37-L&Vm~}wEK=y7>sqENB7IgLk&*iH}>C&J_3W91d_$-WJ`E>#|fCY7H zlD@shR7^iEpe|xDUM*~Y56eA`+Usyy)H_SAr@&ohP#5g?Ob>2t-47PX`B$3&%isJM5=)^@5cZS2hc76zV7M5yRZ7MhlLGHZ?U`%1n;(Il;vL@}Pt8g;} zm{DyDgXPdbjcXKkVpA+x1Tg9ZO2{Bcn0s>}u48U1I;gYk-zK#}Nn9T;f&^rHZooVT zNc53FwTuT^W;4kjbhIl4pT%&AIwRbxo$n}@m*m$j%@<6+ubp8blcB$t0`ZS(?U2uWAhMbFF0b$)vDq{Q7(j=EE8VBT5QfO`xhHpm<6pJ@7Q|3Kwr9 zsz&^G%YuVZISyv2vj6@7YHQ8%;VTQs-m(6Qvm%&D^NOjH=0*Z=dAJH{5cR)xp#LjQ z8eG=D&5{y+#TXVeqz?sHH3(aFf`VLf)8es28E34ExzqzW3h_0@%&joTKV_tkMh*Hg zjauh(!W!LN02T!x{K6}+mAtS90MB`VIV6?xk=JWhEsv5h+7e@hbrjV0wipEM#M|Dr z?p~XU!{G8^?e}}xRC*a>j~4IYxMF)_kGILGM@xqIm3^zT7x00f`^#qvAJ{z=6&1$W zy($K7g{SfkQ{WqytjBBIK&`!LL10z5keh7*S8iWjx*5(v|I|^+xC=5msHPbdpa3G$ z(pCcif(I24O6W_2{LG^sX_rU))mj7M->hG#AQlM IT8IDu04{Sawg3PC literal 0 HcmV?d00001 diff --git a/web/public/empty-state/search/notification-light.webp b/web/public/empty-state/search/notification-light.webp new file mode 100644 index 0000000000000000000000000000000000000000..55c4ffac753590e113f06ad782d485005cb7da48 GIT binary patch literal 2524 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 literal 0 HcmV?d00001 diff --git a/web/public/empty-state/search/search-dark.webp b/web/public/empty-state/search/search-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..92abc616ba7dce14f5746be6b05347ce2c9c0bdf GIT binary patch literal 2330 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 literal 0 HcmV?d00001 diff --git a/web/public/empty-state/search/snooze-dark.webp b/web/public/empty-state/search/snooze-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..296e797e82a3ad629d19c27b6f1006d541c632cc GIT binary patch literal 2464 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 literal 0 HcmV?d00001 diff --git a/web/public/empty-state/search/snooze-light.webp b/web/public/empty-state/search/snooze-light.webp new file mode 100644 index 0000000000000000000000000000000000000000..897ff359887b7abb443bdbbee4dad601aaece2e1 GIT binary patch literal 2790 zcmV#yJa}g)t7sQxErVAuSJAF= z&EyU$O}pF*IfCv@JH8ci0p6Q-f6HV%yfwYZmJOFC(F^5DHcL$}w`FtTUG$PUlcn%3 zdf8IRN>ugYD_MxD8nBXesHzbwSqAT-cC2I-yo;KW?>4!ynv=6VhNdQMWDA_stamg- zVl^yhE8wcetz?2fQS)-XK3vtrl`bxc8kzHz;iQJ1u8UYr&2&+JqQ;hTML4O!C%hcm z)Z|PTlSGZq=~DjG@b?Ru)b!I;U{m8ayaZx3Khp&es{@(3H;B4WhR*$pI#F(2`&CE& zcI;POS-N#sXWlz?S9jjK^rsH}?a;5f^tU^|>eS!P4B{0Tx%^q)ow{&W2bT^cb#Xc!v5$mKhOAEB@LZF+c{;={q$J>Q_hu z5x-F*Vf+F~Bna-8H5AAFI2L5LtihP}YSGYkA`Qp1PmPCpAC3pXJ+cPmZI2caZ3ohj z#Lsd}jGt{W(LN##%AZe4RE(c&QPDmi4a=YRN?erpT3ocZNCOk!$&r!YsgY6N;mD9T z1`W;ORf&!AYK@J5uOv3)#h}6Y^DIY4epaKSKHH*$pP+^(KBV|a4<$azLyeF6(Bflz zkoXV)7&JhC08)h907`@ufH^{DfEFSD0JaEW0F6)q3=#bqVq{1$a?3H2#~2ys7@3zC zsVzqS1xedtWaAj&D3KTu{qItV5#bo&DA{6UYcbN+AX#Fh&M`8NF*4>D$+s9uLyQa* zBM1^A030D3z#1V9V2+R(phQRkxJ5_;pa>ZNK;lCH0JivO4@-R1hY}y4Hg|Cx@0DTX}2&iyC6#p?D0Y?9ga7bkDF&y;fy?hjCzH4yU!FO~Vm_0Yb zfsy^jcqB-E%W&+~YZVTT;xneBLGf9JDCg?x0{g~hiv01MnT;y!K8Lo4*T!AZ1Y&A)6t;9Cl2(A`qi_HiZT*O*Z;F^hT z#RQiAe9dWeFj9^M?HfaNxGqXMT z1|}WZ7BV6jl@>$Dh+tS+tWXJzON#|6fq`kcx=LVVTI9N9L@+dEz9tzFtexBSv;-!n zg|9^B1a{BuD%uUf_LR5&wGqLCRJNYAk-(2sTAx}W@FwMj`yw?XICUti^g-4#fOChu zZ0F*d37m|nEKQClTP+E}-9svK+YTYT&gDkndmK_8=i1uFEU$!lYipgyd>aT12v$%y zAOr;f05CxSodGH)0VV-Hkw%?LC8Q#usk4Znuo4MrZsBU~Wd1RG@!&rIKR`7AKL9^K zAOJrAF8s72^jtsEKem6S`T+X5gKHJb1=|d>LKw{ptj8N#cwT*OP4zEr-w7dh9Kai^O5ai#+~>BJsHLKG ze6~Vf0iqBN^*O|d>!ApOO7zpwLRi#{q9k;{Psin5EaHDT2Tc3pf^VjBeXWW&mu-F7 zK+~KA<%!9z6xh7smNQA6ecDj$XN*tyxy_&>`=OmN%;0*1x!tADX8`%4Oe6`Pk@4&Y z#UU;zUNM%^eDg`1ZN!>liw9Q%3dd;9+#~EzZzfbcd6`X7)>KIm-nD}P1d0Cj`u8OB zx4AGV4s)_4som(-Lq)xH``E@4-kbBQ7fpNfBvn?9Fk^|RgU_*D~0ngV4Rq#%Z>PkPYqLVrFf z(i6<7hBIn&c8YO2czMn3tO0;~;SAA5>l!rQB!4B)o@O3lJlXN!Yp=G4ywNuyg%)o! zYV$%q)LWl6X8r_b%ZLB~{&#BQbszuyb};`w)9McIgz?sMT09BF=)_O3?v1lHGkRGa z;2(DbDtx}bU#}q`fDnDtJLapv3;fsa58Hi9Stzt%5{No4!V{n*+wbL1uF=wJMbN@a zZAj^a!bC$^-L9^=@6639q_7icJ9KwM9%`E}nr?c4d5qy#!iq# z!+2fWO8Gu)S|U}zamd2aXuMr{rasbq?j;KSh+zIoTtRg| z)7+S$7?A(GLBeotBnW9((`F)R!Eihmvn!Ces`pJV@Rtbi+y@g2cf}GTdQtQk!4hTZ zvI3b(Rhw7Z4V6?h=axKPw62>1zh*Oy#*-BE_CGVi5p=B`8|KwyJS3|gO}p~#Fa`AB zE?HbL)t&^5cKrsGXU=S`k+WHNm@>Zw9ASMY|KmfC&%6vbawgB#RKx%P0KJb%IsgCw literal 0 HcmV?d00001 From 13bbb9cde4bc26ebdee5aec5cadd0d4e6f4aede5 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 15 Mar 2024 17:29:22 +0530 Subject: [PATCH 13/22] [WEB-699] chore: implement sub-issues and attachments in the peek overview (#3956) * chore: implement sub-issues and attachments in the peek overview * chore: add the same to full-screen view --- .../issues/attachment/attachments-list.tsx | 20 +- .../delete-attachment-confirmation-modal.tsx | 4 +- web/components/issues/attachment/root.tsx | 2 +- .../issues/peek-overview/header.tsx | 12 +- web/components/issues/peek-overview/index.ts | 3 +- .../peek-overview/issue-attachments.tsx | 111 ++++++++++ .../issues/peek-overview/issue-detail.tsx | 4 +- .../issues/peek-overview/properties.tsx | 2 +- web/components/issues/peek-overview/view.tsx | 45 +++- .../issues/sub-issues/issue-list-item.tsx | 14 +- web/components/issues/sub-issues/root.tsx | 192 +++++++++++------- web/store/issue/issue-details/root.store.ts | 16 +- 12 files changed, 312 insertions(+), 113 deletions(-) create mode 100644 web/components/issues/peek-overview/issue-attachments.tsx diff --git a/web/components/issues/attachment/attachments-list.tsx b/web/components/issues/attachment/attachments-list.tsx index 0f834c1a4..aed1f7922 100644 --- a/web/components/issues/attachment/attachments-list.tsx +++ b/web/components/issues/attachment/attachments-list.tsx @@ -21,23 +21,21 @@ export const IssueAttachmentsList: FC = observer((props) const { attachment: { getAttachmentsByIssueId }, } = useIssueDetail(); - + // derived values const issueAttachments = getAttachmentsByIssueId(issueId); if (!issueAttachments) return <>; return ( <> - {issueAttachments && - issueAttachments.length > 0 && - issueAttachments.map((attachmentId) => ( - - ))} + {issueAttachments?.map((attachmentId) => ( + + ))} ); }); diff --git a/web/components/issues/attachment/delete-attachment-confirmation-modal.tsx b/web/components/issues/attachment/delete-attachment-confirmation-modal.tsx index 096f9e778..85f0ea0e9 100644 --- a/web/components/issues/attachment/delete-attachment-confirmation-modal.tsx +++ b/web/components/issues/attachment/delete-attachment-confirmation-modal.tsx @@ -69,7 +69,7 @@ export const IssueAttachmentDeleteModal: FC = (props) => {
- Delete Attachment + Delete attachment

@@ -94,7 +94,7 @@ export const IssueAttachmentDeleteModal: FC = (props) => { }} disabled={loader} > - {loader ? "Deleting..." : "Delete"} + {loader ? "Deleting" : "Delete"}

diff --git a/web/components/issues/attachment/root.tsx b/web/components/issues/attachment/root.tsx index 3cf6b162a..715e9f840 100644 --- a/web/components/issues/attachment/root.tsx +++ b/web/components/issues/attachment/root.tsx @@ -101,7 +101,7 @@ export const IssueAttachmentRoot: FC = (props) => { return (

Attachments

-
+
= observer((pr handleRestoreIssue, isSubmitting, } = props; - // router - const router = useRouter(); // store hooks const { currentUser } = useUser(); const { @@ -101,10 +99,6 @@ export const IssuePeekOverviewHeader: FC = observer((pr }); }); }; - const redirectToIssueDetail = () => { - router.push({ pathname: `/${issueLink}` }); - removeRoutePeekId(); - }; // auth const isArchivingAllowed = !isArchived && !disabled; const isInArchivableGroup = @@ -122,9 +116,9 @@ export const IssuePeekOverviewHeader: FC = observer((pr - + {currentMode && (
= (props) => { + const { disabled, issueId, projectId, workspaceSlug } = props; + // store hooks + const { captureIssueEvent } = useEventTracker(); + const { + attachment: { createAttachment, removeAttachment }, + } = useIssueDetail(); + + const handleAttachmentOperations: TAttachmentOperations = useMemo( + () => ({ + create: async (data: FormData) => { + try { + const attachmentUploadPromise = createAttachment(workspaceSlug, projectId, issueId, data); + setPromiseToast(attachmentUploadPromise, { + loading: "Uploading attachment...", + success: { + title: "Attachment uploaded", + message: () => "The attachment has been successfully uploaded", + }, + error: { + title: "Attachment not uploaded", + message: () => "The attachment could not be uploaded", + }, + }); + + const res = await attachmentUploadPromise; + captureIssueEvent({ + eventName: "Issue attachment added", + payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, + updates: { + changed_property: "attachment", + change_details: res.id, + }, + }); + } catch (error) { + captureIssueEvent({ + eventName: "Issue attachment added", + payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, + }); + } + }, + remove: async (attachmentId: string) => { + try { + if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields"); + await removeAttachment(workspaceSlug, projectId, issueId, attachmentId); + setToast({ + message: "The attachment has been successfully removed", + type: TOAST_TYPE.SUCCESS, + title: "Attachment removed", + }); + captureIssueEvent({ + eventName: "Issue attachment deleted", + payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, + updates: { + changed_property: "attachment", + change_details: "", + }, + }); + } catch (error) { + captureIssueEvent({ + eventName: "Issue attachment deleted", + payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, + updates: { + changed_property: "attachment", + change_details: "", + }, + }); + setToast({ + message: "The Attachment could not be removed", + type: TOAST_TYPE.ERROR, + title: "Attachment not removed", + }); + } + }, + }), + [workspaceSlug, projectId, issueId, captureIssueEvent, createAttachment, removeAttachment] + ); + + return ( +
+
Attachments
+
+ + +
+
+ ); +}; diff --git a/web/components/issues/peek-overview/issue-detail.tsx b/web/components/issues/peek-overview/issue-detail.tsx index 59b1c1609..2abcec2ff 100644 --- a/web/components/issues/peek-overview/issue-detail.tsx +++ b/web/components/issues/peek-overview/issue-detail.tsx @@ -55,7 +55,7 @@ export const PeekOverviewIssueDetails: FC = observer( : undefined; return ( - <> +
{projectDetails?.identifier}-{issue?.sequence_id} @@ -89,6 +89,6 @@ export const PeekOverviewIssueDetails: FC = observer( currentUser={currentUser} /> )} - +
); }); diff --git a/web/components/issues/peek-overview/properties.tsx b/web/components/issues/peek-overview/properties.tsx index 8ae021b86..ba00755f4 100644 --- a/web/components/issues/peek-overview/properties.tsx +++ b/web/components/issues/peek-overview/properties.tsx @@ -61,7 +61,7 @@ export const PeekOverviewProperties: FC = observer((pro maxDate?.setDate(maxDate.getDate()); return ( -
+
Properties
{/* TODO: render properties using a common component */}
diff --git a/web/components/issues/peek-overview/view.tsx b/web/components/issues/peek-overview/view.tsx index 47890c95c..4109f3feb 100644 --- a/web/components/issues/peek-overview/view.tsx +++ b/web/components/issues/peek-overview/view.tsx @@ -11,13 +11,15 @@ import { PeekOverviewProperties, TIssueOperations, ArchiveIssueModal, + PeekOverviewIssueAttachments, } from "components/issues"; // hooks -import { useIssueDetail } from "hooks/store"; +import { useIssueDetail, useUser } from "hooks/store"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // store hooks import { IssueActivity } from "../issue-detail/issue-activity"; +import { SubIssuesRoot } from "../sub-issues"; interface IIssueView { workspaceSlug: string; @@ -37,6 +39,7 @@ export const IssueView: FC = observer((props) => { // ref const issuePeekOverviewRef = useRef(null); // store hooks + const { currentUser } = useUser(); const { setPeekIssue, isAnyModalOpen, @@ -147,7 +150,7 @@ export const IssueView: FC = observer((props) => { issue && ( <> {["side-peek", "modal"].includes(peekMode) ? ( -
+
= observer((props) => { setIsSubmitting={(value) => setIsSubmitting(value)} /> + {currentUser && ( + + )} + + + = observer((props) => {
) : ( -
+
-
+
= observer((props) => { setIsSubmitting={(value) => setIsSubmitting(value)} /> + {currentUser && ( + + )} + + +
diff --git a/web/components/issues/sub-issues/issue-list-item.tsx b/web/components/issues/sub-issues/issue-list-item.tsx index 170bf622f..781a91a3d 100644 --- a/web/components/issues/sub-issues/issue-list-item.tsx +++ b/web/components/issues/sub-issues/issue-list-item.tsx @@ -1,6 +1,6 @@ import React from "react"; import { observer } from "mobx-react-lite"; -import { ChevronDown, ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react"; +import { ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react"; // components import { ControlLink, CustomMenu, Tooltip } from "@plane/ui"; import { useIssueDetail, useProject, useProjectState } from "hooks/store"; @@ -11,6 +11,7 @@ import { IssueProperty } from "./properties"; // ui // types import { TSubIssueOperations } from "./root"; +import { cn } from "helpers/common.helper"; // import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root"; export interface ISubIssues { @@ -90,11 +91,12 @@ export const IssueListItem: React.FC = observer((props) => { setSubIssueHelpers(parentIssueId, "issue_visibility", issueId); }} > - {subIssueHelpers.issue_visibility.includes(issue.id) ? ( - - ) : ( - - )} +
)} diff --git a/web/components/issues/sub-issues/root.tsx b/web/components/issues/sub-issues/root.tsx index ed46a40f5..c3655286e 100644 --- a/web/components/issues/sub-issues/root.tsx +++ b/web/components/issues/sub-issues/root.tsx @@ -1,9 +1,9 @@ import { FC, useCallback, useEffect, useMemo, useState } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { Plus, ChevronRight, ChevronDown, Loader } from "lucide-react"; +import { Plus, ChevronRight, Loader, Pencil } from "lucide-react"; // hooks -import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; +import { CircularProgressIndicator, CustomMenu, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui"; import { ExistingIssuesListModal } from "components/core"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; import { copyTextToClipboard } from "helpers/string.helper"; @@ -11,7 +11,7 @@ import { useEventTracker, useIssueDetail } from "hooks/store"; // components import { IUser, TIssue } from "@plane/types"; import { IssueList } from "./issues-list"; -import { ProgressBar } from "./progressbar"; +import { cn } from "helpers/common.helper"; // ui // helpers // types @@ -53,6 +53,10 @@ export const SubIssuesRoot: FC = observer((props) => { updateSubIssue, removeSubIssue, deleteSubIssue, + isCreateIssueModalOpen, + toggleCreateIssueModal, + isSubIssuesModalOpen, + toggleSubIssuesModal, } = useIssueDetail(); const { setTrackElement, captureIssueEvent } = useEventTracker(); // state @@ -310,55 +314,81 @@ export const SubIssuesRoot: FC = observer((props) => { <> {subIssues && subIssues?.length > 0 ? ( <> -
-
-
- {subIssueHelpers.preview_loader.includes(parentIssueId) ? ( - - ) : subIssueHelpers.issue_visibility.includes(parentIssueId) ? ( - - ) : ( - - )} +
+
+ +
+ + + {subIssuesDistribution?.completed?.length ?? 0}/{subIssues.length} Done +
-
Sub-issues
-
({subIssues?.length || 0})
-
- -
-
{!disabled && ( -
-
+ + Add sub-issue + + } + buttonClassName="whitespace-nowrap" + placement="bottom-end" + noBorder + noChevron + > + { - setTrackElement("Issue detail add sub-issue"); + setTrackElement("Issue detail nested sub-issue"); handleIssueCrudState("create", parentIssueId, null); + toggleCreateIssueModal(true); }} > - Add sub-issue -
-
+ + Create new +
+ + { - setTrackElement("Issue detail add sub-issue"); + setTrackElement("Issue detail nested sub-issue"); handleIssueCrudState("existing", parentIssueId, null); + toggleSubIssuesModal(true); }} > - Add an existing issue -
-
+
+ + Add existing +
+ + )}
@@ -379,62 +409,74 @@ export const SubIssuesRoot: FC = observer((props) => { ) : ( !disabled && (
-
No Sub-Issues yet
-
- - - Add sub-issue - - } - buttonClassName="whitespace-nowrap" - placement="bottom-end" - noBorder - noChevron +
No sub-issues yet
+ + + Add sub-issue + + } + buttonClassName="whitespace-nowrap" + placement="bottom-end" + noBorder + noChevron + > + { + setTrackElement("Issue detail nested sub-issue"); + handleIssueCrudState("create", parentIssueId, null); + toggleCreateIssueModal(true); + }} > - { - setTrackElement("Issue detail nested sub-issue"); - handleIssueCrudState("create", parentIssueId, null); - }} - > - Create new - - { - setTrackElement("Issue detail nested sub-issue"); - handleIssueCrudState("existing", parentIssueId, null); - }} - > - Add an existing issue - - -
+
+ + Create new +
+ + { + setTrackElement("Issue detail nested sub-issue"); + handleIssueCrudState("existing", parentIssueId, null); + toggleSubIssuesModal(true); + }} + > +
+ + Add existing +
+
+
) )} {/* issue create, add from existing , update and delete modals */} - {issueCrudState?.create?.toggle && issueCrudState?.create?.parentIssueId && ( + {issueCrudState?.create?.toggle && issueCrudState?.create?.parentIssueId && isCreateIssueModalOpen && ( handleIssueCrudState("create", null, null)} + onClose={() => { + handleIssueCrudState("create", null, null); + toggleCreateIssueModal(false); + }} onSubmit={async (_issue: TIssue) => { await subIssueOperations.addSubIssue(workspaceSlug, projectId, parentIssueId, [_issue.id]); }} /> )} - {issueCrudState?.existing?.toggle && issueCrudState?.existing?.parentIssueId && ( + {issueCrudState?.existing?.toggle && issueCrudState?.existing?.parentIssueId && isSubIssuesModalOpen && ( handleIssueCrudState("existing", null, null)} + handleClose={() => { + handleIssueCrudState("existing", null, null); + toggleSubIssuesModal(false); + }} searchParams={{ sub_issue: true, issue_id: issueCrudState?.existing?.parentIssueId }} handleOnSubmit={(_issue) => subIssueOperations.addSubIssue( diff --git a/web/store/issue/issue-details/root.store.ts b/web/store/issue/issue-details/root.store.ts index be77efcd1..c58e1af42 100644 --- a/web/store/issue/issue-details/root.store.ts +++ b/web/store/issue/issue-details/root.store.ts @@ -44,20 +44,24 @@ export interface IIssueDetail IIssueCommentReactionStoreActions { // observables peekIssue: TPeekIssue | undefined; + isCreateIssueModalOpen: boolean; isIssueLinkModalOpen: boolean; isParentIssueModalOpen: boolean; isDeleteIssueModalOpen: boolean; isArchiveIssueModalOpen: boolean; isRelationModalOpen: TIssueRelationTypes | null; + isSubIssuesModalOpen: boolean; // computed isAnyModalOpen: boolean; // actions setPeekIssue: (peekIssue: TPeekIssue | undefined) => void; + toggleCreateIssueModal: (value: boolean) => void; toggleIssueLinkModal: (value: boolean) => void; toggleParentIssueModal: (value: boolean) => void; toggleDeleteIssueModal: (value: boolean) => void; toggleArchiveIssueModal: (value: boolean) => void; toggleRelationModal: (value: TIssueRelationTypes | null) => void; + toggleSubIssuesModal: (value: boolean) => void; // store rootIssueStore: IIssueRootStore; issue: IIssueStore; @@ -75,11 +79,13 @@ export interface IIssueDetail export class IssueDetail implements IIssueDetail { // observables peekIssue: TPeekIssue | undefined = undefined; + isCreateIssueModalOpen: boolean = false; isIssueLinkModalOpen: boolean = false; isParentIssueModalOpen: boolean = false; isDeleteIssueModalOpen: boolean = false; isArchiveIssueModalOpen: boolean = false; isRelationModalOpen: TIssueRelationTypes | null = null; + isSubIssuesModalOpen: boolean = false; // store rootIssueStore: IIssueRootStore; issue: IIssueStore; @@ -97,20 +103,24 @@ export class IssueDetail implements IIssueDetail { makeObservable(this, { // observables peekIssue: observable, + isCreateIssueModalOpen: observable, isIssueLinkModalOpen: observable.ref, isParentIssueModalOpen: observable.ref, isDeleteIssueModalOpen: observable.ref, isArchiveIssueModalOpen: observable.ref, isRelationModalOpen: observable.ref, + isSubIssuesModalOpen: observable.ref, // computed isAnyModalOpen: computed, // action setPeekIssue: action, + toggleCreateIssueModal: action, toggleIssueLinkModal: action, toggleParentIssueModal: action, toggleDeleteIssueModal: action, toggleArchiveIssueModal: action, toggleRelationModal: action, + toggleSubIssuesModal: action, }); // store @@ -130,21 +140,25 @@ export class IssueDetail implements IIssueDetail { // computed get isAnyModalOpen() { return ( + this.isCreateIssueModalOpen || this.isIssueLinkModalOpen || this.isParentIssueModalOpen || this.isDeleteIssueModalOpen || this.isArchiveIssueModalOpen || - Boolean(this.isRelationModalOpen) + Boolean(this.isRelationModalOpen) || + this.isSubIssuesModalOpen ); } // actions setPeekIssue = (peekIssue: TPeekIssue | undefined) => (this.peekIssue = peekIssue); + toggleCreateIssueModal = (value: boolean) => (this.isCreateIssueModalOpen = value); toggleIssueLinkModal = (value: boolean) => (this.isIssueLinkModalOpen = value); toggleParentIssueModal = (value: boolean) => (this.isParentIssueModalOpen = value); toggleDeleteIssueModal = (value: boolean) => (this.isDeleteIssueModalOpen = value); toggleArchiveIssueModal = (value: boolean) => (this.isArchiveIssueModalOpen = value); toggleRelationModal = (value: TIssueRelationTypes | null) => (this.isRelationModalOpen = value); + toggleSubIssuesModal = (value: boolean) => (this.isSubIssuesModalOpen = value); // issue fetchIssue = async ( From 94f1e6d957d8ad206d83f5ac1d96a2080d723792 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 15 Mar 2024 17:29:46 +0530 Subject: [PATCH 14/22] [WEB-698] chore: profile activity page scrollbar added (#3958) * chore: profile activity page scrollbar added * chore: profile activity page padding improvement --- web/pages/[workspaceSlug]/profile/[userId]/activity.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/pages/[workspaceSlug]/profile/[userId]/activity.tsx b/web/pages/[workspaceSlug]/profile/[userId]/activity.tsx index 87029724e..dbef69895 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/activity.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/activity.tsx @@ -54,12 +54,12 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => { currentUser?.id === userId && !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; return ( -
-
+
+

Recent activity

{canDownloadActivity && }
-
+
{activityPages} {pageCount < totalPages && resultsCount !== 0 && (
From c17748eec28c45f1c72deaeaa0be23892fe4a2a4 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 15 Mar 2024 17:30:26 +0530 Subject: [PATCH 15/22] chore: completed cycle empty state valdidation updated (#3959) --- .../issues/issue-layouts/empty-states/cycle.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/web/components/issues/issue-layouts/empty-states/cycle.tsx b/web/components/issues/issue-layouts/empty-states/cycle.tsx index 1a49794c6..8def7cc85 100644 --- a/web/components/issues/issue-layouts/empty-states/cycle.tsx +++ b/web/components/issues/issue-layouts/empty-states/cycle.tsx @@ -62,12 +62,14 @@ export const CycleEmptyState: React.FC = observer((props) => { const isCompletedCycleSnapshotAvailable = !isEmpty(cycleDetails?.progress_snapshot ?? {}); - const emptyStateType = isCompletedCycleSnapshotAvailable + const isCompletedAndEmpty = isCompletedCycleSnapshotAvailable || cycleDetails?.status.toLowerCase() === "completed"; + + const emptyStateType = isCompletedAndEmpty ? EmptyStateType.PROJECT_CYCLE_COMPLETED_NO_ISSUES : isEmptyFilters ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_CYCLE_NO_ISSUES; - const additionalPath = isCompletedCycleSnapshotAvailable ? undefined : activeLayout ?? "list"; + const additionalPath = isCompletedAndEmpty ? undefined : activeLayout ?? "list"; const emptyStateSize = isEmptyFilters ? "lg" : "sm"; return ( @@ -86,7 +88,7 @@ export const CycleEmptyState: React.FC = observer((props) => { additionalPath={additionalPath} size={emptyStateSize} primaryButtonOnClick={ - !isCompletedCycleSnapshotAvailable && !isEmptyFilters + !isCompletedAndEmpty && !isEmptyFilters ? () => { setTrackElement("Cycle issue empty state"); toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); @@ -94,9 +96,7 @@ export const CycleEmptyState: React.FC = observer((props) => { : undefined } secondaryButtonOnClick={ - !isCompletedCycleSnapshotAvailable && isEmptyFilters - ? handleClearAllFilters - : () => setCycleIssuesListModal(true) + !isCompletedAndEmpty && isEmptyFilters ? handleClearAllFilters : () => setCycleIssuesListModal(true) } />
From 07892382823bd4f29079cbbc86ee04af029c04f1 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 15 Mar 2024 17:31:28 +0530 Subject: [PATCH 16/22] fix: cycle layout not getting initialized (#3961) --- web/components/cycles/board/root.tsx | 14 ++++++++------ web/components/cycles/cycles-view-header.tsx | 10 ++++------ .../projects/[projectId]/cycles/index.tsx | 16 +++++++--------- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/web/components/cycles/board/root.tsx b/web/components/cycles/board/root.tsx index 26154becf..e9fde3428 100644 --- a/web/components/cycles/board/root.tsx +++ b/web/components/cycles/board/root.tsx @@ -22,12 +22,14 @@ export const CyclesBoard: FC = observer((props) => {
- + {cycleIds.length > 0 && ( + + )} {completedCycleIds.length !== 0 && ( diff --git a/web/components/cycles/cycles-view-header.tsx b/web/components/cycles/cycles-view-header.tsx index f7ff3567c..737d36f3f 100644 --- a/web/components/cycles/cycles-view-header.tsx +++ b/web/components/cycles/cycles-view-header.tsx @@ -42,6 +42,8 @@ export const CyclesViewHeader: React.FC = observer((props) => { useOutsideClickDetector(inputRef, () => { if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false); }); + // derived values + const activeLayout = currentProjectDisplayFilters?.layout ?? "list"; const handleFilters = useCallback( (key: keyof TCycleFilters, value: string | string[]) => { @@ -140,9 +142,7 @@ export const CyclesViewHeader: React.FC = observer((props) => { diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index fa9008d2f..651d03898 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -47,7 +47,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { const pageTitle = project?.name ? `${project?.name} - Cycles` : undefined; // selected display filters const cycleTab = currentProjectDisplayFilters?.active_tab; - const cycleLayout = currentProjectDisplayFilters?.layout; + const cycleLayout = currentProjectDisplayFilters?.layout ?? "list"; const handleRemoveFilter = (key: keyof TCycleFilters, value: string | null) => { if (!projectId) return; @@ -120,14 +120,12 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { - {cycleTab && cycleLayout && ( - - )} + From 36bc7bf996fb2eced3ab1482646baf059a4a0511 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 15 Mar 2024 17:36:20 +0530 Subject: [PATCH 17/22] chore: app sidebar collapsed state alignment improvement (#3976) --- web/components/workspace/sidebar-menu.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/web/components/workspace/sidebar-menu.tsx b/web/components/workspace/sidebar-menu.tsx index 69860dea2..bc955a7c2 100644 --- a/web/components/workspace/sidebar-menu.tsx +++ b/web/components/workspace/sidebar-menu.tsx @@ -55,10 +55,11 @@ export const WorkspaceSidebarMenu = observer(() => { isMobile={isMobile} >
{ { })} /> } -

{!themeStore?.sidebarCollapsed && link.label}

+ {!themeStore?.sidebarCollapsed &&

{link.label}

} {!themeStore?.sidebarCollapsed && link.key === "active-cycles" && ( )} From 5244ba72c908136525ce069a76e7b9c0a4199c8e Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 15 Mar 2024 17:43:42 +0530 Subject: [PATCH 18/22] [WEB-746] chore: draft issue modal improvement (#3966) * chore: draft issue modal improvement * chore: draft issue modal improvement * chore: draft issue modal improvement * chore: draft issue modal improvement --- web/components/issues/issue-modal/draft-issue-layout.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/components/issues/issue-modal/draft-issue-layout.tsx b/web/components/issues/issue-modal/draft-issue-layout.tsx index 785ccb0bb..71bf1fae0 100644 --- a/web/components/issues/issue-modal/draft-issue-layout.tsx +++ b/web/components/issues/issue-modal/draft-issue-layout.tsx @@ -55,7 +55,10 @@ export const DraftIssueLayout: React.FC = observer((props) => { const handleCreateDraftIssue = async () => { if (!changesMade || !workspaceSlug || !projectId) return; - const payload = { ...changesMade }; + const payload = { + ...changesMade, + name: changesMade.name?.trim() === "" ? "Untitled" : changesMade.name?.trim(), + }; await issueDraftService .createDraftIssue(workspaceSlug.toString(), projectId.toString(), payload) From 861a1c4132cf5fd8d9f17338d71464c73a38d8c2 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 15 Mar 2024 17:44:01 +0530 Subject: [PATCH 19/22] [WEB-738] chore: conditionally render projects in the dropdown (#3952) * chore: show authorized projects in the dropdown * refactor: command palette logic * chore: add helper function to check for project role * fix: add preventDefault for shortcuts --- .../command-palette/command-palette.tsx | 182 +++++++++++++----- web/components/cycles/form.tsx | 2 + web/components/dropdowns/project.tsx | 46 +++-- web/components/issues/issue-modal/form.tsx | 3 +- web/components/modules/form.tsx | 2 + .../project/create-project-modal.tsx | 35 +--- web/helpers/project.helper.ts | 10 + 7 files changed, 179 insertions(+), 101 deletions(-) diff --git a/web/components/command-palette/command-palette.tsx b/web/components/command-palette/command-palette.tsx index ab2743afd..0d02614ae 100644 --- a/web/components/command-palette/command-palette.tsx +++ b/web/components/command-palette/command-palette.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, FC } from "react"; +import React, { useCallback, useEffect, FC, useMemo } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; @@ -23,24 +23,29 @@ import { EIssuesStoreType } from "constants/issue"; import { copyTextToClipboard } from "helpers/string.helper"; import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; import { IssueService } from "services/issue"; +import { EUserProjectRoles } from "constants/project"; +import { EUserWorkspaceRoles } from "constants/workspace"; // services const issueService = new IssueService(); export const CommandPalette: FC = observer(() => { + // router const router = useRouter(); const { workspaceSlug, projectId, issueId, cycleId, moduleId } = router.query; - + // store hooks const { commandPalette, theme: { toggleSidebar }, } = useApplication(); const { setTrackElement } = useEventTracker(); - const { currentUser } = useUser(); + const { + currentUser, + membership: { currentWorkspaceRole, currentProjectRole }, + } = useUser(); const { issues: { removeIssue }, } = useIssues(EIssuesStoreType.PROJECT); - const { toggleCommandPaletteModal, isCreateIssueModalOpen, @@ -91,6 +96,105 @@ export const CommandPalette: FC = observer(() => { }); }, [issueId]); + // auth + const canPerformProjectCreateActions = useCallback( + (showToast: boolean = true) => { + const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + if (!isAllowed && showToast) + setToast({ + type: TOAST_TYPE.ERROR, + title: "You don't have permission to perform this action.", + }); + + return isAllowed; + }, + [currentProjectRole] + ); + const canPerformWorkspaceCreateActions = useCallback( + (showToast: boolean = true) => { + const isAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; + console.log("currentWorkspaceRole", currentWorkspaceRole); + console.log("isAllowed", isAllowed); + if (!isAllowed && showToast) + setToast({ + type: TOAST_TYPE.ERROR, + title: "You don't have permission to perform this action.", + }); + return isAllowed; + }, + [currentWorkspaceRole] + ); + + const shortcutsList: { + global: Record void }>; + workspace: Record void }>; + project: Record void }>; + } = useMemo( + () => ({ + global: { + c: { + title: "Create a new issue", + description: "Create a new issue in the current project", + action: () => toggleCreateIssueModal(true), + }, + h: { + title: "Show shortcuts", + description: "Show all the available shortcuts", + action: () => toggleShortcutModal(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(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), + }, + }, + }), + [ + toggleBulkDeleteIssueModal, + toggleCreateCycleModal, + toggleCreateIssueModal, + toggleCreateModuleModal, + toggleCreatePageModal, + toggleCreateProjectModal, + toggleCreateViewModal, + toggleShortcutModal, + ] + ); + const handleKeyDown = useCallback( (e: KeyboardEvent) => { const { key, ctrlKey, metaKey, altKey } = e; @@ -102,7 +206,7 @@ export const CommandPalette: FC = observer(() => { if ( e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLInputElement || - (e.target as Element).classList?.contains("ProseMirror") + (e.target as Element)?.classList?.contains("ProseMirror") ) return; @@ -119,42 +223,37 @@ export const CommandPalette: FC = observer(() => { } } else if (!isAnyModalOpen) { setTrackElement("Shortcut key"); - if (keyPressed === "c") { - toggleCreateIssueModal(true); - } else if (keyPressed === "p") { - toggleCreateProjectModal(true); - } else if (keyPressed === "h") { - toggleShortcutModal(true); - } else if (keyPressed === "v" && workspaceSlug && projectId) { - toggleCreateViewModal(true); - } else if (keyPressed === "d" && workspaceSlug && projectId) { - toggleCreatePageModal(true); - } else if (keyPressed === "q" && workspaceSlug && projectId) { - toggleCreateCycleModal(true); - } else if (keyPressed === "m" && workspaceSlug && projectId) { - toggleCreateModuleModal(true); - } else if (keyPressed === "backspace" || keyPressed === "delete") { + if (Object.keys(shortcutsList.global).includes(keyPressed)) shortcutsList.global[keyPressed].action(); + // workspace authorized actions + else if ( + Object.keys(shortcutsList.workspace).includes(keyPressed) && + workspaceSlug && + canPerformWorkspaceCreateActions() + ) + shortcutsList.workspace[keyPressed].action(); + // project authorized actions + else if ( + Object.keys(shortcutsList.project).includes(keyPressed) && + projectId && + canPerformProjectCreateActions() + ) { e.preventDefault(); - toggleBulkDeleteIssueModal(true); + // actions that can be performed only inside a project + shortcutsList.project[keyPressed].action(); } } }, [ + canPerformProjectCreateActions, + canPerformWorkspaceCreateActions, copyIssueUrlToClipboard, - toggleCreateProjectModal, - toggleCreateViewModal, - toggleCreatePageModal, - toggleShortcutModal, - toggleCreateCycleModal, - toggleCreateModuleModal, - toggleBulkDeleteIssueModal, + isAnyModalOpen, + projectId, + setTrackElement, + shortcutsList, toggleCommandPaletteModal, toggleSidebar, - toggleCreateIssueModal, - projectId, workspaceSlug, - isAnyModalOpen, - setTrackElement, ] ); @@ -169,18 +268,11 @@ export const CommandPalette: FC = observer(() => { return ( <> - { - toggleShortcutModal(false); - }} - /> + toggleShortcutModal(false)} /> {workspaceSlug && ( { - toggleCreateProjectModal(false); - }} + onClose={() => toggleCreateProjectModal(false)} workspaceSlug={workspaceSlug.toString()} /> )} @@ -194,9 +286,7 @@ export const CommandPalette: FC = observer(() => { /> { - toggleCreateModuleModal(false); - }} + onClose={() => toggleCreateModuleModal(false)} workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} /> @@ -236,9 +326,7 @@ export const CommandPalette: FC = observer(() => { { - toggleBulkDeleteIssueModal(false); - }} + onClose={() => toggleBulkDeleteIssueModal(false)} user={currentUser} /> diff --git a/web/components/cycles/form.tsx b/web/components/cycles/form.tsx index 4e2f55ef9..d470b1bb9 100644 --- a/web/components/cycles/form.tsx +++ b/web/components/cycles/form.tsx @@ -6,6 +6,7 @@ import { DateRangeDropdown, ProjectDropdown } from "components/dropdowns"; // ui // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { shouldRenderProject } from "helpers/project.helper"; // types import { ICycle } from "@plane/types"; @@ -66,6 +67,7 @@ export const CycleForm: React.FC = (props) => { setActiveProject(val); }} buttonVariant="background-with-text" + renderCondition={(project) => shouldRenderProject(project)} tabIndex={7} /> )} diff --git a/web/components/dropdowns/project.tsx b/web/components/dropdowns/project.tsx index 719b89802..ce490583a 100644 --- a/web/components/dropdowns/project.tsx +++ b/web/components/dropdowns/project.tsx @@ -15,6 +15,7 @@ import { ProjectLogo } from "components/project"; // types import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; import { TDropdownProps } from "./types"; +import { IProject } from "@plane/types"; // constants type Props = TDropdownProps & { @@ -23,6 +24,7 @@ type Props = TDropdownProps & { dropdownArrowClassName?: string; onChange: (val: string) => void; onClose?: () => void; + renderCondition?: (project: IProject) => boolean; value: string | null; }; @@ -41,6 +43,7 @@ export const ProjectDropdown: React.FC = observer((props) => { onClose, placeholder = "Project", placement, + renderCondition, showTooltip = false, tabIndex, value, @@ -71,7 +74,7 @@ export const ProjectDropdown: React.FC = observer((props) => { const options = joinedProjectIds?.map((projectId) => { const projectDetails = getProjectById(projectId); - + if (renderCondition && projectDetails && !renderCondition(projectDetails)) return; return { value: projectId, query: `${projectDetails?.name}`, @@ -89,7 +92,7 @@ export const ProjectDropdown: React.FC = observer((props) => { }); const filteredOptions = - query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); + query === "" ? options : options?.filter((o) => o?.query.toLowerCase().includes(query.toLowerCase())); const selectedProject = value ? getProjectById(value) : null; @@ -205,24 +208,27 @@ export const ProjectDropdown: React.FC = observer((props) => {
{filteredOptions ? ( filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ - active ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` - } - > - {({ selected }) => ( - <> - {option.content} - {selected && } - - )} - - )) + filteredOptions.map((option) => { + if (!option) return; + return ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + ); + }) ) : (

No matching results

) diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx index f585fae55..7e38acc1c 100644 --- a/web/components/issues/issue-modal/form.tsx +++ b/web/components/issues/issue-modal/form.tsx @@ -30,6 +30,7 @@ import { FileService } from "services/file.service"; // ui // helpers import { getChangedIssuefields } from "helpers/issue.helper"; +import { shouldRenderProject } from "helpers/project.helper"; // types import type { TIssue, ISearchIssueResponse } from "@plane/types"; @@ -304,7 +305,7 @@ export const IssueFormRoot: FC = observer((props) => { handleFormChange(); }} buttonVariant="border-with-text" - // TODO: update tabIndex logic + renderCondition={(project) => shouldRenderProject(project)} tabIndex={getTabIndex("project_id")} />
diff --git a/web/components/modules/form.tsx b/web/components/modules/form.tsx index 4af097591..8fa657b99 100644 --- a/web/components/modules/form.tsx +++ b/web/components/modules/form.tsx @@ -7,6 +7,7 @@ import { ModuleStatusSelect } from "components/modules"; // ui // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { shouldRenderProject } from "helpers/project.helper"; // types import { IModule } from "@plane/types"; @@ -78,6 +79,7 @@ export const ModuleForm: React.FC = (props) => { setActiveProject(val); }} buttonVariant="border-with-text" + renderCondition={(project) => shouldRenderProject(project)} tabIndex={10} />
diff --git a/web/components/project/create-project-modal.tsx b/web/components/project/create-project-modal.tsx index 93f3d065a..5e8cf1895 100644 --- a/web/components/project/create-project-modal.tsx +++ b/web/components/project/create-project-modal.tsx @@ -1,15 +1,8 @@ import { useEffect, Fragment, FC, useState } from "react"; -import { observer } from "mobx-react-lite"; import { Dialog, Transition } from "@headlessui/react"; -// ui -import { setToast, TOAST_TYPE } from "@plane/ui"; // components import { CreateProjectForm } from "./create-project-form"; import { ProjectFeatureUpdate } from "./project-feature-update"; -// constants -import { EUserWorkspaceRoles } from "constants/workspace"; -// hooks -import { useUser } from "hooks/store"; type Props = { isOpen: boolean; @@ -23,32 +16,11 @@ enum EProjectCreationSteps { FEATURE_SELECTION = "FEATURE_SELECTION", } -interface IIsGuestCondition { - onClose: () => void; -} - -const IsGuestCondition: FC = ({ onClose }) => { - useEffect(() => { - onClose(); - setToast({ - title: "Error", - type: TOAST_TYPE.ERROR, - message: "You don't have permission to create project.", - }); - }, [onClose]); - - return null; -}; - -export const CreateProjectModal: FC = observer((props) => { +export const CreateProjectModal: FC = (props) => { const { isOpen, onClose, setToFavorite = false, workspaceSlug } = props; // states const [currentStep, setCurrentStep] = useState(EProjectCreationSteps.CREATE_PROJECT); const [createdProjectId, setCreatedProjectId] = useState(null); - // hooks - const { - membership: { currentWorkspaceRole }, - } = useUser(); useEffect(() => { if (isOpen) { @@ -57,9 +29,6 @@ export const CreateProjectModal: FC = observer((props) => { } }, [isOpen]); - if (currentWorkspaceRole && isOpen) - if (currentWorkspaceRole < EUserWorkspaceRoles.MEMBER) return ; - const handleNextStep = (projectId: string) => { if (!projectId) return; setCreatedProjectId(projectId); @@ -111,4 +80,4 @@ export const CreateProjectModal: FC = observer((props) => { ); -}); +}; diff --git a/web/helpers/project.helper.ts b/web/helpers/project.helper.ts index ba0d52742..fbed0ba5b 100644 --- a/web/helpers/project.helper.ts +++ b/web/helpers/project.helper.ts @@ -3,6 +3,8 @@ import sortBy from "lodash/sortBy"; import { satisfiesDateFilter } from "helpers/filter.helper"; // types import { IProject, TProjectDisplayFilters, TProjectFilters, TProjectOrderByOptions } from "@plane/types"; +// constants +import { EUserProjectRoles } from "constants/project"; /** * Updates the sort order of the project. @@ -51,6 +53,14 @@ export const orderJoinedProjects = ( export const projectIdentifierSanitizer = (identifier: string): string => identifier.replace(/[^ÇŞĞIİÖÜA-Za-z0-9]/g, ""); +/** + * @description Checks if the project should be rendered or not based on the user role + * @param {IProject} project + * @returns {boolean} + */ +export const shouldRenderProject = (project: IProject): boolean => + !!project.member_role && project.member_role >= EUserProjectRoles.MEMBER; + /** * @description filters projects based on the filter * @param {IProject} project From 4a93fdbdb4f28d68adf4535e981b4501a9f327f2 Mon Sep 17 00:00:00 2001 From: Ramesh Kumar Chandra <31303617+rameshkumarchandra@users.noreply.github.com> Date: Mon, 18 Mar 2024 12:50:33 +0530 Subject: [PATCH 20/22] [WEB-700] chore: sidebar hamburger refactor (#3960) --- web/components/cycles/cycle-mobile-header.tsx | 2 +- .../cycles/cycles-list-mobile-header.tsx | 52 ++++ web/components/headers/cycle-issues.tsx | 13 +- web/components/headers/cycles.tsx | 50 +-- web/components/headers/global-issues.tsx | 9 +- web/components/headers/module-issues.tsx | 13 +- web/components/headers/modules-list.tsx | 293 ++++++++---------- web/components/headers/page-details.tsx | 4 +- web/components/headers/pages.tsx | 4 +- .../project-archived-issue-details.tsx | 4 +- .../headers/project-archived-issues.tsx | 16 +- .../headers/project-draft-issues.tsx | 5 +- web/components/headers/project-inbox.tsx | 4 +- .../headers/project-issue-details.tsx | 4 +- web/components/headers/project-issues.tsx | 8 +- web/components/headers/project-settings.tsx | 4 +- .../headers/project-view-issues.tsx | 4 +- web/components/headers/project-views.tsx | 4 +- web/components/headers/projects.tsx | 4 +- web/components/headers/user-profile.tsx | 4 +- .../headers/workspace-active-cycles.tsx | 4 +- .../headers/workspace-analytics.tsx | 4 +- .../headers/workspace-dashboard.tsx | 4 +- web/components/headers/workspace-settings.tsx | 2 - .../issues/issues-mobile-header.tsx | 7 +- .../modules/moduels-list-mobile-header.tsx | 40 +++ .../modules/module-mobile-header.tsx | 15 +- web/layouts/app-layout/layout.tsx | 17 +- .../projects/[projectId]/cycles/[cycleId].tsx | 3 +- .../projects/[projectId]/cycles/index.tsx | 3 +- .../projects/[projectId]/issues/index.tsx | 3 +- .../[projectId]/modules/[moduleId].tsx | 3 +- .../projects/[projectId]/modules/index.tsx | 3 +- 33 files changed, 289 insertions(+), 320 deletions(-) create mode 100644 web/components/cycles/cycles-list-mobile-header.tsx create mode 100644 web/components/modules/moduels-list-mobile-header.tsx diff --git a/web/components/cycles/cycle-mobile-header.tsx b/web/components/cycles/cycle-mobile-header.tsx index add78943c..9fbc96df3 100644 --- a/web/components/cycles/cycle-mobile-header.tsx +++ b/web/components/cycles/cycle-mobile-header.tsx @@ -109,7 +109,7 @@ export const CycleMobileHeader = () => { onClose={() => setAnalyticsModal(false)} cycleDetails={cycleDetails ?? undefined} /> -
+
{ + const { currentProjectDetails } = useProject(); + // hooks + const { updateDisplayFilters } = useCycleFilter(); + return ( +
+ + + Layout + + } + customButtonClassName="flex flex-grow justify-center items-center text-custom-text-200 text-sm" + closeOnSelect + > + {CYCLE_VIEW_LAYOUTS.map((layout) => { + if (layout.key == "gantt") return; + return ( + { + updateDisplayFilters(currentProjectDetails!.id, { + layout: layout.key, + }); + }} + className="flex items-center gap-2" + > + +
{layout.title}
+
+ ); + })} +
+
+ ); +}); + +export default CyclesListMobileHeader; diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index 8c5018ffb..a9dc3a8f4 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -9,8 +9,6 @@ import { ArrowRight, Plus, PanelRight } from "lucide-react"; import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip } from "@plane/ui"; import { ProjectAnalyticsModal } from "components/analytics"; import { BreadcrumbLink } from "components/common"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { CycleMobileHeader } from "components/cycles/cycle-mobile-header"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; @@ -159,9 +157,8 @@ export const CycleIssuesHeader: React.FC = observer(() => { cycleDetails={cycleDetails ?? undefined} />
-
+
- { {issueCount && issueCount > 0 ? ( 1 ? "issues" : "issue" - } in this cycle`} + tooltipContent={`There are ${issueCount} ${issueCount > 1 ? "issues" : "issue" + } in this cycle`} position="bottom" > @@ -299,9 +295,6 @@ export const CycleIssuesHeader: React.FC = observer(() => {
-
- -
); diff --git a/web/components/headers/cycles.tsx b/web/components/headers/cycles.tsx index 6f019f3bd..375a04a13 100644 --- a/web/components/headers/cycles.tsx +++ b/web/components/headers/cycles.tsx @@ -1,19 +1,15 @@ -import { FC, useCallback } from "react"; +import { FC } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { List, Plus } from "lucide-react"; +import { Plus } from "lucide-react"; // hooks // ui -import { Breadcrumbs, Button, ContrastIcon, CustomMenu } from "@plane/ui"; +import { Breadcrumbs, Button, ContrastIcon } from "@plane/ui"; // helpers // components import { BreadcrumbLink } from "components/common"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { CYCLE_VIEW_LAYOUTS } from "constants/cycle"; import { EUserProjectRoles } from "constants/project"; import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; -import useLocalStorage from "hooks/use-local-storage"; -import { TCycleLayoutOptions } from "@plane/types"; import { ProjectLogo } from "components/project"; export const CyclesHeader: FC = observer(() => { @@ -33,20 +29,11 @@ export const CyclesHeader: FC = observer(() => { const canUserCreateCycle = currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); - const { setValue: setCycleLayout } = useLocalStorage("cycle_layout", "list"); - - const handleCurrentLayout = useCallback( - (_layout: TCycleLayoutOptions) => { - setCycleLayout(_layout); - }, - [setCycleLayout] - ); return (
-
+
-
{
)}
-
- - - Layout - - } - customButtonClassName="flex flex-grow justify-center items-center text-custom-text-200 text-sm" - closeOnSelect - > - {CYCLE_VIEW_LAYOUTS.map((layout) => ( - { - // handleLayoutChange(ISSUE_LAYOUTS[index].key); - handleCurrentLayout(layout.key as TCycleLayoutOptions); - }} - className="flex items-center gap-2" - > - -
{layout.title}
-
- ))} -
-
); }); diff --git a/web/components/headers/global-issues.tsx b/web/components/headers/global-issues.tsx index 13dccba41..cf44e389e 100644 --- a/web/components/headers/global-issues.tsx +++ b/web/components/headers/global-issues.tsx @@ -7,7 +7,6 @@ import { usePlatformOS } from "hooks/use-platform-os"; import { List, PlusIcon, Sheet } from "lucide-react"; import { Breadcrumbs, Button, LayersIcon, PhotoFilterIcon, Tooltip } from "@plane/ui"; import { BreadcrumbLink } from "components/common"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "components/issues"; // components import { CreateUpdateWorkspaceViewModal } from "components/workspace"; @@ -109,9 +108,8 @@ export const GlobalIssuesHeader: React.FC = observer((props) => { return ( <> setCreateViewModal(false)} /> -
+
- = observer((props) => {
{ moduleDetails={moduleDetails ?? undefined} />
-
+
- {

{moduleDetails?.name && moduleDetails.name}

{issueCount && issueCount > 0 ? ( 1 ? "issues" : "issue" - } in this module`} + isMobile={isMobile} + tooltipContent={`There are ${issueCount} ${issueCount > 1 ? "issues" : "issue" + } in this module`} position="bottom" > @@ -309,7 +305,6 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
-
); diff --git a/web/components/headers/modules-list.tsx b/web/components/headers/modules-list.tsx index fcd26dd59..a6147cad3 100644 --- a/web/components/headers/modules-list.tsx +++ b/web/components/headers/modules-list.tsx @@ -1,13 +1,12 @@ import { useCallback, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { GanttChartSquare, LayoutGrid, List, ListFilter, Plus, Search, X } from "lucide-react"; +import { ListFilter, Plus, Search, X } from "lucide-react"; // hooks import { useApplication, useEventTracker, useMember, useModuleFilter, useProject, useUser } from "hooks/store"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { BreadcrumbLink } from "components/common"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { ProjectLogo } from "components/project"; // constants import { MODULE_VIEW_LAYOUTS } from "constants/module"; @@ -17,7 +16,7 @@ import { usePlatformOS } from "hooks/use-platform-os"; import { ModuleFiltersSelection, ModuleOrderByDropdown } from "components/modules"; import { FiltersDropdown } from "components/issues"; // ui -import { Breadcrumbs, Button, Tooltip, DiceIcon, CustomMenu } from "@plane/ui"; +import { Breadcrumbs, Button, Tooltip, DiceIcon } from "@plane/ui"; // helpers import { cn } from "helpers/common.helper"; // types @@ -89,176 +88,138 @@ export const ModulesListHeader: React.FC = observer(() => { currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); return ( -
-
-
- -
- - - - - ) - } - /> - } - /> - } />} - /> - -
-
-
-
- {!isSearchOpen && ( - - )} -
- - updateSearchQuery(e.target.value)} - onKeyDown={handleInputKeyDown} - /> - {isSearchOpen && ( - - )} -
-
-
- {MODULE_VIEW_LAYOUTS.map((layout) => ( - - - - ))} -
- { - if (!projectId || val === displayFilters?.order_by) return; - updateDisplayFilters(projectId.toString(), { - order_by: val, - }); - }} - /> - } title="Filters" placement="bottom-end"> - { - if (!projectId) return; - updateDisplayFilters(projectId.toString(), val); - }} - handleFiltersUpdate={handleFilters} - memberIds={workspaceMemberIds ?? undefined} +
+
+
+ + + + + ) + } + /> + } /> - - {canUserCreateModule && ( - - )} + } />} + /> +
-
- - {displayFilters?.layout === "gantt" ? ( - - ) : displayFilters?.layout === "board" ? ( - - ) : ( - - )} - Layout - - } - customButtonClassName="flex flex-grow justify-center items-center text-custom-text-200 text-sm" - closeOnSelect - > - {MODULE_VIEW_LAYOUTS.map((layout) => ( - +
+ {!isSearchOpen && ( + + )} +
+ + updateSearchQuery(e.target.value)} + onKeyDown={handleInputKeyDown} + /> + {isSearchOpen && ( + + )} +
+
+
+ {MODULE_VIEW_LAYOUTS.map((layout) => ( + + + ))} - +
+ { + if (!projectId || val === displayFilters?.order_by) return; + updateDisplayFilters(projectId.toString(), { + order_by: val, + }); + }} + /> + } title="Filters" placement="bottom-end"> + { + if (!projectId) return; + updateDisplayFilters(projectId.toString(), val); + }} + handleFiltersUpdate={handleFilters} + memberIds={workspaceMemberIds ?? undefined} + /> + + {canUserCreateModule && ( + + )}
); diff --git a/web/components/headers/page-details.tsx b/web/components/headers/page-details.tsx index 2c05d95fa..4404fc0f3 100644 --- a/web/components/headers/page-details.tsx +++ b/web/components/headers/page-details.tsx @@ -7,7 +7,6 @@ import { FileText, Plus } from "lucide-react"; import { Breadcrumbs, Button } from "@plane/ui"; // helpers import { BreadcrumbLink } from "components/common"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; // components import { useApplication, usePage, useProject } from "hooks/store"; import { ProjectLogo } from "components/project"; @@ -28,9 +27,8 @@ export const PageDetailsHeader: FC = observer((props) => { const pageDetails = usePage(pageId as string); return ( -
+
-
{ currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); return ( -
+
-
{ ); return ( -
+
-
{ : undefined; return ( -
+
- -
- -
- + { : undefined; return ( -
+
-
{ const { currentProjectDetails } = useProject(); return ( -
+
-
{ const isSidebarCollapsed = themeStore.issueDetailSidebarCollapsed; return ( -
+
-
{ projectDetails={currentProjectDetails ?? undefined} />
-
+
-
router.back()}> { )}
-
- -
); diff --git a/web/components/headers/project-settings.tsx b/web/components/headers/project-settings.tsx index 817d842b4..831965fad 100644 --- a/web/components/headers/project-settings.tsx +++ b/web/components/headers/project-settings.tsx @@ -5,7 +5,6 @@ import { useRouter } from "next/router"; import { Breadcrumbs, CustomMenu } from "@plane/ui"; // helper import { BreadcrumbLink } from "components/common"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "constants/project"; // hooks import { useProject, useUser } from "hooks/store"; @@ -31,8 +30,7 @@ export const ProjectSettingHeader: FC = observer((props) if (currentProjectRole && currentProjectRole <= EUserProjectRoles.VIEWER) return null; return ( -
- +
diff --git a/web/components/headers/project-view-issues.tsx b/web/components/headers/project-view-issues.tsx index ab3959716..c59258463 100644 --- a/web/components/headers/project-view-issues.tsx +++ b/web/components/headers/project-view-issues.tsx @@ -8,7 +8,6 @@ import { Plus } from "lucide-react"; // ui import { Breadcrumbs, Button, CustomMenu, PhotoFilterIcon } from "@plane/ui"; import { BreadcrumbLink } from "components/common"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; // helpers // types @@ -128,9 +127,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); return ( -
+
- { return ( <> -
+
-
{ }; return ( -
+
-
= observer((props) => { const { theme: themStore } = useApplication(); return ( -
+
-
( -
+
-
{ return ( <>
-
{ return ( <> -
+
-
= observer((pro return (
-
{ +export const IssuesMobileHeader = observer(() => { const layouts = [ { key: "list", title: "List", icon: List }, { key: "kanban", title: "Kanban", icon: Kanban }, @@ -87,7 +88,7 @@ export const IssuesMobileHeader = () => { onClose={() => setAnalyticsModal(false)} projectDetails={currentProjectDetails ?? undefined} /> -
+
{
); -}; +}); diff --git a/web/components/modules/moduels-list-mobile-header.tsx b/web/components/modules/moduels-list-mobile-header.tsx new file mode 100644 index 000000000..90919a998 --- /dev/null +++ b/web/components/modules/moduels-list-mobile-header.tsx @@ -0,0 +1,40 @@ +import { CustomMenu } from "@plane/ui"; +import { MODULE_VIEW_LAYOUTS } from "constants/module"; +import { useModuleFilter, useProject } from "hooks/store"; +import { observer } from "mobx-react"; + +const ModulesListMobileHeader = observer(() => { + const { currentProjectDetails } = useProject(); + const { updateDisplayFilters } = useModuleFilter(); + + return ( +
+ Layout} + customButtonClassName="flex flex-grow justify-center items-center text-custom-text-200 text-sm" + closeOnSelect + > + {MODULE_VIEW_LAYOUTS.map((layout) => { + if (layout.key == "gantt") return; + return ( + { + updateDisplayFilters(currentProjectDetails!.id.toString(), { layout: layout.key }); + }} + className="flex items-center gap-2" + > + +
{layout.title}
+
+ ); + })} +
+
+ ); +}); + +export default ModulesListMobileHeader; diff --git a/web/components/modules/module-mobile-header.tsx b/web/components/modules/module-mobile-header.tsx index 4763639ed..ead9eb6c5 100644 --- a/web/components/modules/module-mobile-header.tsx +++ b/web/components/modules/module-mobile-header.tsx @@ -1,14 +1,21 @@ import { useCallback, useState } from "react"; +import { observer } from "mobx-react"; import router from "next/router"; +// icons import { Calendar, ChevronDown, Kanban, List } from "lucide-react"; +// ui import { CustomMenu } from "@plane/ui"; +// components import { ProjectAnalyticsModal } from "components/analytics"; import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues"; -import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "constants/issue"; +// hooks import { useIssues, useLabel, useMember, useModule, useProjectState } from "hooks/store"; +// types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; +// constants +import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "constants/issue"; -export const ModuleMobileHeader = () => { +export const ModuleMobileHeader = observer(() => { const [analyticsModal, setAnalyticsModal] = useState(false); const { getModuleById } = useModule(); const layouts = [ @@ -83,7 +90,7 @@ export const ModuleMobileHeader = () => { onClose={() => setAnalyticsModal(false)} moduleDetails={moduleDetails ?? undefined} /> -
+
{
); -}; +}); diff --git a/web/layouts/app-layout/layout.tsx b/web/layouts/app-layout/layout.tsx index dd1df164f..b417a5dfc 100644 --- a/web/layouts/app-layout/layout.tsx +++ b/web/layouts/app-layout/layout.tsx @@ -1,22 +1,21 @@ import { FC, ReactNode } from "react"; // layouts import { observer } from "mobx-react-lite"; -import useSWR from "swr"; import { CommandPalette } from "components/command-palette"; -import { EIssuesStoreType } from "constants/issue"; -import { useIssues } from "hooks/store/use-issues"; import { UserAuthWrapper, WorkspaceAuthWrapper, ProjectAuthWrapper } from "layouts/auth-layout"; // components import { AppSidebar } from "./sidebar"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; export interface IAppLayout { children: ReactNode; header: ReactNode; withProjectWrapper?: boolean; + mobileHeader?: ReactNode; } export const AppLayout: FC = observer((props) => { - const { children, header, withProjectWrapper = false } = props; + const { children, header, withProjectWrapper = false, mobileHeader } = props; return ( <> @@ -26,7 +25,15 @@ export const AppLayout: FC = observer((props) => {
- {header} +
+
+
+ +
+
{header}
+
+ {mobileHeader && mobileHeader} +
{withProjectWrapper ? {children} : <>{children}} diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx index 6cf76cd70..5f1c9de77 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx @@ -17,6 +17,7 @@ import { AppLayout } from "layouts/app-layout"; // assets import { NextPageWithLayout } from "lib/types"; import emptyCycle from "public/empty-state/cycle.svg"; +import { CycleMobileHeader } from "components/cycles/cycle-mobile-header"; // types const CycleDetailPage: NextPageWithLayout = observer(() => { @@ -85,7 +86,7 @@ const CycleDetailPage: NextPageWithLayout = observer(() => { CycleDetailPage.getLayout = function getLayout(page: ReactElement) { return ( - } withProjectWrapper> + } mobileHeader={} withProjectWrapper> {page} ); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index 651d03898..391f69007 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -27,6 +27,7 @@ import { TCycleFilters } from "@plane/types"; // constants import { CYCLE_TABS_LIST } from "constants/cycle"; import { EmptyStateType } from "constants/empty-state"; +import CyclesListMobileHeader from "components/cycles/cycles-list-mobile-header"; const ProjectCyclesPage: NextPageWithLayout = observer(() => { // states @@ -137,7 +138,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { ProjectCyclesPage.getLayout = function getLayout(page: ReactElement) { return ( - } withProjectWrapper> + } mobileHeader={} withProjectWrapper> {page} ); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx index 241af79c4..e79ef109d 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx @@ -10,6 +10,7 @@ import { ProjectLayoutRoot } from "components/issues"; import { useProject } from "hooks/store"; import { AppLayout } from "layouts/app-layout"; import { NextPageWithLayout } from "lib/types"; +import { IssuesMobileHeader } from "components/issues/issues-mobile-header"; // layouts // hooks @@ -42,7 +43,7 @@ const ProjectIssuesPage: NextPageWithLayout = observer(() => { ProjectIssuesPage.getLayout = function getLayout(page: ReactElement) { return ( - } withProjectWrapper> + } mobileHeader={} withProjectWrapper> {page} ); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx index c5f41d419..949731f3b 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx @@ -16,6 +16,7 @@ import { AppLayout } from "layouts/app-layout"; // assets import { NextPageWithLayout } from "lib/types"; import emptyModule from "public/empty-state/module.svg"; +import { ModuleMobileHeader } from "components/modules/module-mobile-header"; // types const ModuleIssuesPage: NextPageWithLayout = observer(() => { @@ -83,7 +84,7 @@ const ModuleIssuesPage: NextPageWithLayout = observer(() => { ModuleIssuesPage.getLayout = function getLayout(page: ReactElement) { return ( - } withProjectWrapper> + } mobileHeader={} withProjectWrapper> {page} ); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx index eb3c92044..44bce7da7 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx @@ -13,6 +13,7 @@ import { AppLayout } from "layouts/app-layout"; import { NextPageWithLayout } from "lib/types"; import { calculateTotalFilters } from "helpers/filter.helper"; import { TModuleFilters } from "@plane/types"; +import ModulesListMobileHeader from "components/modules/moduels-list-mobile-header"; const ProjectModulesPage: NextPageWithLayout = observer(() => { const router = useRouter(); @@ -59,7 +60,7 @@ const ProjectModulesPage: NextPageWithLayout = observer(() => { ProjectModulesPage.getLayout = function getLayout(page: ReactElement) { return ( - } withProjectWrapper> + } mobileHeader={} withProjectWrapper> {page} ); From e9774e1af370351b8ae2e1eaacd10dc70dfa4417 Mon Sep 17 00:00:00 2001 From: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com> Date: Mon, 18 Mar 2024 12:51:19 +0530 Subject: [PATCH 21/22] [WEB-759] style: added calendar layout responsiveness (#3969) * style: added calendar layout responsiveness * fix: quick-add-form * fix: popover menu close on item select * fix: class conditiion * code review --- .../issue-layouts/calendar/calendar.tsx | 55 +++++- .../issue-layouts/calendar/day-tile.tsx | 74 ++++---- .../calendar/dropdowns/options-dropdown.tsx | 41 ++-- .../issues/issue-layouts/calendar/header.tsx | 4 +- .../issues/issue-layouts/calendar/index.ts | 2 + .../calendar/issue-block-root.tsx | 22 +++ .../issue-layouts/calendar/issue-block.tsx | 113 +++++++++++ .../issue-layouts/calendar/issue-blocks.tsx | 177 ++++++++---------- .../calendar/quick-add-issue-form.tsx | 8 +- .../issue-layouts/calendar/week-days.tsx | 8 +- .../issue-layouts/calendar/week-header.tsx | 4 +- 11 files changed, 343 insertions(+), 165 deletions(-) create mode 100644 web/components/issues/issue-layouts/calendar/issue-block-root.tsx create mode 100644 web/components/issues/issue-layouts/calendar/issue-block.tsx diff --git a/web/components/issues/issue-layouts/calendar/calendar.tsx b/web/components/issues/issue-layouts/calendar/calendar.tsx index efd785d3e..96d915149 100644 --- a/web/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/components/issues/issue-layouts/calendar/calendar.tsx @@ -1,9 +1,11 @@ +import { useState } from "react"; import { observer } from "mobx-react-lite"; // hooks +import useSize from "hooks/use-window-size"; // components // ui import { Spinner } from "@plane/ui"; -import { CalendarHeader, CalendarWeekDays, CalendarWeekHeader } from "components/issues"; +import { CalendarHeader, CalendarIssueBlocks, CalendarWeekDays, CalendarWeekHeader } from "components/issues"; // types import { IIssueDisplayFilterOptions, @@ -15,6 +17,9 @@ import { TIssueMap, } from "@plane/types"; import { ICalendarWeek } from "./types"; +// helpers +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { cn } from "helpers/common.helper"; // constants import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; @@ -24,6 +29,7 @@ import { ICycleIssuesFilter } from "store/issue/cycle"; import { IModuleIssuesFilter } from "store/issue/module"; import { IProjectIssuesFilter } from "store/issue/project"; import { IProjectViewIssuesFilter } from "store/issue/project-views"; +import { MONTHS_LIST } from "constants/calendar"; type Props = { issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; @@ -62,6 +68,8 @@ export const CalendarChart: React.FC = observer((props) => { updateFilters, readOnly = false, } = props; + // states + const [selectedDate, setSelectedDate] = useState(new Date()); // store hooks const { issues: { viewFlags }, @@ -70,6 +78,7 @@ export const CalendarChart: React.FC = observer((props) => { const { membership: { currentProjectRole }, } = useUser(); + const [windowWidth] = useSize(); const { enableIssueCreation } = viewFlags || {}; const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; @@ -78,18 +87,30 @@ export const CalendarChart: React.FC = observer((props) => { const allWeeksOfActiveMonth = issueCalendarView.allWeeksOfActiveMonth; - if (!calendarPayload) + const formattedDatePayload = renderFormattedPayloadDate(selectedDate) ?? undefined; + + if (!calendarPayload || !formattedDatePayload) return (
); + const issueIdList = groupedIssueIds ? groupedIssueIds[formattedDatePayload] : null; + return ( <>
- -
+ +
768, + })} + >
{layout === "month" && ( @@ -97,6 +118,8 @@ export const CalendarChart: React.FC = observer((props) => { {allWeeksOfActiveMonth && Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => ( = observer((props) => { )} {layout === "week" && ( = observer((props) => { /> )}
+ + {/* mobile view */} +
+

+ {`${selectedDate.getDate()} ${ + MONTHS_LIST[selectedDate.getMonth() + 1].title + }, ${selectedDate.getFullYear()}`} +

+ +
diff --git a/web/components/issues/issue-layouts/calendar/day-tile.tsx b/web/components/issues/issue-layouts/calendar/day-tile.tsx index 8ac1e460c..12b7f767b 100644 --- a/web/components/issues/issue-layouts/calendar/day-tile.tsx +++ b/web/components/issues/issue-layouts/calendar/day-tile.tsx @@ -1,10 +1,10 @@ -import { useState } from "react"; import { Droppable } from "@hello-pangea/dnd"; import { observer } from "mobx-react-lite"; // components -import { CalendarIssueBlocks, ICalendarDate, CalendarQuickAddIssueForm } from "components/issues"; +import { CalendarIssueBlocks, ICalendarDate } from "components/issues"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { cn } from "helpers/common.helper"; // constants import { MONTHS_LIST } from "constants/calendar"; // types @@ -31,6 +31,8 @@ type Props = { addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; readOnly?: boolean; + selectedDate: Date; + setSelectedDate: (date: Date) => void; }; export const CalendarDayTile: React.FC = observer((props) => { @@ -46,8 +48,10 @@ export const CalendarDayTile: React.FC = observer((props) => { addIssuesToView, viewId, readOnly = false, + selectedDate, + setSelectedDate, } = props; - const [showAllIssues, setShowAllIssues] = useState(false); + const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month"; const formattedDatePayload = renderFormattedPayloadDate(date.date); @@ -57,13 +61,14 @@ export const CalendarDayTile: React.FC = observer((props) => { const totalIssues = issueIdList?.length ?? 0; const isToday = date.date.toDateString() === new Date().toDateString(); + const isSelectedDate = date.date.toDateString() == selectedDate.toDateString(); return ( <>
{/* header */}
= observer((props) => {
{/* content */} -
+
{(provided, snapshot) => (
= observer((props) => { ref={provided.innerRef} > - - {enableQuickIssueCreate && !disableIssueCreation && !readOnly && ( -
- setShowAllIssues(true)} - /> -
- )} - - {totalIssues > 4 && ( -
- -
- )} - {provided.placeholder}
)}
+ + {/* Mobile view content */} +
setSelectedDate(date.date)} + className={cn( + "text-sm py-2.5 h-full w-full font-medium mx-auto flex flex-col justify-start items-center md:hidden cursor-pointer", + { + "bg-custom-background-100": date.date.getDay() !== 0 && date.date.getDay() !== 6, + } + )} + > +
+ {date.date.getDate()} +
+ + {totalIssues > 0 &&
} +
); diff --git a/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx b/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx index 3050bba72..953c8384f 100644 --- a/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx +++ b/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx @@ -4,9 +4,10 @@ import { useRouter } from "next/router"; import { usePopper } from "react-popper"; import { Popover, Transition } from "@headlessui/react"; // hooks +import useSize from "hooks/use-window-size"; // ui // icons -import { Check, ChevronUp } from "lucide-react"; +import { Check, ChevronUp, MoreVerticalIcon } from "lucide-react"; import { ToggleSwitch } from "@plane/ui"; // types import { @@ -41,6 +42,7 @@ export const CalendarOptionsDropdown: React.FC = observer((prop const { projectId } = router.query; const issueCalendarView = useCalendarView(); + const [windowWidth] = useSize(); const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -60,7 +62,7 @@ export const CalendarOptionsDropdown: React.FC = observer((prop const calendarLayout = issuesFilterStore.issueFilters?.displayFilters?.calendar?.layout ?? "month"; const showWeekends = issuesFilterStore.issueFilters?.displayFilters?.calendar?.show_weekends ?? false; - const handleLayoutChange = (layout: TCalendarLayouts) => { + const handleLayoutChange = (layout: TCalendarLayouts, closePopover: any) => { if (!projectId || !updateFilters) return; updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, { @@ -75,6 +77,7 @@ export const CalendarOptionsDropdown: React.FC = observer((prop ? issueCalendarView.calendarFilters.activeMonthDate : issueCalendarView.calendarFilters.activeWeekDate ); + if (windowWidth <= 768) closePopover(); // close the popover on mobile }; const handleToggleWeekends = () => { @@ -92,21 +95,24 @@ export const CalendarOptionsDropdown: React.FC = observer((prop return ( - {({ open }) => ( + {({ open, close: closePopover }) => ( <> - @@ -132,7 +138,7 @@ export const CalendarOptionsDropdown: React.FC = observer((prop key={layout} type="button" className="flex w-full items-center justify-between gap-2 rounded px-1 py-1.5 text-left text-xs hover:bg-custom-background-80" - onClick={() => handleLayoutChange(layoutDetails.key)} + onClick={() => handleLayoutChange(layoutDetails.key, closePopover)} > {layoutDetails.title} {calendarLayout === layout && } @@ -144,7 +150,12 @@ export const CalendarOptionsDropdown: React.FC = observer((prop onClick={handleToggleWeekends} > Show weekends - {}} /> + { + if (windowWidth <= 768) closePopover(); // close the popover on mobile + }} + />
diff --git a/web/components/issues/issue-layouts/calendar/header.tsx b/web/components/issues/issue-layouts/calendar/header.tsx index aa055534d..bb3bc3c6d 100644 --- a/web/components/issues/issue-layouts/calendar/header.tsx +++ b/web/components/issues/issue-layouts/calendar/header.tsx @@ -24,10 +24,11 @@ interface ICalendarHeader { filterType: EIssueFilterType, filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters ) => Promise; + setSelectedDate: (date: Date) => void; } export const CalendarHeader: React.FC = observer((props) => { - const { issuesFilterStore, updateFilters } = props; + const { issuesFilterStore, updateFilters, setSelectedDate } = props; const issueCalendarView = useCalendarView(); @@ -91,6 +92,7 @@ export const CalendarHeader: React.FC = observer((props) => { activeMonthDate: firstDayOfCurrentMonth, activeWeekDate: today, }); + setSelectedDate(today); }; return ( diff --git a/web/components/issues/issue-layouts/calendar/index.ts b/web/components/issues/issue-layouts/calendar/index.ts index 69fce6662..527a9eff4 100644 --- a/web/components/issues/issue-layouts/calendar/index.ts +++ b/web/components/issues/issue-layouts/calendar/index.ts @@ -5,6 +5,8 @@ export * from "./types.d"; export * from "./day-tile"; export * from "./header"; export * from "./issue-blocks"; +export * from "./issue-block-root"; +export * from "./issue-block"; export * from "./week-days"; export * from "./week-header"; export * from "./quick-add-issue-form"; diff --git a/web/components/issues/issue-layouts/calendar/issue-block-root.tsx b/web/components/issues/issue-layouts/calendar/issue-block-root.tsx new file mode 100644 index 000000000..1f2f84869 --- /dev/null +++ b/web/components/issues/issue-layouts/calendar/issue-block-root.tsx @@ -0,0 +1,22 @@ +import React from "react"; +// components +import { CalendarIssueBlock } from "components/issues"; +// types +import { TIssue, TIssueMap } from "@plane/types"; + +type Props = { + issues: TIssueMap | undefined; + issueId: string; + quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; + isDragging?: boolean; +}; + +export const CalendarIssueBlockRoot: React.FC = (props) => { + const { issues, issueId, quickActions, isDragging } = props; + + if (!issues?.[issueId]) return null; + + const issue = issues?.[issueId]; + + return ; +}; diff --git a/web/components/issues/issue-layouts/calendar/issue-block.tsx b/web/components/issues/issue-layouts/calendar/issue-block.tsx new file mode 100644 index 000000000..226d447f2 --- /dev/null +++ b/web/components/issues/issue-layouts/calendar/issue-block.tsx @@ -0,0 +1,113 @@ +import { useState, useRef } from "react"; +import { MoreHorizontal } from "lucide-react"; +import { observer } from "mobx-react"; +// components +import { Tooltip, ControlLink } from "@plane/ui"; +// hooks +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { TIssue } from "@plane/types"; +import { usePlatformOS } from "hooks/use-platform-os"; + +type Props = { + issue: TIssue; + quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; + isDragging?: boolean; +}; + +export const CalendarIssueBlock: React.FC = observer((props) => { + const { issue, quickActions, isDragging = false } = props; + // hooks + const { + router: { workspaceSlug, projectId }, + } = useApplication(); + const { getProjectIdentifierById } = useProject(); + const { getProjectStates } = useProjectState(); + const { peekIssue, setPeekIssue } = useIssueDetail(); + const { isMobile } = usePlatformOS(); + // states + const [isMenuActive, setIsMenuActive] = useState(false); + + const menuActionRef = useRef(null); + + const stateColor = getProjectStates(issue?.project_id)?.find((state) => state?.id == issue?.state_id)?.color || ""; + + const handleIssuePeekOverview = (issue: TIssue) => + workspaceSlug && + issue && + issue.project_id && + issue.id && + setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id }); + + useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false)); + + const customActionButton = ( +
setIsMenuActive(!isMenuActive)} + > + +
+ ); + + return ( + handleIssuePeekOverview(issue)} + className="w-full cursor-pointer text-sm text-custom-text-100" + disabled={!!issue?.tempId} + > + <> + {issue?.tempId !== undefined && ( +
+ )} + +
+
+ +
+ {getProjectIdentifierById(issue?.project_id)}-{issue.sequence_id} +
+ +
{issue.name}
+
+
+
{ + e.preventDefault(); + e.stopPropagation(); + }} + > + {quickActions(issue, customActionButton)} +
+
+ + + ); +}); diff --git a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx index df183ba3d..0cb4e572c 100644 --- a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx +++ b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx @@ -1,74 +1,62 @@ -import { useState, useRef } from "react"; +import { useState } from "react"; import { Draggable } from "@hello-pangea/dnd"; import { observer } from "mobx-react-lite"; -import { MoreHorizontal } from "lucide-react"; // components -import { Tooltip, ControlLink } from "@plane/ui"; -// hooks -import { cn } from "helpers/common.helper"; -import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store"; -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -import { usePlatformOS } from "hooks/use-platform-os"; +import { CalendarQuickAddIssueForm, CalendarIssueBlockRoot } from "components/issues"; // helpers +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types import { TIssue, TIssueMap } from "@plane/types"; +import useSize from "hooks/use-window-size"; type Props = { + date: Date; issues: TIssueMap | undefined; issueIdList: string[] | null; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; - showAllIssues?: boolean; isDragDisabled?: boolean; + enableQuickIssueCreate?: boolean; + disableIssueCreation?: boolean; + quickAddCallback?: ( + workspaceSlug: string, + projectId: string, + data: TIssue, + viewId?: string + ) => Promise; + addIssuesToView?: (issueIds: string[]) => Promise; + viewId?: string; + readOnly?: boolean; }; export const CalendarIssueBlocks: React.FC = observer((props) => { - const { issues, issueIdList, quickActions, showAllIssues = false, isDragDisabled = false } = props; - // hooks const { - router: { workspaceSlug, projectId }, - } = useApplication(); - const { getProjectIdentifierById } = useProject(); - const { getProjectStates } = useProjectState(); - const { peekIssue, setPeekIssue } = useIssueDetail(); - const { isMobile } = usePlatformOS(); + date, + issues, + issueIdList, + quickActions, + isDragDisabled = false, + enableQuickIssueCreate, + disableIssueCreation, + quickAddCallback, + addIssuesToView, + viewId, + readOnly, + } = props; // states - const [isMenuActive, setIsMenuActive] = useState(false); + const [showAllIssues, setShowAllIssues] = useState(false); + // hooks + const [windowWidth] = useSize(); - const menuActionRef = useRef(null); + const formattedDatePayload = renderFormattedPayloadDate(date); + const totalIssues = issueIdList?.length ?? 0; - const handleIssuePeekOverview = (issue: TIssue) => - workspaceSlug && - issue && - issue.project_id && - issue.id && - setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id }); - - useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false)); - - const customActionButton = ( -
setIsMenuActive(!isMenuActive)} - > - -
- ); + if (!formattedDatePayload) return null; return ( <> - {issueIdList?.slice(0, showAllIssues ? issueIdList.length : 4).map((issueId, index) => { - if (!issues?.[issueId]) return null; - - const issue = issues?.[issueId]; - - const stateColor = - getProjectStates(issue?.project_id)?.find((state) => state?.id == issue?.state_id)?.color || ""; - - return ( - + {issueIdList?.slice(0, showAllIssues || windowWidth <= 768 ? issueIdList.length : 4).map((issueId, index) => + windowWidth > 768 ? ( + {(provided, snapshot) => (
= observer((props) => { {...provided.dragHandleProps} ref={provided.innerRef} > - handleIssuePeekOverview(issue)} - className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" - disabled={!!issue?.tempId} - > - <> - {issue?.tempId !== undefined && ( -
- )} - -
-
- -
- {getProjectIdentifierById(issue?.project_id)}-{issue.sequence_id} -
- -
{issue.name}
-
-
-
{ - e.preventDefault(); - e.stopPropagation(); - }} - > - {quickActions(issue, customActionButton)} -
-
- - +
)} - ); - })} + ) : ( + + ) + )} + + {enableQuickIssueCreate && !disableIssueCreation && !readOnly && ( +
+ setShowAllIssues(true)} + /> +
+ )} + {totalIssues > 4 && ( +
+ +
+ )} ); }); diff --git a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx index 5f62706dc..3b319fdad 100644 --- a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx @@ -50,7 +50,7 @@ const Inputs = (props: any) => { return ( <> -

{projectDetails?.identifier ?? "..."}

+

{projectDetails?.identifier ?? "..."}

{ {...register("name", { required: "Issue title is required.", })} - className="w-full rounded-md bg-transparent py-1.5 pr-2 text-xs font-medium leading-5 text-custom-text-200 outline-none" + className="w-full rounded-md bg-transparent py-1.5 pr-2 text-sm md:text-xs font-medium leading-5 text-custom-text-200 outline-none" /> ); @@ -221,7 +221,7 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { >
@@ -230,7 +230,7 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { {!isOpen && (
diff --git a/web/components/issues/issue-layouts/calendar/week-days.tsx b/web/components/issues/issue-layouts/calendar/week-days.tsx index ec1d12e59..968ae4097 100644 --- a/web/components/issues/issue-layouts/calendar/week-days.tsx +++ b/web/components/issues/issue-layouts/calendar/week-days.tsx @@ -28,6 +28,8 @@ type Props = { addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; readOnly?: boolean; + selectedDate: Date; + setSelectedDate: (date: Date) => void; }; export const CalendarWeekDays: React.FC = observer((props) => { @@ -43,6 +45,8 @@ export const CalendarWeekDays: React.FC = observer((props) => { addIssuesToView, viewId, readOnly = false, + selectedDate, + setSelectedDate, } = props; const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month"; @@ -52,7 +56,7 @@ export const CalendarWeekDays: React.FC = observer((props) => { return (
@@ -61,6 +65,8 @@ export const CalendarWeekDays: React.FC = observer((props) => { return ( = observer((props) => { return (
@@ -24,7 +24,7 @@ export const CalendarWeekHeader: React.FC = observer((props) => { if (!showWeekends && (day.shortTitle === "Sat" || day.shortTitle === "Sun")) return null; return ( -
+
{day.shortTitle}
); From 0759666b75944a2cd75c78dff13cd5c303813b08 Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Mon, 18 Mar 2024 12:51:35 +0530 Subject: [PATCH 22/22] [WEB-755] fix: Clearing nodes to default node i.e. paragraph before converting it to other type (#3974) * fix: clearing nodes to default node i.e paragraph before converting it to other types of nodes For more reference on what this does, please refer https://tiptap.dev/docs/editor/api/commands/clear-nodes * chore: clearNodes after delete in case of selections being present * fix: hiding link selector in the bubble menu if inline code block is selected --- .../editor/core/src/lib/editor-commands.ts | 44 +++++++++---------- .../src/extensions/slash-commands.tsx | 5 ++- .../src/ui/menus/bubble-menu/index.tsx | 33 ++++++++------ .../ui/menus/bubble-menu/link-selector.tsx | 2 +- .../ui/menus/bubble-menu/node-selector.tsx | 2 +- 5 files changed, 48 insertions(+), 38 deletions(-) diff --git a/packages/editor/core/src/lib/editor-commands.ts b/packages/editor/core/src/lib/editor-commands.ts index 6524d1ff5..7c3e7f11e 100644 --- a/packages/editor/core/src/lib/editor-commands.ts +++ b/packages/editor/core/src/lib/editor-commands.ts @@ -4,18 +4,18 @@ import { findTableAncestor } from "src/lib/utils"; import { UploadImage } from "src/types/upload-image"; export const toggleHeadingOne = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run(); - else editor.chain().focus().toggleHeading({ level: 1 }).run(); + if (range) editor.chain().focus().deleteRange(range).clearNodes().setNode("heading", { level: 1 }).run(); + else editor.chain().focus().clearNodes().toggleHeading({ level: 1 }).run(); }; export const toggleHeadingTwo = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run(); - else editor.chain().focus().toggleHeading({ level: 2 }).run(); + if (range) editor.chain().focus().deleteRange(range).clearNodes().setNode("heading", { level: 2 }).run(); + else editor.chain().focus().clearNodes().toggleHeading({ level: 2 }).run(); }; export const toggleHeadingThree = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run(); - else editor.chain().focus().toggleHeading({ level: 3 }).run(); + if (range) editor.chain().focus().deleteRange(range).clearNodes().setNode("heading", { level: 3 }).run(); + else editor.chain().focus().clearNodes().toggleHeading({ level: 3 }).run(); }; export const toggleBold = (editor: Editor, range?: Range) => { @@ -37,10 +37,10 @@ export const toggleCodeBlock = (editor: Editor, range?: Range) => { // Check if code block is active then toggle code block if (editor.isActive("codeBlock")) { if (range) { - editor.chain().focus().deleteRange(range).toggleCodeBlock().run(); + editor.chain().focus().deleteRange(range).clearNodes().toggleCodeBlock().run(); return; } - editor.chain().focus().toggleCodeBlock().run(); + editor.chain().focus().clearNodes().toggleCodeBlock().run(); return; } @@ -49,32 +49,32 @@ export const toggleCodeBlock = (editor: Editor, range?: Range) => { if (isSelectionEmpty) { if (range) { - editor.chain().focus().deleteRange(range).toggleCodeBlock().run(); + editor.chain().focus().deleteRange(range).clearNodes().toggleCodeBlock().run(); return; } - editor.chain().focus().toggleCodeBlock().run(); + editor.chain().focus().clearNodes().toggleCodeBlock().run(); } else { if (range) { - editor.chain().focus().deleteRange(range).toggleCode().run(); + editor.chain().focus().deleteRange(range).clearNodes().toggleCode().run(); return; } - editor.chain().focus().toggleCode().run(); + editor.chain().focus().clearNodes().toggleCode().run(); } }; export const toggleOrderedList = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).toggleOrderedList().run(); - else editor.chain().focus().toggleOrderedList().run(); + if (range) editor.chain().focus().deleteRange(range).clearNodes().toggleOrderedList().run(); + else editor.chain().focus().clearNodes().toggleOrderedList().run(); }; export const toggleBulletList = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).toggleBulletList().run(); - else editor.chain().focus().toggleBulletList().run(); + if (range) editor.chain().focus().deleteRange(range).clearNodes().toggleBulletList().run(); + else editor.chain().focus().clearNodes().toggleBulletList().run(); }; export const toggleTaskList = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).toggleTaskList().run(); - else editor.chain().focus().toggleTaskList().run(); + if (range) editor.chain().focus().deleteRange(range).clearNodes().toggleTaskList().run(); + else editor.chain().focus().clearNodes().toggleTaskList().run(); }; export const toggleStrike = (editor: Editor, range?: Range) => { @@ -83,8 +83,8 @@ export const toggleStrike = (editor: Editor, range?: Range) => { }; export const toggleBlockquote = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).toggleBlockquote().run(); - else editor.chain().focus().toggleBlockquote().run(); + if (range) editor.chain().focus().deleteRange(range).clearNodes().toggleBlockquote().run(); + else editor.chain().focus().clearNodes().toggleBlockquote().run(); }; export const insertTableCommand = (editor: Editor, range?: Range) => { @@ -97,8 +97,8 @@ export const insertTableCommand = (editor: Editor, range?: Range) => { } } } - if (range) editor.chain().focus().deleteRange(range).insertTable({ rows: 3, cols: 3 }).run(); - else editor.chain().focus().insertTable({ rows: 3, cols: 3 }).run(); + if (range) editor.chain().focus().deleteRange(range).clearNodes().insertTable({ rows: 3, cols: 3 }).run(); + else editor.chain().focus().clearNodes().insertTable({ rows: 3, cols: 3 }).run(); }; export const unsetLinkEditor = (editor: Editor) => { diff --git a/packages/editor/extensions/src/extensions/slash-commands.tsx b/packages/editor/extensions/src/extensions/slash-commands.tsx index 88e257cef..f37d18c68 100644 --- a/packages/editor/extensions/src/extensions/slash-commands.tsx +++ b/packages/editor/extensions/src/extensions/slash-commands.tsx @@ -85,7 +85,10 @@ const getSuggestionItems = searchTerms: ["p", "paragraph"], icon: , command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run(); + if (range) { + editor.chain().focus().deleteRange(range).clearNodes().run(); + } + editor.chain().focus().clearNodes().run(); }, }, { diff --git a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/index.tsx b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/index.tsx index f96e7293e..2dbc86cec 100644 --- a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/index.tsx +++ b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/index.tsx @@ -25,16 +25,20 @@ type EditorBubbleMenuProps = Omit; export const EditorBubbleMenu: FC = (props: any) => { const items: BubbleMenuItem[] = [ - BoldItem(props.editor), - ItalicItem(props.editor), - UnderLineItem(props.editor), - StrikeThroughItem(props.editor), + ...(props.editor.isActive("code") + ? [] + : [ + BoldItem(props.editor), + ItalicItem(props.editor), + UnderLineItem(props.editor), + StrikeThroughItem(props.editor), + ]), CodeItem(props.editor), ]; const bubbleMenuProps: EditorBubbleMenuProps = { ...props, - shouldShow: ({ view, state, editor }) => { + shouldShow: ({ state, editor }) => { const { selection } = state; const { empty } = selection; @@ -64,6 +68,7 @@ export const EditorBubbleMenu: FC = (props: any) => { const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false); const [isSelecting, setIsSelecting] = useState(false); + useEffect(() => { function handleMouseDown() { function handleMouseMove() { @@ -108,14 +113,16 @@ export const EditorBubbleMenu: FC = (props: any) => { }} /> )} - { - setIsLinkSelectorOpen(!isLinkSelectorOpen); - setIsNodeSelectorOpen(false); - }} - /> + {!props.editor.isActive("code") && ( + { + setIsLinkSelectorOpen(!isLinkSelectorOpen); + setIsNodeSelectorOpen(false); + }} + /> + )}
{items.map((item) => (