From 1aadbee7e2f9eef12875c7ab96096d2581651557 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Fri, 22 Sep 2023 17:43:23 +0530 Subject: [PATCH 01/35] fix: resolved pending issue graph in analytics, user wishes in dashboard, and typo in projects list (#2247) --- .../analytics/scope-and-demand/scope.tsx | 19 +++++----- .../project/single-project-card.tsx | 2 +- web/pages/[workspaceSlug]/index.tsx | 37 ++++++++++--------- 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/web/components/analytics/scope-and-demand/scope.tsx b/web/components/analytics/scope-and-demand/scope.tsx index b01354b93..9231947bd 100644 --- a/web/components/analytics/scope-and-demand/scope.tsx +++ b/web/components/analytics/scope-and-demand/scope.tsx @@ -15,17 +15,19 @@ export const AnalyticsScope: React.FC = ({ defaultAnalytics }) => (
Pending issues
- {defaultAnalytics.pending_issue_user.length > 0 ? ( + {defaultAnalytics.pending_issue_user && defaultAnalytics.pending_issue_user.length > 0 ? ( `#f97316`} - customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) => d.count)} + customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) => + d.count > 0 ? d.count : 50 + )} tooltip={(datum) => { const assignee = defaultAnalytics.pending_issue_user.find( - (a) => a.assignees__display_name === `${datum.indexValue}` + (a) => a.assignees__id === `${datum.indexValue}` ); return ( @@ -39,10 +41,9 @@ export const AnalyticsScope: React.FC = ({ defaultAnalytics }) => ( }} axisBottom={{ renderTick: (datum) => { - const avatar = - defaultAnalytics.pending_issue_user[datum.tickIndex]?.assignees__avatar ?? ""; + const assignee = defaultAnalytics.pending_issue_user[datum.tickIndex] ?? ""; - if (avatar && avatar !== "") + if (assignee && assignee?.assignees__avatar && assignee?.assignees__avatar !== "") return ( = ({ defaultAnalytics }) => ( y={10} width={16} height={16} - xlinkHref={avatar} + xlinkHref={assignee?.assignees__avatar} style={{ clipPath: "circle(50%)" }} /> @@ -60,7 +61,7 @@ export const AnalyticsScope: React.FC = ({ defaultAnalytics }) => ( - {datum.value ? `${datum.value}`.toUpperCase()[0] : "?"} + {datum.value ? `${assignee.assignees__display_name}`.toUpperCase()[0] : "?"} ); diff --git a/web/components/project/single-project-card.tsx b/web/components/project/single-project-card.tsx index 547211c53..eb88e7381 100644 --- a/web/components/project/single-project-card.tsx +++ b/web/components/project/single-project-card.tsx @@ -149,7 +149,7 @@ export const SingleProjectCard: React.FC = ({ ) : ( - Member + Joined )} {project.is_favorite && ( diff --git a/web/pages/[workspaceSlug]/index.tsx b/web/pages/[workspaceSlug]/index.tsx index df1a69865..79e9571b7 100644 --- a/web/pages/[workspaceSlug]/index.tsx +++ b/web/pages/[workspaceSlug]/index.tsx @@ -127,9 +127,21 @@ const WorkspacePage: NextPage = () => { />
)} - {projects ? ( - projects.length > 0 ? ( -
+
+
+

+ Good {greeting}, {user?.first_name} {user?.last_name} +

+
+
{greeting === "morning" ? "🌤️" : greeting === "afternoon" ? "🌥️" : "🌙️"}
+
+ {DAYS[today.getDay()]}, {renderShortDate(today)} {render12HourFormatTime(today)} +
+
+
+ + {projects ? ( + projects.length > 0 ? (
@@ -143,17 +155,8 @@ const WorkspacePage: NextPage = () => { />
-
- ) : ( -
-

- Good {greeting}, {user?.first_name} {user?.last_name} -

-
- {greeting === "morning" ? "🌤️" : greeting === "afternoon" ? "🌥️" : "🌙️"} - {DAYS[today.getDay()]}, {renderShortDate(today)} {render12HourFormatTime(today)} -
-
+ ) : ( +
Create a project

@@ -174,9 +177,9 @@ const WorkspacePage: NextPage = () => { Empty Dashboard

-
- ) - ) : null} + ) + ) : null} +
); }; From c9a6380636d50c18a953a17b033fc87ac1baa3af Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 22 Sep 2023 18:47:10 +0530 Subject: [PATCH 02/35] style: settings page improvement (#2211) * style: settings page improvement * style: toggle switch styling --------- Co-authored-by: Anmol Singh Bhatia --- web/components/ui/toggle-switch.tsx | 2 +- web/pages/[workspaceSlug]/me/profile/activity.tsx | 2 +- web/pages/[workspaceSlug]/me/profile/preferences.tsx | 2 +- web/pages/[workspaceSlug]/settings/billing.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/web/components/ui/toggle-switch.tsx b/web/components/ui/toggle-switch.tsx index e52ff26c9..d6c512ad7 100644 --- a/web/components/ui/toggle-switch.tsx +++ b/web/components/ui/toggle-switch.tsx @@ -35,7 +35,7 @@ export const ToggleSwitch: React.FC = (props) => { : size === "md" ? "translate-x-4" : "translate-x-5") + " bg-white" - : "translate-x-1 bg-custom-background-90" + : "translate-x-0.5 bg-custom-background-90" }`} /> diff --git a/web/pages/[workspaceSlug]/me/profile/activity.tsx b/web/pages/[workspaceSlug]/me/profile/activity.tsx index ee527829b..d8588390e 100644 --- a/web/pages/[workspaceSlug]/me/profile/activity.tsx +++ b/web/pages/[workspaceSlug]/me/profile/activity.tsx @@ -46,7 +46,7 @@ const ProfileActivity = () => { {userActivity ? (
-

Acitivity

+

Activity

    diff --git a/web/pages/[workspaceSlug]/me/profile/preferences.tsx b/web/pages/[workspaceSlug]/me/profile/preferences.tsx index b1b16a3d4..eb6a3c821 100644 --- a/web/pages/[workspaceSlug]/me/profile/preferences.tsx +++ b/web/pages/[workspaceSlug]/me/profile/preferences.tsx @@ -66,7 +66,7 @@ const ProfilePreferences = observer(() => {
    -

    Acitivity

    +

    Preferences

    diff --git a/web/pages/[workspaceSlug]/settings/billing.tsx b/web/pages/[workspaceSlug]/settings/billing.tsx index 898b716bf..eeb95a5a3 100644 --- a/web/pages/[workspaceSlug]/settings/billing.tsx +++ b/web/pages/[workspaceSlug]/settings/billing.tsx @@ -50,7 +50,7 @@ const BillingSettings: NextPage = () => {
    -

    Billing & Plan

    +

    Billing & Plans

    From e8d303dd101ae1b42d5b9594629d26bc13183185 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Fri, 22 Sep 2023 19:48:07 +0530 Subject: [PATCH 03/35] chore: changed priority props in workspace and project (#2253) --- apiserver/plane/db/models/project.py | 2 +- apiserver/plane/db/models/workspace.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index 08d825b59..4cd2134ac 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -26,7 +26,7 @@ ROLE_CHOICES = ( def get_default_props(): return { "filters": { - "priority": "none", + "priority": None, "state": None, "state_group": None, "assignees": None, diff --git a/apiserver/plane/db/models/workspace.py b/apiserver/plane/db/models/workspace.py index e063d873a..c85268435 100644 --- a/apiserver/plane/db/models/workspace.py +++ b/apiserver/plane/db/models/workspace.py @@ -17,7 +17,7 @@ ROLE_CHOICES = ( def get_default_props(): return { "filters": { - "priority": "none", + "priority": None, "state": None, "state_group": None, "assignees": None, From 68c8741f93dffd402d830836a77491a0d1c8125e Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 25 Sep 2023 12:18:35 +0530 Subject: [PATCH 04/35] fix: bug fix related to fetching dropdown options for the profile issue (#2246) --- .../core/views/board-view/single-board.tsx | 1 + .../core/views/board-view/single-issue.tsx | 7 ++++++- .../core/views/calendar-view/single-date.tsx | 1 + .../core/views/calendar-view/single-issue.tsx | 7 ++++++- .../core/views/list-view/single-issue.tsx | 7 ++++++- .../core/views/list-view/single-list.tsx | 1 + .../views/spreadsheet-view/single-issue.tsx | 17 ++++++++--------- .../spreadsheet-view/spreadsheet-issues.tsx | 1 + web/components/project/label-select.tsx | 10 ++++++---- web/components/project/members-select.tsx | 6 ++++-- web/components/states/state-select.tsx | 8 +++++--- 11 files changed, 45 insertions(+), 21 deletions(-) diff --git a/web/components/core/views/board-view/single-board.tsx b/web/components/core/views/board-view/single-board.tsx index 3e174ead2..4226c3091 100644 --- a/web/components/core/views/board-view/single-board.tsx +++ b/web/components/core/views/board-view/single-board.tsx @@ -157,6 +157,7 @@ export const SingleBoard: React.FC = (props) => { type={type} index={index} issue={issue} + projectId={issue.project_detail.id} groupTitle={groupTitle} editIssue={() => handleIssueAction(issue, "edit")} makeIssueCopy={() => handleIssueAction(issue, "copy")} diff --git a/web/components/core/views/board-view/single-issue.tsx b/web/components/core/views/board-view/single-issue.tsx index 0e27f895b..f7d545eb5 100644 --- a/web/components/core/views/board-view/single-issue.tsx +++ b/web/components/core/views/board-view/single-issue.tsx @@ -56,6 +56,7 @@ type Props = { provided: DraggableProvided; snapshot: DraggableStateSnapshot; issue: IIssue; + projectId: string; groupTitle?: string; index: number; editIssue: () => void; @@ -77,6 +78,7 @@ export const SingleBoardIssue: React.FC = ({ provided, snapshot, issue, + projectId, index, editIssue, makeIssueCopy, @@ -104,7 +106,7 @@ export const SingleBoardIssue: React.FC = ({ const { displayFilters, properties, mutateIssues } = viewProps; const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + const { workspaceSlug, cycleId, moduleId } = router.query; const isDraftIssue = router.pathname.includes("draft-issues"); @@ -452,6 +454,7 @@ export const SingleBoardIssue: React.FC = ({ @@ -479,6 +482,7 @@ export const SingleBoardIssue: React.FC = ({ {properties.labels && issue.labels.length > 0 && ( = ({ {properties.assignee && ( = (props) => { provided={provided} snapshot={snapshot} issue={issue} + projectId={issue.project_detail.id} handleEditIssue={() => handleIssueAction(issue, "edit")} handleDeleteIssue={() => handleIssueAction(issue, "delete")} user={user} diff --git a/web/components/core/views/calendar-view/single-issue.tsx b/web/components/core/views/calendar-view/single-issue.tsx index 81d6f631f..e0e9aa2a5 100644 --- a/web/components/core/views/calendar-view/single-issue.tsx +++ b/web/components/core/views/calendar-view/single-issue.tsx @@ -41,6 +41,7 @@ type Props = { provided: DraggableProvided; snapshot: DraggableStateSnapshot; issue: IIssue; + projectId: string; user: ICurrentUserResponse | undefined; isNotAllowed: boolean; }; @@ -52,11 +53,12 @@ export const SingleCalendarIssue: React.FC = ({ provided, snapshot, issue, + projectId, user, isNotAllowed, }) => { const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; + const { workspaceSlug, cycleId, moduleId, viewId } = router.query; const { setToastAlert } = useToast(); @@ -310,6 +312,7 @@ export const SingleCalendarIssue: React.FC = ({ {properties.state && ( = ({ {properties.labels && issue.labels.length > 0 && ( = ({ {properties.assignee && ( void; index: number; @@ -69,6 +70,7 @@ type Props = { export const SingleListIssue: React.FC = ({ type, issue, + projectId, editIssue, index, makeIssueCopy, @@ -88,7 +90,7 @@ export const SingleListIssue: React.FC = ({ const [contextMenuPosition, setContextMenuPosition] = useState(null); const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId, userId } = router.query; + const { workspaceSlug, cycleId, moduleId, userId } = router.query; const isArchivedIssues = router.pathname.includes("archived-issues"); const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues"; @@ -376,6 +378,7 @@ export const SingleListIssue: React.FC = ({ {properties.state && ( = ({ {properties.labels && ( = ({ {properties.assignee && ( = (props) => { key={issue.id} type={type} issue={issue} + projectId={issue.project_detail.id} groupTitle={groupTitle} index={index} editIssue={() => handleIssueAction(issue, "edit")} diff --git a/web/components/core/views/spreadsheet-view/single-issue.tsx b/web/components/core/views/spreadsheet-view/single-issue.tsx index 7a309f728..32cb4ba77 100644 --- a/web/components/core/views/spreadsheet-view/single-issue.tsx +++ b/web/components/core/views/spreadsheet-view/single-issue.tsx @@ -49,6 +49,7 @@ import { renderLongDetailDateFormat } from "helpers/date-time.helper"; type Props = { issue: IIssue; + projectId: string; index: number; expanded: boolean; handleToggleExpand: (issueId: string) => void; @@ -64,6 +65,7 @@ type Props = { export const SingleSpreadsheetIssue: React.FC = ({ issue, + projectId, index, expanded, handleToggleExpand, @@ -80,7 +82,7 @@ export const SingleSpreadsheetIssue: React.FC = ({ const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; + const { workspaceSlug, cycleId, moduleId, viewId } = router.query; const { params } = useSpreadsheetIssuesView(); @@ -96,7 +98,7 @@ export const SingleSpreadsheetIssue: React.FC = ({ ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params) : viewId ? VIEW_ISSUES(viewId.toString(), params) - : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params); + : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId, params); if (issue.parent) mutate( @@ -136,13 +138,7 @@ export const SingleSpreadsheetIssue: React.FC = ({ ); issuesService - .patchIssue( - workspaceSlug as string, - projectId as string, - issue.id as string, - formData, - user - ) + .patchIssue(workspaceSlug as string, projectId, issue.id as string, formData, user) .then(() => { if (issue.parent) { mutate(SUB_ISSUES(issue.parent as string)); @@ -368,6 +364,7 @@ export const SingleSpreadsheetIssue: React.FC = ({
    = ({
    = ({
    = ({
    void; labelsDetails: any[]; className?: string; @@ -37,6 +38,7 @@ type Props = { export const LabelSelect: React.FC = ({ value, + projectId, onChange, labelsDetails, className = "", @@ -54,15 +56,15 @@ export const LabelSelect: React.FC = ({ const [labelModal, setLabelModal] = useState(false); const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug } = router.query; const dropdownBtn = useRef(null); const dropdownOptions = useRef(null); const { data: issueLabels } = useSWR( - projectId && fetchStates ? PROJECT_ISSUE_LABELS(projectId.toString()) : null, + projectId && fetchStates ? PROJECT_ISSUE_LABELS(projectId) : null, workspaceSlug && projectId && fetchStates - ? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string) + ? () => issuesService.getIssueLabels(workspaceSlug as string, projectId) : null ); @@ -150,7 +152,7 @@ export const LabelSelect: React.FC = ({ setLabelModal(false)} - projectId={projectId.toString()} + projectId={projectId} user={user} /> )} diff --git a/web/components/project/members-select.tsx b/web/components/project/members-select.tsx index 4ffad72b9..57523df8d 100644 --- a/web/components/project/members-select.tsx +++ b/web/components/project/members-select.tsx @@ -18,6 +18,7 @@ import { IUser } from "types"; type Props = { value: string | string[]; + projectId: string; onChange: (data: any) => void; membersDetails: IUser[]; renderWorkspaceMembers?: boolean; @@ -30,6 +31,7 @@ type Props = { export const MembersSelect: React.FC = ({ value, + projectId, onChange, membersDetails, renderWorkspaceMembers = false, @@ -44,14 +46,14 @@ export const MembersSelect: React.FC = ({ const [fetchStates, setFetchStates] = useState(false); const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug } = router.query; const dropdownBtn = useRef(null); const dropdownOptions = useRef(null); const { members } = useProjectMembers( workspaceSlug?.toString(), - projectId?.toString(), + projectId, fetchStates && !renderWorkspaceMembers ); diff --git a/web/components/states/state-select.tsx b/web/components/states/state-select.tsx index ed37e97b5..9f6b40d04 100644 --- a/web/components/states/state-select.tsx +++ b/web/components/states/state-select.tsx @@ -25,6 +25,7 @@ import { getStatesList } from "helpers/state.helper"; type Props = { value: IState; onChange: (data: any, states: IState[] | undefined) => void; + projectId: string; className?: string; buttonClassName?: string; optionsClassName?: string; @@ -35,6 +36,7 @@ type Props = { export const StateSelect: React.FC = ({ value, onChange, + projectId, className = "", buttonClassName = "", optionsClassName = "", @@ -50,12 +52,12 @@ export const StateSelect: React.FC = ({ const [fetchStates, setFetchStates] = useState(false); const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug } = router.query; const { data: stateGroups } = useSWR( - workspaceSlug && projectId && fetchStates ? STATES_LIST(projectId as string) : null, + workspaceSlug && projectId && fetchStates ? STATES_LIST(projectId) : null, workspaceSlug && projectId && fetchStates - ? () => stateService.getStates(workspaceSlug as string, projectId as string) + ? () => stateService.getStates(workspaceSlug as string, projectId) : null ); From afa10d7195224a65de9411d3a0ffb9eabddfe135 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 25 Sep 2023 13:18:03 +0530 Subject: [PATCH 05/35] fix: sub issue state and member select build error (#2254) --- web/components/issues/sub-issues/properties.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/components/issues/sub-issues/properties.tsx b/web/components/issues/sub-issues/properties.tsx index 4f2dca43b..e2caefffa 100644 --- a/web/components/issues/sub-issues/properties.tsx +++ b/web/components/issues/sub-issues/properties.tsx @@ -161,6 +161,7 @@ export const IssueProperty: React.FC = ({
    = ({
    Date: Mon, 25 Sep 2023 13:38:49 +0530 Subject: [PATCH 06/35] rename view to layout (#2255) Co-authored-by: Your Name --- web/components/core/filters/issues-view-filter.tsx | 2 +- web/components/issues/my-issues/my-issues-view-options.tsx | 2 +- web/components/profile/profile-issues-view-options.tsx | 2 +- .../[workspaceSlug]/projects/[projectId]/modules/index.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/web/components/core/filters/issues-view-filter.tsx b/web/components/core/filters/issues-view-filter.tsx index afb7eb2b0..f5b0f477a 100644 --- a/web/components/core/filters/issues-view-filter.tsx +++ b/web/components/core/filters/issues-view-filter.tsx @@ -93,7 +93,7 @@ export const IssuesFilterView: React.FC = () => { {replaceUnderscoreIfSnakeCase(option.type)} View + {replaceUnderscoreIfSnakeCase(option.type)} Layout } position="bottom" > diff --git a/web/components/issues/my-issues/my-issues-view-options.tsx b/web/components/issues/my-issues/my-issues-view-options.tsx index 90d9b8971..549a00df2 100644 --- a/web/components/issues/my-issues/my-issues-view-options.tsx +++ b/web/components/issues/my-issues/my-issues-view-options.tsx @@ -49,7 +49,7 @@ export const MyIssuesViewOptions: React.FC = () => { {replaceUnderscoreIfSnakeCase(option.type)} View + {replaceUnderscoreIfSnakeCase(option.type)} Layout } position="bottom" > diff --git a/web/components/profile/profile-issues-view-options.tsx b/web/components/profile/profile-issues-view-options.tsx index 4f36faa04..30dcb8df4 100644 --- a/web/components/profile/profile-issues-view-options.tsx +++ b/web/components/profile/profile-issues-view-options.tsx @@ -81,7 +81,7 @@ export const ProfileIssuesViewOptions: React.FC = () => { {replaceUnderscoreIfSnakeCase(option.type)} View + {replaceUnderscoreIfSnakeCase(option.type)} Layout } position="bottom" > diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx index 6e7e1bca4..028e95540 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx @@ -96,7 +96,7 @@ const ProjectModules: NextPage = () => { {replaceUnderscoreIfSnakeCase(option.type)} View + {replaceUnderscoreIfSnakeCase(option.type)} Layout } position="bottom" > From de7a672b799717dba522c6e9285bc8d95e8024a8 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 25 Sep 2023 16:15:49 +0530 Subject: [PATCH 07/35] fix: bug fixes and ui improvement (#2250) --- web/components/cycles/single-cycle-list.tsx | 6 +- web/components/labels/single-label.tsx | 78 +++++++++++-------- .../[workspaceSlug]/me/profile/index.tsx | 2 + web/pages/[workspaceSlug]/settings/index.tsx | 10 +-- 4 files changed, 57 insertions(+), 39 deletions(-) diff --git a/web/components/cycles/single-cycle-list.tsx b/web/components/cycles/single-cycle-list.tsx index ec01da9e7..a4c21128a 100644 --- a/web/components/cycles/single-cycle-list.tsx +++ b/web/components/cycles/single-cycle-list.tsx @@ -149,6 +149,10 @@ export const SingleCycleList: React.FC = ({ color: group.color, })); + const completedIssues = cycle.completed_issues + cycle.cancelled_issues; + + const percentage = cycle.total_issues > 0 ? (completedIssues / cycle.total_issues) * 100 : 0; + return (
    @@ -307,7 +311,7 @@ export const SingleCycleList: React.FC = ({ ) : cycleStatus === "completed" ? ( - {100} % + {Math.round(percentage)} % ) : ( diff --git a/web/components/labels/single-label.tsx b/web/components/labels/single-label.tsx index be981e510..c163a3735 100644 --- a/web/components/labels/single-label.tsx +++ b/web/components/labels/single-label.tsx @@ -1,11 +1,13 @@ -import React from "react"; +import React, { useRef, useState } from "react"; +//hook +import useOutsideClickDetector from "hooks/use-outside-click-detector"; // ui import { CustomMenu } from "components/ui"; // types import { IIssueLabels } from "types"; //icons -import { RectangleGroupIcon, PencilIcon } from "@heroicons/react/24/outline"; +import { PencilIcon } from "@heroicons/react/24/outline"; import { Component, X } from "lucide-react"; type Props = { @@ -20,9 +22,14 @@ export const SingleLabel: React.FC = ({ addLabelToGroup, editLabel, handleLabelDelete, -}) => ( -
    -
    +}) => { + const [isMenuActive, setIsMenuActive] = useState(false); + const actionSectionRef = useRef(null); + + useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false)); + + return ( +
    = ({ />
    {label.name}
    -
    -
    - - -
    - } +
    + setIsMenuActive(!isMenuActive)}> + +
    + } + > + addLabelToGroup(label)}> + + + Convert to group + + + editLabel(label)}> + + + Edit label + + + +
    +
    - -
    -
    -
    -); + ); +}; diff --git a/web/pages/[workspaceSlug]/me/profile/index.tsx b/web/pages/[workspaceSlug]/me/profile/index.tsx index c536c384a..40ba8ecf3 100644 --- a/web/pages/[workspaceSlug]/me/profile/index.tsx +++ b/web/pages/[workspaceSlug]/me/profile/index.tsx @@ -253,6 +253,7 @@ const Profile: NextPage = () => { placeholder="Enter your first name" className="!px-3 !py-2 rounded-md font-medium" autoComplete="off" + maxLength={24} />
    @@ -266,6 +267,7 @@ const Profile: NextPage = () => { placeholder="Enter your last name" autoComplete="off" className="!px-3 !py-2 rounded-md font-medium" + maxLength={24} />
    diff --git a/web/pages/[workspaceSlug]/settings/index.tsx b/web/pages/[workspaceSlug]/settings/index.tsx index 4f576aa0a..7d0d8b1b6 100644 --- a/web/pages/[workspaceSlug]/settings/index.tsx +++ b/web/pages/[workspaceSlug]/settings/index.tsx @@ -337,10 +337,10 @@ const WorkspaceSettings: NextPage = () => {
    - The danger zone of the project delete page is a critical area that - requires careful consideration and attention. When deleting a project, all - of the data and resources within that project will be permanently removed - and cannot be recovered. + The danger zone of the workspace delete page is a critical area that + requires careful consideration and attention. When deleting a workspace, + all of the data and resources within that workspace will be permanently + removed and cannot be recovered.
    { className="!text-sm" outline > - Delete my project + Delete my workspace
    From 5e8d523ed40b7cff5d78dc37d33fee260a5370cb Mon Sep 17 00:00:00 2001 From: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Date: Mon, 25 Sep 2023 19:08:26 +0530 Subject: [PATCH 08/35] feat: quick-add placement in spreadsheet and gantt (#2259) * feat: sticking quick-add at the bottom of the screen fix: opening create issue modal instead of quick-add in draft-issues, my-issue and profile page * fix: build error due to dynamic import --- .../core/views/board-view/single-board.tsx | 10 ++- .../views/inline-issue-create-wrapper.tsx | 9 -- .../list-view/inline-create-issue-form.tsx | 19 ++-- .../core/views/list-view/single-list.tsx | 15 +++- .../spreadsheet-view/spreadsheet-view.tsx | 86 +++++++++---------- web/components/gantt-chart/sidebar.tsx | 45 +++++----- .../profile/profile-issues-view.tsx | 3 +- web/layouts/app-layout/app-sidebar.tsx | 13 ++- .../projects/[projectId]/cycles/[cycleId].tsx | 10 ++- .../[projectId]/modules/[moduleId].tsx | 6 +- 10 files changed, 121 insertions(+), 95 deletions(-) diff --git a/web/components/core/views/board-view/single-board.tsx b/web/components/core/views/board-view/single-board.tsx index 4226c3091..8f851527d 100644 --- a/web/components/core/views/board-view/single-board.tsx +++ b/web/components/core/views/board-view/single-board.tsx @@ -63,6 +63,10 @@ export const SingleBoard: React.FC = (props) => { const router = useRouter(); const { cycleId, moduleId } = router.query; + const isMyIssuesPage = router.pathname.split("/")[3] === "my-issues"; + const isProfileIssuesPage = router.pathname.split("/")[2] === "profile"; + const isDraftIssuesPage = router.pathname.split("/")[4] === "draft-issues"; + const type = cycleId ? "cycle" : moduleId ? "module" : "issue"; // Check if it has at least 4 tickets since it is enough to accommodate the Calendar height @@ -214,7 +218,11 @@ export const SingleBoard: React.FC = (props) => {
    {spreadsheetIssues ? ( -
    +
    {spreadsheetIssues.map((issue: IIssue, index) => ( = ({ /> ))} - setIsInlineCreateIssueFormOpen(false)} - prePopulatedData={{ - ...(cycleId && { cycle: cycleId.toString() }), - ...(moduleId && { module: moduleId.toString() }), - }} - /> +
    + setIsInlineCreateIssueFormOpen(false)} + prePopulatedData={{ + ...(cycleId && { cycle: cycleId.toString() }), + ...(moduleId && { module: moduleId.toString() }), + }} + /> -
    - {!isInlineCreateIssueFormOpen && ( - <> - {type === "issue" ? ( + {type === "issue" + ? !disableUserActions && + !isInlineCreateIssueFormOpen && ( - ) : ( - !disableUserActions && ( - - - Add Issue - - } - position="left" - optionsClassName="left-5 !w-36" - noBorder - > - setIsInlineCreateIssueFormOpen(true)}> - Create new + ) + : !disableUserActions && + !isInlineCreateIssueFormOpen && ( + + + Add Issue + + } + position="left" + verticalPosition="top" + optionsClassName="left-5 !w-36" + noBorder + > + setIsInlineCreateIssueFormOpen(true)}> + Create new + + {openIssuesListModal && ( + + Add an existing issue - {openIssuesListModal && ( - - Add an existing issue - - )} - - ) + )} + )} - - )}
    ) : ( diff --git a/web/components/gantt-chart/sidebar.tsx b/web/components/gantt-chart/sidebar.tsx index 0d90ffdd0..35b253ef9 100644 --- a/web/components/gantt-chart/sidebar.tsx +++ b/web/components/gantt-chart/sidebar.tsx @@ -155,31 +155,32 @@ export const GanttSidebar: React.FC = (props) => { )} {droppableProvided.placeholder} - - setIsCreateIssueFormOpen(false)} - prePopulatedData={{ - start_date: new Date(Date.now()).toISOString().split("T")[0], - target_date: new Date(Date.now() + 86400000).toISOString().split("T")[0], - ...(cycleId && { cycle: cycleId.toString() }), - ...(moduleId && { module: moduleId.toString() }), - }} - /> - - {!isCreateIssueFormOpen && ( - - )}
    )} +
    + setIsCreateIssueFormOpen(false)} + prePopulatedData={{ + start_date: new Date(Date.now()).toISOString().split("T")[0], + target_date: new Date(Date.now() + 86400000).toISOString().split("T")[0], + ...(cycleId && { cycle: cycleId.toString() }), + ...(moduleId && { module: moduleId.toString() }), + }} + /> + + {!isCreateIssueFormOpen && ( + + )} +
    ); }; diff --git a/web/components/profile/profile-issues-view.tsx b/web/components/profile/profile-issues-view.tsx index 035e8b990..9d6a53ffe 100644 --- a/web/components/profile/profile-issues-view.tsx +++ b/web/components/profile/profile-issues-view.tsx @@ -227,7 +227,8 @@ export const ProfileIssuesView = () => { router.pathname.includes("my-issues")) ?? false; - const disableAddIssueOption = isSubscribedIssuesRoute || isMySubscribedIssues; + const disableAddIssueOption = + isSubscribedIssuesRoute || isMySubscribedIssues || user?.id !== userId; return ( <> diff --git a/web/layouts/app-layout/app-sidebar.tsx b/web/layouts/app-layout/app-sidebar.tsx index 03ac72387..19878458a 100644 --- a/web/layouts/app-layout/app-sidebar.tsx +++ b/web/layouts/app-layout/app-sidebar.tsx @@ -1,3 +1,4 @@ +import dynamic from "next/dynamic"; // hooks import useTheme from "hooks/use-theme"; // components @@ -5,8 +6,18 @@ import { WorkspaceHelpSection, WorkspaceSidebarDropdown, WorkspaceSidebarMenu, - WorkspaceSidebarQuickAction, } from "components/workspace"; + +const WorkspaceSidebarQuickAction = dynamic<{}>( + () => + import("components/workspace/sidebar-quick-action").then( + (mod) => mod.WorkspaceSidebarQuickAction + ), + { + ssr: false, + } +); + import { ProjectSidebarList } from "components/project"; import { PublishProjectModal } from "components/project/publish-project/modal"; import { ConfirmProjectLeaveModal } from "components/project/confirm-project-leave-modal"; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx index de6ad561e..f29629384 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx @@ -189,10 +189,12 @@ const SingleCycle: React.FC = () => { {cycleStatus === "completed" && ( setTransferIssuesModal(true)} /> )} - +
    + +
    { onClose={() => setAnalyticsModal(false)} />
    From 7db78594dc4fa7ff205f08960ad81db59d43a0ef Mon Sep 17 00:00:00 2001 From: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Date: Mon, 25 Sep 2023 19:11:10 +0530 Subject: [PATCH 09/35] fix: draft issue delete not working (#2249) * fix: draft issue not deleting, project can't be changed in draft issue modal * fix: removed mutation for view where draft issues are not shown * fix: inline create issue for draft issue * fix: clearing data from localstorage on discard click --- .../core/views/inline-issue-create-wrapper.tsx | 16 ++++++++++++++-- .../issues/delete-draft-issue-modal.tsx | 9 ++++++--- web/components/issues/draft-issue-form.tsx | 4 +++- web/components/issues/draft-issue-modal.tsx | 18 +++++++----------- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/web/components/core/views/inline-issue-create-wrapper.tsx b/web/components/core/views/inline-issue-create-wrapper.tsx index 808cca7d8..3c01c50ce 100644 --- a/web/components/core/views/inline-issue-create-wrapper.tsx +++ b/web/components/core/views/inline-issue-create-wrapper.tsx @@ -39,6 +39,7 @@ import { CYCLE_DETAILS, MODULE_DETAILS, PROJECT_ISSUES_LIST_WITH_PARAMS, + PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS, } from "constants/fetch-keys"; // types @@ -119,6 +120,8 @@ export const InlineCreateIssueFormWrapper: React.FC = (props) => { const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; + const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues"; + const { user } = useUser(); const { setToastAlert } = useToast(); @@ -184,8 +187,15 @@ export const InlineCreateIssueFormWrapper: React.FC = (props) => { reset({ ...defaultValues }); - await issuesService - .createIssues(workspaceSlug.toString(), projectId.toString(), formData, user) + await (!isDraftIssues + ? issuesService.createIssues(workspaceSlug.toString(), projectId.toString(), formData, user) + : issuesService.createDraftIssue( + workspaceSlug.toString(), + projectId.toString(), + formData, + user + ) + ) .then(async (res) => { mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params)); if (formData.cycle && formData.cycle !== "") @@ -207,6 +217,8 @@ export const InlineCreateIssueFormWrapper: React.FC = (props) => { params ); + if (isDraftIssues) + mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(projectId.toString() ?? "", params)); if (displayFilters.layout === "calendar") mutate(calendarFetchKey); if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey); if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey); diff --git a/web/components/issues/delete-draft-issue-modal.tsx b/web/components/issues/delete-draft-issue-modal.tsx index 6fc4f4218..8347f555b 100644 --- a/web/components/issues/delete-draft-issue-modal.tsx +++ b/web/components/issues/delete-draft-issue-modal.tsx @@ -4,6 +4,8 @@ import { useRouter } from "next/router"; import { mutate } from "swr"; +import useUser from "hooks/use-user"; + // headless ui import { Dialog, Transition } from "@headlessui/react"; // services @@ -16,7 +18,7 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // ui import { SecondaryButton, DangerButton } from "components/ui"; // types -import type { IIssue, ICurrentUserResponse } from "types"; +import type { IIssue } from "types"; // fetch-keys import { PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS } from "constants/fetch-keys"; @@ -24,12 +26,11 @@ type Props = { isOpen: boolean; handleClose: () => void; data: IIssue | null; - user?: ICurrentUserResponse; onSubmit?: () => Promise | void; }; export const DeleteDraftIssueModal: React.FC = (props) => { - const { isOpen, handleClose, data, user, onSubmit } = props; + const { isOpen, handleClose, data, onSubmit } = props; const [isDeleteLoading, setIsDeleteLoading] = useState(false); @@ -40,6 +41,8 @@ export const DeleteDraftIssueModal: React.FC = (props) => { const { setToastAlert } = useToast(); + const { user } = useUser(); + useEffect(() => { setIsDeleteLoading(false); }, [isOpen]); diff --git a/web/components/issues/draft-issue-form.tsx b/web/components/issues/draft-issue-form.tsx index aac5dede7..7433da82c 100644 --- a/web/components/issues/draft-issue-form.tsx +++ b/web/components/issues/draft-issue-form.tsx @@ -66,6 +66,7 @@ interface IssueFormProps { createMore: boolean; setCreateMore: React.Dispatch>; handleClose: () => void; + handleDiscard: () => void; status: boolean; user: ICurrentUserResponse | undefined; fieldsToShow: ( @@ -97,6 +98,7 @@ export const DraftIssueForm: FC = (props) => { status, user, fieldsToShow, + handleDiscard, } = props; const [stateModal, setStateModal] = useState(false); @@ -569,7 +571,7 @@ export const DraftIssueForm: FC = (props) => { {}} size="md" />
    - Discard + Discard diff --git a/web/components/issues/draft-issue-modal.tsx b/web/components/issues/draft-issue-modal.tsx index c060c72f6..b6479d067 100644 --- a/web/components/issues/draft-issue-modal.tsx +++ b/web/components/issues/draft-issue-modal.tsx @@ -97,6 +97,11 @@ export const CreateUpdateDraftIssueModal: React.FC = (props) = setActiveProject(null); }; + const onDiscard = () => { + clearDraftIssueLocalStorage(); + onClose(); + }; + useEffect(() => { setPreloadedData(prePopulateDataProps ?? {}); @@ -141,7 +146,7 @@ export const CreateUpdateDraftIssueModal: React.FC = (props) = if (prePopulateData && prePopulateData.project && !activeProject) return setActiveProject(prePopulateData.project); - if (prePopulateData && prePopulateData.project) + if (prePopulateData && prePopulateData.project && !activeProject) return setActiveProject(prePopulateData.project); // if data is not present, set active project to the project @@ -180,16 +185,8 @@ export const CreateUpdateDraftIssueModal: React.FC = (props) = await issuesService .createDraftIssue(workspaceSlug as string, activeProject ?? "", payload, user) .then(async () => { - mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); - if (displayFilters.layout === "calendar") mutate(calendarFetchKey); - if (displayFilters.layout === "gantt_chart") - mutate(ganttFetchKey, { - start_target_date: true, - order_by: "sort_order", - }); - if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey); if (groupedIssues) mutateMyIssues(); setToastAlert({ @@ -200,8 +197,6 @@ export const CreateUpdateDraftIssueModal: React.FC = (props) = if (payload.assignees_list?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE(workspaceSlug as string)); - - if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent)); }) .catch(() => { setToastAlert({ @@ -396,6 +391,7 @@ export const CreateUpdateDraftIssueModal: React.FC = (props) = createMore={createMore} setCreateMore={setCreateMore} handleClose={onClose} + handleDiscard={onDiscard} projectId={activeProject ?? ""} setActiveProject={setActiveProject} status={data ? true : false} From 1ad99873a9458c672c807d4c7716623515a30461 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Tue, 26 Sep 2023 13:09:08 +0530 Subject: [PATCH 10/35] feat: Add peek overview in sub issues and updated UI for empty states. (#2263) --- web/components/issues/sub-issues/issue.tsx | 239 ++++++++++-------- .../issues/sub-issues/issues-list.tsx | 3 + web/components/issues/sub-issues/root.tsx | 57 ++++- 3 files changed, 184 insertions(+), 115 deletions(-) diff --git a/web/components/issues/sub-issues/issue.tsx b/web/components/issues/sub-issues/issue.tsx index 2adb7c6f9..d9c6fd303 100644 --- a/web/components/issues/sub-issues/issue.tsx +++ b/web/components/issues/sub-issues/issue.tsx @@ -1,6 +1,7 @@ import React from "react"; // next imports import Link from "next/link"; +import { useRouter } from "next/router"; // lucide icons import { ChevronDown, @@ -37,6 +38,7 @@ export interface ISubIssues { issueId: string, issue?: IIssue | null ) => void; + setPeekParentId: (id: string) => void; } export const SubIssues: React.FC = ({ @@ -52,38 +54,54 @@ export const SubIssues: React.FC = ({ handleIssuesLoader, copyText, handleIssueCrudOperation, -}) => ( -
    - {issue && ( -
    -
    - {issue?.sub_issues_count > 0 && ( - <> - {issuesLoader.sub_issues.includes(issue?.id) ? ( -
    - -
    - ) : ( -
    handleIssuesLoader({ key: "visibility", issueId: issue?.id })} - > - {issuesLoader && issuesLoader.visibility.includes(issue?.id) ? ( - - ) : ( - - )} -
    - )} - - )} -
    + setPeekParentId, +}) => { + const router = useRouter(); - - + const openPeekOverview = (issue_id: string) => { + const { query } = router; + + setPeekParentId(parentIssue?.id); + router.push({ + pathname: router.pathname, + query: { ...query, peekIssue: issue_id }, + }); + }; + + return ( +
    + {issue && ( +
    +
    + {issue?.sub_issues_count > 0 && ( + <> + {issuesLoader.sub_issues.includes(issue?.id) ? ( +
    + +
    + ) : ( +
    handleIssuesLoader({ key: "visibility", issueId: issue?.id })} + > + {issuesLoader && issuesLoader.visibility.includes(issue?.id) ? ( + + ) : ( + + )} +
    + )} + + )} +
    + +
    openPeekOverview(issue?.id)} + > -
    - -
    +
    + +
    + +
    + + {editable && ( + handleIssueCrudOperation("edit", parentIssue?.id, issue)} + > +
    + + Edit issue +
    +
    + )} + + {editable && ( + handleIssueCrudOperation("delete", parentIssue?.id, issue)} + > +
    + + Delete issue +
    +
    + )} -
    - - {editable && ( handleIssueCrudOperation("edit", parentIssue?.id, issue)} + onClick={() => + copyText(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`) + } >
    - - Edit issue + + Copy issue link
    - )} +
    +
    - {editable && ( - handleIssueCrudOperation("delete", parentIssue?.id, issue)} - > -
    - - Delete issue + {editable && ( + <> + {issuesLoader.delete.includes(issue?.id) ? ( +
    +
    - - )} - - - copyText(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`) - } - > -
    - - Copy issue link -
    -
    - + ) : ( +
    { + handleIssuesLoader({ key: "delete", issueId: issue?.id }); + removeIssueFromSubIssues(parentIssue?.id, issue); + }} + > + +
    + )} + + )}
    + )} - {editable && ( - <> - {issuesLoader.delete.includes(issue?.id) ? ( -
    - -
    - ) : ( -
    { - handleIssuesLoader({ key: "delete", issueId: issue?.id }); - removeIssueFromSubIssues(parentIssue?.id, issue); - }} - > - -
    - )} - - )} -
    - )} - - {issuesLoader.visibility.includes(issue?.id) && issue?.sub_issues_count > 0 && ( - - )} -
    -); + {issuesLoader.visibility.includes(issue?.id) && issue?.sub_issues_count > 0 && ( + + )} +
    + ); +}; diff --git a/web/components/issues/sub-issues/issues-list.tsx b/web/components/issues/sub-issues/issues-list.tsx index 9fc77992e..a713d6fb8 100644 --- a/web/components/issues/sub-issues/issues-list.tsx +++ b/web/components/issues/sub-issues/issues-list.tsx @@ -27,6 +27,7 @@ export interface ISubIssuesRootList { issueId: string, issue?: IIssue | null ) => void; + setPeekParentId: (id: string) => void; } export const SubIssuesRootList: React.FC = ({ @@ -41,6 +42,7 @@ export const SubIssuesRootList: React.FC = ({ handleIssuesLoader, copyText, handleIssueCrudOperation, + setPeekParentId, }) => { const { data: issues, isLoading } = useSWR( workspaceSlug && projectId && parentIssue && parentIssue?.id @@ -81,6 +83,7 @@ export const SubIssuesRootList: React.FC = ({ handleIssuesLoader={handleIssuesLoader} copyText={copyText} handleIssueCrudOperation={handleIssueCrudOperation} + setPeekParentId={setPeekParentId} /> ))} diff --git a/web/components/issues/sub-issues/root.tsx b/web/components/issues/sub-issues/root.tsx index 305a9150b..352546eab 100644 --- a/web/components/issues/sub-issues/root.tsx +++ b/web/components/issues/sub-issues/root.tsx @@ -10,6 +10,7 @@ import { ExistingIssuesListModal } from "components/core"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; import { SubIssuesRootList } from "./issues-list"; import { ProgressBar } from "./progressbar"; +import { IssuePeekOverview } from "components/issues/peek-overview"; // ui import { CustomMenu } from "components/ui"; // hooks @@ -41,7 +42,11 @@ export interface ISubIssuesRootLoadersHandler { export const SubIssuesRoot: React.FC = ({ parentIssue, user }) => { const router = useRouter(); - const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; + const { workspaceSlug, projectId, peekIssue } = router.query as { + workspaceSlug: string; + projectId: string; + peekIssue: string; + }; const { memberRole } = useProjectMyMembership(); const { setToastAlert } = useToast(); @@ -55,6 +60,8 @@ export const SubIssuesRoot: React.FC = ({ parentIssue, user }) = : null ); + const [peekParentId, setPeekParentId] = React.useState(""); + const [issuesLoader, setIssuesLoader] = React.useState({ visibility: [parentIssue?.id], delete: [], @@ -230,15 +237,48 @@ export const SubIssuesRoot: React.FC = ({ parentIssue, user }) = handleIssuesLoader={handleIssuesLoader} copyText={copyText} handleIssueCrudOperation={handleIssueCrudOperation} + setPeekParentId={setPeekParentId} />
    )} + +
    + + + Add sub-issue + + } + buttonClassName="whitespace-nowrap" + position="left" + noBorder + noChevron + > + { + mutateSubIssues(parentIssue?.id); + handleIssueCrudOperation("create", parentIssue?.id); + }} + > + Create new + + { + mutateSubIssues(parentIssue?.id); + handleIssueCrudOperation("existing", parentIssue?.id); + }} + > + Add an existing issue + + +
    ) : ( isEditable && ( -
    -
    No sub issues are available
    - <> +
    +
    No Sub-Issues yet
    +
    @@ -268,7 +308,7 @@ export const SubIssuesRoot: React.FC = ({ parentIssue, user }) = Add an existing issue - +
    ) )} @@ -323,6 +363,13 @@ export const SubIssuesRoot: React.FC = ({ parentIssue, user }) = )} )} + + peekParentId && peekIssue && mutateSubIssues(peekParentId)} + projectId={projectId ?? ""} + workspaceSlug={workspaceSlug ?? ""} + readOnly={!isEditable} + />
    ); }; From 6d3bd780520e928c1e1838108ccbf1b538e4e01f Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 26 Sep 2023 13:10:28 +0530 Subject: [PATCH 11/35] chore: add tooltip to show full time on activity logs (#2235) --- web/components/issues/activity.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/web/components/issues/activity.tsx b/web/components/issues/activity.tsx index fe322afe9..e6f54f512 100644 --- a/web/components/issues/activity.tsx +++ b/web/components/issues/activity.tsx @@ -7,9 +7,9 @@ import { useRouter } from "next/router"; import { ActivityIcon, ActivityMessage } from "components/core"; import { CommentCard } from "components/issues/comment"; // ui -import { Icon, Loader } from "components/ui"; +import { Icon, Loader, Tooltip } from "components/ui"; // helpers -import { timeAgo } from "helpers/date-time.helper"; +import { render24HourFormatTime, renderLongDateFormat, timeAgo } from "helpers/date-time.helper"; // types import { IIssueActivity, IIssueComment } from "types"; @@ -120,9 +120,15 @@ export const IssueActivitySection: React.FC = ({ )}{" "} {message}{" "} - - {timeAgo(activityItem.created_at)} - + + + {timeAgo(activityItem.created_at)} + +
    From dae8ca60532156047ea922218585a7eee453bab4 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 26 Sep 2023 13:11:00 +0530 Subject: [PATCH 12/35] fix: issue automation iterable error (#2208) --- apiserver/plane/bgtasks/issue_automation_task.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/apiserver/plane/bgtasks/issue_automation_task.py b/apiserver/plane/bgtasks/issue_automation_task.py index a1b42073f..68c64403a 100644 --- a/apiserver/plane/bgtasks/issue_automation_task.py +++ b/apiserver/plane/bgtasks/issue_automation_task.py @@ -58,20 +58,23 @@ def archive_old_issues(): # Check if Issues if issues: + # Set the archive time to current time + archive_at = timezone.now() + issues_to_update = [] for issue in issues: - issue.archived_at = timezone.now() + issue.archived_at = archive_at issues_to_update.append(issue) # Bulk Update the issues and log the activity if issues_to_update: - updated_issues = Issue.objects.bulk_update( + Issue.objects.bulk_update( issues_to_update, ["archived_at"], batch_size=100 ) [ issue_activity.delay( type="issue.activity.updated", - requested_data=json.dumps({"archived_at": str(issue.archived_at)}), + requested_data=json.dumps({"archived_at": str(archive_at)}), actor_id=str(project.created_by_id), issue_id=issue.id, project_id=project_id, @@ -79,7 +82,7 @@ def archive_old_issues(): subscriber=False, epoch=int(timezone.now().timestamp()) ) - for issue in updated_issues + for issue in issues_to_update ] return except Exception as e: @@ -139,7 +142,7 @@ def close_old_issues(): # Bulk Update the issues and log the activity if issues_to_update: - updated_issues = Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100) + Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100) [ issue_activity.delay( type="issue.activity.updated", @@ -151,7 +154,7 @@ def close_old_issues(): subscriber=False, epoch=int(timezone.now().timestamp()) ) - for issue in updated_issues + for issue in issues_to_update ] return except Exception as e: From d38594376babd6df450b70b2d8180201d4f584e5 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 26 Sep 2023 13:11:23 +0530 Subject: [PATCH 13/35] fix: n+1 queries for cycle list and project member endpoints (#2257) --- apiserver/plane/api/serializers/cycle.py | 22 +--------------------- apiserver/plane/api/views/project.py | 2 +- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/apiserver/plane/api/serializers/cycle.py b/apiserver/plane/api/serializers/cycle.py index 664368033..ad214c52a 100644 --- a/apiserver/plane/api/serializers/cycle.py +++ b/apiserver/plane/api/serializers/cycle.py @@ -34,7 +34,6 @@ class CycleSerializer(BaseSerializer): unstarted_issues = serializers.IntegerField(read_only=True) backlog_issues = serializers.IntegerField(read_only=True) assignees = serializers.SerializerMethodField(read_only=True) - labels = serializers.SerializerMethodField(read_only=True) total_estimates = serializers.IntegerField(read_only=True) completed_estimates = serializers.IntegerField(read_only=True) started_estimates = serializers.IntegerField(read_only=True) @@ -50,11 +49,10 @@ class CycleSerializer(BaseSerializer): members = [ { "avatar": assignee.avatar, - "first_name": assignee.first_name, "display_name": assignee.display_name, "id": assignee.id, } - for issue_cycle in obj.issue_cycle.all() + for issue_cycle in obj.issue_cycle.prefetch_related("issue__assignees").all() for assignee in issue_cycle.issue.assignees.all() ] # Use a set comprehension to return only the unique objects @@ -64,24 +62,6 @@ class CycleSerializer(BaseSerializer): unique_list = [dict(item) for item in unique_objects] return unique_list - - def get_labels(self, obj): - labels = [ - { - "name": label.name, - "color": label.color, - "id": label.id, - } - for issue_cycle in obj.issue_cycle.all() - for label in issue_cycle.issue.labels.all() - ] - # Use a set comprehension to return only the unique objects - unique_objects = {frozenset(item.items()) for item in labels} - - # Convert the set back to a list of dictionaries - unique_list = [dict(item) for item in unique_objects] - - return unique_list class Meta: model = Cycle diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index 093c8ff78..c72b8d423 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -1094,7 +1094,7 @@ class ProjectMemberEndpoint(BaseAPIView): project_id=project_id, workspace__slug=slug, member__is_bot=False, - ).select_related("project", "member") + ).select_related("project", "member", "workspace") serializer = ProjectMemberSerializer(project_members, many=True) return Response(serializer.data, status=status.HTTP_200_OK) except Exception as e: From 88a35efa06585d47c0653bc63a56dd30283280b7 Mon Sep 17 00:00:00 2001 From: Henit Chobisa Date: Tue, 26 Sep 2023 13:46:38 +0530 Subject: [PATCH 14/35] [fix] nginx continuously rewriting and reloading on index page of spaces app (#2236) * chore: shifted index page to /home route * chore: added rewrite logic, to rewrite index to /home * chore: routed home to login route as login page * chore: updated nginx config to route to login * chore: updated path for home --- nginx/nginx.conf.template | 8 +++++++- space/components/accounts/sign-in.tsx | 4 ++-- space/components/views/index.ts | 2 +- space/components/views/{home.tsx => login.tsx} | 2 +- space/pages/index.tsx | 8 -------- space/pages/login/index.tsx | 8 ++++++++ 6 files changed, 19 insertions(+), 13 deletions(-) rename space/components/views/{home.tsx => login.tsx} (88%) delete mode 100644 space/pages/index.tsx create mode 100644 space/pages/login/index.tsx diff --git a/nginx/nginx.conf.template b/nginx/nginx.conf.template index af80b04fa..4775dcbfa 100644 --- a/nginx/nginx.conf.template +++ b/nginx/nginx.conf.template @@ -11,6 +11,11 @@ http { client_max_body_size ${FILE_SIZE_LIMIT}; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + add_header Permissions-Policy "interest-cohort=()" always; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + location / { proxy_pass http://web:3000/; } @@ -20,6 +25,7 @@ http { } location /spaces/ { + rewrite ^/spaces/?$ /spaces/login break; proxy_pass http://space:3000/spaces/; } @@ -27,4 +33,4 @@ http { proxy_pass http://plane-minio:9000/uploads/; } } -} \ No newline at end of file +} diff --git a/space/components/accounts/sign-in.tsx b/space/components/accounts/sign-in.tsx index d3c29103d..c6a151d44 100644 --- a/space/components/accounts/sign-in.tsx +++ b/space/components/accounts/sign-in.tsx @@ -33,7 +33,7 @@ export const SignInView = observer(() => { const onSignInSuccess = (response: any) => { const isOnboarded = response?.user?.onboarding_step?.profile_complete || false; - const nextPath = router.asPath.includes("next_path") ? router.asPath.split("/?next_path=")[1] : "/"; + const nextPath = router.asPath.includes("next_path") ? router.asPath.split("/?next_path=")[1] : "/login"; userStore.setCurrentUser(response?.user); @@ -41,7 +41,7 @@ export const SignInView = observer(() => { router.push(`/onboarding?next_path=${nextPath}`); return; } - router.push((nextPath ?? "/").toString()); + router.push((nextPath ?? "/login").toString()); }; const handleGoogleSignIn = async ({ clientId, credential }: any) => { diff --git a/space/components/views/index.ts b/space/components/views/index.ts index 84d36cd29..f54d11bdd 100644 --- a/space/components/views/index.ts +++ b/space/components/views/index.ts @@ -1 +1 @@ -export * from "./home"; +export * from "./login"; diff --git a/space/components/views/home.tsx b/space/components/views/login.tsx similarity index 88% rename from space/components/views/home.tsx rename to space/components/views/login.tsx index 999fce073..d01a22681 100644 --- a/space/components/views/home.tsx +++ b/space/components/views/login.tsx @@ -4,7 +4,7 @@ import { useMobxStore } from "lib/mobx/store-provider"; // components import { SignInView, UserLoggedIn } from "components/accounts"; -export const HomeView = observer(() => { +export const LoginView = observer(() => { const { user: userStore } = useMobxStore(); if (!userStore.currentUser) return ; diff --git a/space/pages/index.tsx b/space/pages/index.tsx deleted file mode 100644 index fe0b7d33a..000000000 --- a/space/pages/index.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from "react"; - -// components -import { HomeView } from "components/views"; - -const HomePage = () => ; - -export default HomePage; diff --git a/space/pages/login/index.tsx b/space/pages/login/index.tsx new file mode 100644 index 000000000..a80eff873 --- /dev/null +++ b/space/pages/login/index.tsx @@ -0,0 +1,8 @@ +import React from "react"; + +// components +import { LoginView } from "components/views"; + +const LoginPage = () => ; + +export default LoginPage; \ No newline at end of file From 52b57b1e37662de5722fc09e88f2789f0379f2a6 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Tue, 26 Sep 2023 14:18:06 +0530 Subject: [PATCH 15/35] dev: migration for 0.13 (#2266) * dev: updated migrations * dev: migration for 0.13 --- .../db/migrations/0044_auto_20230913_0709.py | 26 ++++---- .../db/migrations/0045_auto_20230915_0655.py | 62 ++++++++++++++++--- .../db/migrations/0046_auto_20230919_1421.py | 53 ---------------- .../db/migrations/0047_auto_20230921_0758.py | 27 -------- 4 files changed, 65 insertions(+), 103 deletions(-) delete mode 100644 apiserver/plane/db/migrations/0046_auto_20230919_1421.py delete mode 100644 apiserver/plane/db/migrations/0047_auto_20230921_0758.py diff --git a/apiserver/plane/db/migrations/0044_auto_20230913_0709.py b/apiserver/plane/db/migrations/0044_auto_20230913_0709.py index f30062371..19a1449af 100644 --- a/apiserver/plane/db/migrations/0044_auto_20230913_0709.py +++ b/apiserver/plane/db/migrations/0044_auto_20230913_0709.py @@ -26,19 +26,19 @@ def workspace_member_props(old_props): "calendar_date_range": old_props.get("calendarDateRange", ""), }, "display_properties": { - "assignee": old_props.get("properties", {}).get("assignee",None), - "attachment_count": old_props.get("properties", {}).get("attachment_count", None), - "created_on": old_props.get("properties", {}).get("created_on", None), - "due_date": old_props.get("properties", {}).get("due_date", None), - "estimate": old_props.get("properties", {}).get("estimate", None), - "key": old_props.get("properties", {}).get("key", None), - "labels": old_props.get("properties", {}).get("labels", None), - "link": old_props.get("properties", {}).get("link", None), - "priority": old_props.get("properties", {}).get("priority", None), - "start_date": old_props.get("properties", {}).get("start_date", None), - "state": old_props.get("properties", {}).get("state", None), - "sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", None), - "updated_on": old_props.get("properties", {}).get("updated_on", None), + "assignee": old_props.get("properties", {}).get("assignee", True), + "attachment_count": old_props.get("properties", {}).get("attachment_count", True), + "created_on": old_props.get("properties", {}).get("created_on", True), + "due_date": old_props.get("properties", {}).get("due_date", True), + "estimate": old_props.get("properties", {}).get("estimate", True), + "key": old_props.get("properties", {}).get("key", True), + "labels": old_props.get("properties", {}).get("labels", True), + "link": old_props.get("properties", {}).get("link", True), + "priority": old_props.get("properties", {}).get("priority", True), + "start_date": old_props.get("properties", {}).get("start_date", True), + "state": old_props.get("properties", {}).get("state", True), + "sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", True), + "updated_on": old_props.get("properties", {}).get("updated_on", True), }, } return new_props diff --git a/apiserver/plane/db/migrations/0045_auto_20230915_0655.py b/apiserver/plane/db/migrations/0045_auto_20230915_0655.py index a8360c63d..8512594ba 100644 --- a/apiserver/plane/db/migrations/0045_auto_20230915_0655.py +++ b/apiserver/plane/db/migrations/0045_auto_20230915_0655.py @@ -1,24 +1,66 @@ # Generated by Django 4.2.3 on 2023-09-15 06:55 -from django.db import migrations +from django.db import migrations, models +from django.conf import settings +import django.db.models.deletion +import uuid def update_issue_activity(apps, schema_editor): - IssueActivityModel = apps.get_model("db", "IssueActivity") + IssueActivity = apps.get_model("db", "IssueActivity") updated_issue_activity = [] - for obj in IssueActivityModel.objects.all(): - if obj.field == "blocks": - obj.field = "blocked_by" - updated_issue_activity.append(obj) - IssueActivityModel.objects.bulk_update(updated_issue_activity, ["field"], batch_size=100) + for obj in IssueActivity.objects.all(): + obj.epoch = int(obj.created_at.timestamp()) + + # Set the old and new value to none if it is empty for Priority + if obj.field == "priority": + obj.new_value = obj.new_value or "none" + obj.old_value = obj.old_value or "none" + + # Change the field name from blocks to blocked_by + if obj.field == "blocks": + obj.field = "blocked_by" + + updated_issue_activity.append(obj) + IssueActivity.objects.bulk_update( + updated_issue_activity, + ["epoch", "field", "new_value", "old_value"], + batch_size=100, + ) class Migration(migrations.Migration): - dependencies = [ - ('db', '0044_auto_20230913_0709'), + ("db", "0044_auto_20230913_0709"), ] operations = [ - migrations.RunPython(update_issue_activity), + migrations.CreateModel( + name="GlobalView", + fields=[ + ("created_at", models.DateTimeField(auto_now_add=True, verbose_name="Created At"),), + ("updated_at", models.DateTimeField(auto_now=True, verbose_name="Last Modified At"),), + ("id", models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True,),), + ("name", models.CharField(max_length=255, verbose_name="View Name")), + ("description", models.TextField(blank=True, verbose_name="View Description"),), + ("query", models.JSONField(verbose_name="View Query")), + ("access", models.PositiveSmallIntegerField(choices=[(0, "Private"), (1, "Public")], default=1),), + ("query_data", models.JSONField(default=dict)), + ("created_by", models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="%(class)s_created_by", to=settings.AUTH_USER_MODEL, verbose_name="Created By",),), + ("updated_by", models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="%(class)s_updated_by", to=settings.AUTH_USER_MODEL, verbose_name="Last Modified By",),), + ("workspace", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="global_views", to="db.workspace",),), + ], + options={ + "verbose_name": "Global View", + "verbose_name_plural": "Global Views", + "db_table": "global_views", + "ordering": ("-created_at",), + }, + ), + migrations.AddField( + model_name="issueactivity", + name="epoch", + field=models.FloatField(null=True), + ), + migrations.RunPython(update_issue_activity), ] diff --git a/apiserver/plane/db/migrations/0046_auto_20230919_1421.py b/apiserver/plane/db/migrations/0046_auto_20230919_1421.py deleted file mode 100644 index 4005a94d4..000000000 --- a/apiserver/plane/db/migrations/0046_auto_20230919_1421.py +++ /dev/null @@ -1,53 +0,0 @@ -# Generated by Django 4.2.3 on 2023-09-19 14:21 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -def update_epoch(apps, schema_editor): - IssueActivity = apps.get_model('db', 'IssueActivity') - updated_issue_activity = [] - for obj in IssueActivity.objects.all(): - obj.epoch = int(obj.created_at.timestamp()) - updated_issue_activity.append(obj) - IssueActivity.objects.bulk_update(updated_issue_activity, ["epoch"], batch_size=100) - - -class Migration(migrations.Migration): - - dependencies = [ - ('db', '0045_auto_20230915_0655'), - ] - - operations = [ - migrations.CreateModel( - name='GlobalView', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=255, verbose_name='View Name')), - ('description', models.TextField(blank=True, verbose_name='View Description')), - ('query', models.JSONField(verbose_name='View Query')), - ('access', models.PositiveSmallIntegerField(choices=[(0, 'Private'), (1, 'Public')], default=1)), - ('query_data', models.JSONField(default=dict)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='global_views', to='db.workspace')), - ], - options={ - 'verbose_name': 'Global View', - 'verbose_name_plural': 'Global Views', - 'db_table': 'global_views', - 'ordering': ('-created_at',), - }, - ), - migrations.AddField( - model_name='issueactivity', - name='epoch', - field=models.FloatField(null=True), - ), - migrations.RunPython(update_epoch), - ] diff --git a/apiserver/plane/db/migrations/0047_auto_20230921_0758.py b/apiserver/plane/db/migrations/0047_auto_20230921_0758.py deleted file mode 100644 index 4344963cd..000000000 --- a/apiserver/plane/db/migrations/0047_auto_20230921_0758.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 4.2.3 on 2023-09-21 07:58 - - -from django.db import migrations - - -def update_priority_history(apps, schema_editor): - IssueActivity = apps.get_model("db", "IssueActivity") - updated_issue_activity = [] - for obj in IssueActivity.objects.all(): - if obj.field == "priority": - obj.new_value = obj.new_value or "none" - obj.old_value = obj.old_value or "none" - updated_issue_activity.append(obj) - IssueActivity.objects.bulk_update( - updated_issue_activity, ["new_value", "old_value"], batch_size=100 - ) - - -class Migration(migrations.Migration): - dependencies = [ - ("db", "0046_auto_20230919_1421"), - ] - - operations = [ - migrations.RunPython(update_priority_history), - ] From 6e0999c35a133a0d8b803d047b1cf3fd8eb8ae27 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Tue, 26 Sep 2023 16:25:52 +0530 Subject: [PATCH 16/35] dev: re-split migrations into two different files (#2268) * dev: split issue activity migration separate files * dev: resplit migrations into two different files * dev: changed the batch size --- .../db/migrations/0045_auto_20230915_0655.py | 26 +---------- .../db/migrations/0046_auto_20230926_1015.py | 26 +++++++++++ .../db/migrations/0047_auto_20230926_1029.py | 44 +++++++++++++++++++ 3 files changed, 71 insertions(+), 25 deletions(-) create mode 100644 apiserver/plane/db/migrations/0046_auto_20230926_1015.py create mode 100644 apiserver/plane/db/migrations/0047_auto_20230926_1029.py diff --git a/apiserver/plane/db/migrations/0045_auto_20230915_0655.py b/apiserver/plane/db/migrations/0045_auto_20230915_0655.py index 8512594ba..cd9aa6902 100644 --- a/apiserver/plane/db/migrations/0045_auto_20230915_0655.py +++ b/apiserver/plane/db/migrations/0045_auto_20230915_0655.py @@ -6,29 +6,6 @@ import django.db.models.deletion import uuid -def update_issue_activity(apps, schema_editor): - IssueActivity = apps.get_model("db", "IssueActivity") - updated_issue_activity = [] - for obj in IssueActivity.objects.all(): - obj.epoch = int(obj.created_at.timestamp()) - - # Set the old and new value to none if it is empty for Priority - if obj.field == "priority": - obj.new_value = obj.new_value or "none" - obj.old_value = obj.old_value or "none" - - # Change the field name from blocks to blocked_by - if obj.field == "blocks": - obj.field = "blocked_by" - - updated_issue_activity.append(obj) - IssueActivity.objects.bulk_update( - updated_issue_activity, - ["epoch", "field", "new_value", "old_value"], - batch_size=100, - ) - - class Migration(migrations.Migration): dependencies = [ ("db", "0044_auto_20230913_0709"), @@ -61,6 +38,5 @@ class Migration(migrations.Migration): model_name="issueactivity", name="epoch", field=models.FloatField(null=True), - ), - migrations.RunPython(update_issue_activity), + ), ] diff --git a/apiserver/plane/db/migrations/0046_auto_20230926_1015.py b/apiserver/plane/db/migrations/0046_auto_20230926_1015.py new file mode 100644 index 000000000..8bce37d95 --- /dev/null +++ b/apiserver/plane/db/migrations/0046_auto_20230926_1015.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.5 on 2023-09-26 10:15 + +from django.db import migrations + + +def update_issue_activity(apps, schema_editor): + IssueActivity = apps.get_model("db", "IssueActivity") + updated_issue_activity = [] + for obj in IssueActivity.objects.all(): + obj.epoch = int(obj.created_at.timestamp()) + updated_issue_activity.append(obj) + IssueActivity.objects.bulk_update( + updated_issue_activity, + ["epoch"], + batch_size=5000, + ) + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0045_auto_20230915_0655'), + ] + + operations = [ + migrations.RunPython(update_issue_activity), + ] diff --git a/apiserver/plane/db/migrations/0047_auto_20230926_1029.py b/apiserver/plane/db/migrations/0047_auto_20230926_1029.py new file mode 100644 index 000000000..da64e11c8 --- /dev/null +++ b/apiserver/plane/db/migrations/0047_auto_20230926_1029.py @@ -0,0 +1,44 @@ +# Generated by Django 4.2.5 on 2023-09-26 10:29 + +from django.db import migrations + + +def update_issue_activity_priority(apps, schema_editor): + IssueActivity = apps.get_model("db", "IssueActivity") + updated_issue_activity = [] + for obj in IssueActivity.objects.filter(field="priority"): + # Set the old and new value to none if it is empty for Priority + obj.new_value = obj.new_value or "none" + obj.old_value = obj.old_value or "none" + updated_issue_activity.append(obj) + IssueActivity.objects.bulk_update( + updated_issue_activity, + ["new_value", "old_value"], + batch_size=1000, + ) + +def update_issue_activity_blocked(apps, schema_editor): + IssueActivity = apps.get_model("db", "IssueActivity") + updated_issue_activity = [] + for obj in IssueActivity.objects.filter(field="blocks"): + # Set the field to blocked_by + obj.field = "blocked_by" + updated_issue_activity.append(obj) + IssueActivity.objects.bulk_update( + updated_issue_activity, + ["field"], + batch_size=1000, + ) + + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0046_auto_20230926_1015'), + ] + + operations = [ + migrations.RunPython(update_issue_activity_priority), + migrations.RunPython(update_issue_activity_blocked), + ] From b317a14983ac4d1d2a0b10d9a5df7a80d2ebc773 Mon Sep 17 00:00:00 2001 From: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Date: Tue, 26 Sep 2023 17:35:51 +0530 Subject: [PATCH 17/35] fix: bugs in quick-add and draft issues (#2269) * fix: 'Last Drafted Issue' making sidebar look weird on collapsed * feat: scroll to the bottom when issue is created * fix: 'Add Issue' button overlapping issue card in spreadsheet view * fix: wrong placement of quick-add in calender layout * fix: spacing for issue card in spreadsheet view --- .../core/views/calendar-view/calendar.tsx | 5 +- .../inline-create-issue-form.tsx | 23 +++- .../core/views/calendar-view/single-date.tsx | 1 + .../views/inline-issue-create-wrapper.tsx | 10 +- .../spreadsheet-view/spreadsheet-view.tsx | 112 ++++++++++-------- web/components/gantt-chart/sidebar.tsx | 14 +++ .../workspace/sidebar-quick-action.tsx | 20 +++- .../projects/[projectId]/cycles/[cycleId].tsx | 2 +- 8 files changed, 118 insertions(+), 69 deletions(-) diff --git a/web/components/core/views/calendar-view/calendar.tsx b/web/components/core/views/calendar-view/calendar.tsx index 030f8b747..8fbe35305 100644 --- a/web/components/core/views/calendar-view/calendar.tsx +++ b/web/components/core/views/calendar-view/calendar.tsx @@ -183,7 +183,10 @@ export const CalendarView: React.FC = ({ {calendarIssues ? (
    -
    +
    void; onSuccess?: (data: IIssue) => Promise | void; prePopulatedData?: Partial; + dependencies: any[]; }; -const useCheckIfThereIsSpaceOnRight = (ref: React.RefObject) => { +const useCheckIfThereIsSpaceOnRight = (ref: React.RefObject, deps: any[]) => { const [isThereSpaceOnRight, setIsThereSpaceOnRight] = useState(true); + const router = useRouter(); + const { moduleId, cycleId, viewId } = router.query; + + const container = document.getElementById(`calendar-view-${cycleId ?? moduleId ?? viewId}`); + useEffect(() => { if (!ref.current) return; const { right } = ref.current.getBoundingClientRect(); - const width = right + 250; + const width = right; - if (width > window.innerWidth) setIsThereSpaceOnRight(false); + const innerWidth = container?.getBoundingClientRect().width ?? window.innerWidth; + + if (width > innerWidth) setIsThereSpaceOnRight(false); else setIsThereSpaceOnRight(true); - }, [ref]); + }, [ref, deps, container]); return isThereSpaceOnRight; }; @@ -63,11 +74,11 @@ const InlineInput = () => { }; export const CalendarInlineCreateIssueForm: React.FC = (props) => { - const { isOpen } = props; + const { isOpen, dependencies } = props; const ref = useRef(null); - const isSpaceOnRight = useCheckIfThereIsSpaceOnRight(ref); + const isSpaceOnRight = useCheckIfThereIsSpaceOnRight(ref, dependencies); return ( <> diff --git a/web/components/core/views/calendar-view/single-date.tsx b/web/components/core/views/calendar-view/single-date.tsx index 178151204..02ea56678 100644 --- a/web/components/core/views/calendar-view/single-date.tsx +++ b/web/components/core/views/calendar-view/single-date.tsx @@ -83,6 +83,7 @@ export const SingleCalendarDate: React.FC = (props) => { setIsCreateIssueFormOpen(false)} prePopulatedData={{ target_date: date.date, diff --git a/web/components/core/views/inline-issue-create-wrapper.tsx b/web/components/core/views/inline-issue-create-wrapper.tsx index 3c01c50ce..ace407ae1 100644 --- a/web/components/core/views/inline-issue-create-wrapper.tsx +++ b/web/components/core/views/inline-issue-create-wrapper.tsx @@ -218,11 +218,11 @@ export const InlineCreateIssueFormWrapper: React.FC = (props) => { ); if (isDraftIssues) - mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(projectId.toString() ?? "", params)); - if (displayFilters.layout === "calendar") mutate(calendarFetchKey); - if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey); - if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey); - if (groupedIssues) mutateMyIssues(); + await mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(projectId.toString() ?? "", params)); + if (displayFilters.layout === "calendar") await mutate(calendarFetchKey); + if (displayFilters.layout === "gantt_chart") await mutate(ganttFetchKey); + if (displayFilters.layout === "spreadsheet") await mutate(spreadsheetFetchKey); + if (groupedIssues) await mutateMyIssues(); setToastAlert({ type: "success", diff --git a/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx b/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx index 2315c21c3..d11fe85d6 100644 --- a/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx +++ b/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx @@ -68,7 +68,11 @@ export const SpreadsheetView: React.FC = ({ workspaceSlug={workspaceSlug?.toString() ?? ""} readOnly={disableUserActions} /> -
    +
    @@ -89,62 +93,66 @@ export const SpreadsheetView: React.FC = ({ userAuth={userAuth} /> ))} - -
    - setIsInlineCreateIssueFormOpen(false)} - prePopulatedData={{ - ...(cycleId && { cycle: cycleId.toString() }), - ...(moduleId && { module: moduleId.toString() }), - }} - /> - - {type === "issue" - ? !disableUserActions && - !isInlineCreateIssueFormOpen && ( - - ) - : !disableUserActions && - !isInlineCreateIssueFormOpen && ( - - - Add Issue - - } - position="left" - verticalPosition="top" - optionsClassName="left-5 !w-36" - noBorder - > - setIsInlineCreateIssueFormOpen(true)}> - Create new - - {openIssuesListModal && ( - - Add an existing issue - - )} - - )} -
    ) : ( )}
    + +
    + setIsInlineCreateIssueFormOpen(false)} + prePopulatedData={{ + ...(cycleId && { cycle: cycleId.toString() }), + ...(moduleId && { module: moduleId.toString() }), + }} + /> + + {type === "issue" + ? !disableUserActions && + !isInlineCreateIssueFormOpen && ( + + ) + : !disableUserActions && + !isInlineCreateIssueFormOpen && ( + + + Add Issue + + } + position="left" + verticalPosition="top" + optionsClassName="left-5 !w-36" + noBorder + > + setIsInlineCreateIssueFormOpen(true)}> + Create new + + {openIssuesListModal && ( + + Add an existing issue + + )} + + )} +
    ); }; diff --git a/web/components/gantt-chart/sidebar.tsx b/web/components/gantt-chart/sidebar.tsx index 35b253ef9..fc2cea1e0 100644 --- a/web/components/gantt-chart/sidebar.tsx +++ b/web/components/gantt-chart/sidebar.tsx @@ -92,6 +92,7 @@ export const GanttSidebar: React.FC = (props) => { {(droppableProvided) => (
    = (props) => { setIsCreateIssueFormOpen(false)} + onSuccess={() => { + const ganttSidebar = document.getElementById(`gantt-sidebar-${cycleId}`); + + const timeoutId = setTimeout(() => { + if (ganttSidebar) + ganttSidebar.scrollBy({ + top: ganttSidebar.scrollHeight, + left: 0, + behavior: "smooth", + }); + clearTimeout(timeoutId); + }, 10); + }} prePopulatedData={{ start_date: new Date(Date.now()).toISOString().split("T")[0], target_date: new Date(Date.now() + 86400000).toISOString().split("T")[0], diff --git a/web/components/workspace/sidebar-quick-action.tsx b/web/components/workspace/sidebar-quick-action.tsx index 8923abc14..1c614f694 100644 --- a/web/components/workspace/sidebar-quick-action.tsx +++ b/web/components/workspace/sidebar-quick-action.tsx @@ -44,7 +44,9 @@ export const WorkspaceSidebarQuickAction = () => { > -
    +
    + + + + +
    + } + placement="bottom-start" + > + + +
    + )} +
    + + {issue.sub_issues_count > 0 && ( +
    + +
    + )} +
    + + + +
    + ); +}; diff --git a/web/components/core/views/spreadsheet-view/spreadsheet-issues.tsx b/web/components/core/views/spreadsheet-view/issue-column/spreadsheet-issue-column.tsx similarity index 72% rename from web/components/core/views/spreadsheet-view/spreadsheet-issues.tsx rename to web/components/core/views/spreadsheet-view/issue-column/spreadsheet-issue-column.tsx index 0b16b617b..966852a5b 100644 --- a/web/components/core/views/spreadsheet-view/spreadsheet-issues.tsx +++ b/web/components/core/views/spreadsheet-view/issue-column/spreadsheet-issue-column.tsx @@ -1,36 +1,34 @@ -import React, { useState } from "react"; +import React from "react"; // components -import { SingleSpreadsheetIssue } from "components/core"; +import { IssueColumn } from "components/core"; // hooks import useSubIssue from "hooks/use-sub-issue"; // types -import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types"; +import { IIssue, Properties, UserAuth } from "types"; type Props = { issue: IIssue; - index: number; + projectId: string; expandedIssues: string[]; setExpandedIssues: React.Dispatch>; properties: Properties; handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void; - gridTemplateColumns: string; + setCurrentProjectId: React.Dispatch>; disableUserActions: boolean; - user: ICurrentUserResponse | undefined; userAuth: UserAuth; nestingLevel?: number; }; -export const SpreadsheetIssues: React.FC = ({ - index, +export const SpreadsheetIssuesColumn: React.FC = ({ issue, + projectId, expandedIssues, setExpandedIssues, - gridTemplateColumns, properties, handleIssueAction, + setCurrentProjectId, disableUserActions, - user, userAuth, nestingLevel = 0, }) => { @@ -49,22 +47,20 @@ export const SpreadsheetIssues: React.FC = ({ const isExpanded = expandedIssues.indexOf(issue.id) > -1; - const { subIssues, isLoading } = useSubIssue(issue.id, isExpanded); + const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded); return (
    - handleIssueAction(issue, "edit")} handleDeleteIssue={() => handleIssueAction(issue, "delete")} + setCurrentProjectId={setCurrentProjectId} disableUserActions={disableUserActions} - user={user} userAuth={userAuth} nestingLevel={nestingLevel} /> @@ -74,17 +70,16 @@ export const SpreadsheetIssues: React.FC = ({ subIssues && subIssues.length > 0 && subIssues.map((subIssue: IIssue) => ( - diff --git a/web/components/core/views/spreadsheet-view/label-column/index.ts b/web/components/core/views/spreadsheet-view/label-column/index.ts new file mode 100644 index 000000000..a1b69c1a9 --- /dev/null +++ b/web/components/core/views/spreadsheet-view/label-column/index.ts @@ -0,0 +1,2 @@ +export * from "./spreadsheet-label-column"; +export * from "./label-column"; diff --git a/web/components/core/views/spreadsheet-view/label-column/label-column.tsx b/web/components/core/views/spreadsheet-view/label-column/label-column.tsx new file mode 100644 index 000000000..cad1e7666 --- /dev/null +++ b/web/components/core/views/spreadsheet-view/label-column/label-column.tsx @@ -0,0 +1,47 @@ +import React from "react"; + +// components +import { LabelSelect } from "components/project"; +// types +import { ICurrentUserResponse, IIssue, Properties } from "types"; + +type Props = { + issue: IIssue; + projectId: string; + partialUpdateIssue: (formData: Partial, issue: IIssue) => void; + properties: Properties; + user: ICurrentUserResponse | undefined; + isNotAllowed: boolean; +}; + +export const LabelColumn: React.FC = ({ + issue, + projectId, + partialUpdateIssue, + properties, + user, + isNotAllowed, +}) => { + const handleLabelChange = (data: any) => { + partialUpdateIssue({ labels_list: data }, issue); + }; + + return ( +
    + + {properties.labels && ( + + )} + +
    + ); +}; diff --git a/web/components/core/views/spreadsheet-view/label-column/spreadsheet-label-column.tsx b/web/components/core/views/spreadsheet-view/label-column/spreadsheet-label-column.tsx new file mode 100644 index 000000000..5ab77e909 --- /dev/null +++ b/web/components/core/views/spreadsheet-view/label-column/spreadsheet-label-column.tsx @@ -0,0 +1,62 @@ +import React from "react"; + +// components +import { LabelColumn } from "components/core"; +// hooks +import useSubIssue from "hooks/use-sub-issue"; +// types +import { ICurrentUserResponse, IIssue, Properties } from "types"; + +type Props = { + issue: IIssue; + projectId: string; + partialUpdateIssue: (formData: Partial, issue: IIssue) => void; + expandedIssues: string[]; + properties: Properties; + user: ICurrentUserResponse | undefined; + isNotAllowed: boolean; +}; + +export const SpreadsheetLabelColumn: React.FC = ({ + issue, + projectId, + partialUpdateIssue, + expandedIssues, + properties, + user, + isNotAllowed, +}) => { + const isExpanded = expandedIssues.indexOf(issue.id) > -1; + + const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded); + + return ( +
    + + + {isExpanded && + !isLoading && + subIssues && + subIssues.length > 0 && + subIssues.map((subIssue: IIssue) => ( + + ))} +
    + ); +}; diff --git a/web/components/core/views/spreadsheet-view/priority-column/index.ts b/web/components/core/views/spreadsheet-view/priority-column/index.ts new file mode 100644 index 000000000..fc542331e --- /dev/null +++ b/web/components/core/views/spreadsheet-view/priority-column/index.ts @@ -0,0 +1,2 @@ +export * from "./spreadsheet-priority-column"; +export * from "./priority-column"; diff --git a/web/components/core/views/spreadsheet-view/priority-column/priority-column.tsx b/web/components/core/views/spreadsheet-view/priority-column/priority-column.tsx new file mode 100644 index 000000000..feb8acdf5 --- /dev/null +++ b/web/components/core/views/spreadsheet-view/priority-column/priority-column.tsx @@ -0,0 +1,64 @@ +import React from "react"; + +import { useRouter } from "next/router"; + +// components +import { PrioritySelect } from "components/project"; +// services +import trackEventServices from "services/track-event.service"; +// types +import { ICurrentUserResponse, IIssue, Properties, TIssuePriorities } from "types"; + +type Props = { + issue: IIssue; + projectId: string; + partialUpdateIssue: (formData: Partial, issue: IIssue) => void; + properties: Properties; + user: ICurrentUserResponse | undefined; + isNotAllowed: boolean; +}; + +export const PriorityColumn: React.FC = ({ + issue, + projectId, + partialUpdateIssue, + properties, + user, + isNotAllowed, +}) => { + const router = useRouter(); + + const { workspaceSlug } = router.query; + + const handlePriorityChange = (data: TIssuePriorities) => { + partialUpdateIssue({ priority: data }, issue); + trackEventServices.trackIssuePartialPropertyUpdateEvent( + { + workspaceSlug, + workspaceId: issue.workspace, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + "ISSUE_PROPERTY_UPDATE_PRIORITY", + user + ); + }; + + return ( +
    + + {properties.priority && ( + + )} + +
    + ); +}; diff --git a/web/components/core/views/spreadsheet-view/priority-column/spreadsheet-priority-column.tsx b/web/components/core/views/spreadsheet-view/priority-column/spreadsheet-priority-column.tsx new file mode 100644 index 000000000..f0b84fb59 --- /dev/null +++ b/web/components/core/views/spreadsheet-view/priority-column/spreadsheet-priority-column.tsx @@ -0,0 +1,62 @@ +import React from "react"; + +// components +import { PriorityColumn } from "components/core"; +// hooks +import useSubIssue from "hooks/use-sub-issue"; +// types +import { ICurrentUserResponse, IIssue, Properties } from "types"; + +type Props = { + issue: IIssue; + projectId: string; + partialUpdateIssue: (formData: Partial, issue: IIssue) => void; + expandedIssues: string[]; + properties: Properties; + user: ICurrentUserResponse | undefined; + isNotAllowed: boolean; +}; + +export const SpreadsheetPriorityColumn: React.FC = ({ + issue, + projectId, + partialUpdateIssue, + expandedIssues, + properties, + user, + isNotAllowed, +}) => { + const isExpanded = expandedIssues.indexOf(issue.id) > -1; + + const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded); + + return ( +
    + + + {isExpanded && + !isLoading && + subIssues && + subIssues.length > 0 && + subIssues.map((subIssue: IIssue) => ( + + ))} +
    + ); +}; diff --git a/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx b/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx index d11fe85d6..797aa7785 100644 --- a/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx +++ b/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx @@ -1,23 +1,52 @@ -import React, { useState } from "react"; +import React, { useCallback, useState } from "react"; // next import { useRouter } from "next/router"; +import { KeyedMutator, mutate } from "swr"; + // components -import { SpreadsheetColumns, SpreadsheetIssues, ListInlineCreateIssueForm } from "components/core"; -import { CustomMenu, Spinner } from "components/ui"; +import { + SpreadsheetAssigneeColumn, + SpreadsheetCreatedOnColumn, + SpreadsheetDueDateColumn, + SpreadsheetEstimateColumn, + SpreadsheetIssuesColumn, + SpreadsheetLabelColumn, + SpreadsheetPriorityColumn, + SpreadsheetStartDateColumn, + SpreadsheetStateColumn, + SpreadsheetUpdatedOnColumn, +} from "components/core"; +import { Spinner } from "components/ui"; import { IssuePeekOverview } from "components/issues"; // hooks import useIssuesProperties from "hooks/use-issue-properties"; -import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; // types -import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types"; -// constants -import { SPREADSHEET_COLUMN } from "constants/spreadsheet"; +import { ICurrentUserResponse, IIssue, ISubIssueResponse, UserAuth } from "types"; +import useWorkspaceIssuesFilters from "hooks/use-worskpace-issue-filter"; +import { + CYCLE_DETAILS, + CYCLE_ISSUES_WITH_PARAMS, + MODULE_DETAILS, + MODULE_ISSUES_WITH_PARAMS, + PROJECT_ISSUES_LIST_WITH_PARAMS, + SUB_ISSUES, + VIEW_ISSUES, + WORKSPACE_VIEW_ISSUES, +} from "constants/fetch-keys"; +import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; +import projectIssuesServices from "services/issues.service"; // icon -import { PlusIcon } from "@heroicons/react/24/outline"; type Props = { + spreadsheetIssues: IIssue[]; + mutateIssues: KeyedMutator< + | IIssue[] + | { + [key: string]: IIssue[]; + } + >; handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void; openIssuesListModal?: (() => void) | null; disableUserActions: boolean; @@ -26,6 +55,8 @@ type Props = { }; export const SpreadsheetView: React.FC = ({ + spreadsheetIssues, + mutateIssues, handleIssueAction, openIssuesListModal, disableUserActions, @@ -33,126 +64,220 @@ export const SpreadsheetView: React.FC = ({ userAuth, }) => { const [expandedIssues, setExpandedIssues] = useState([]); - const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false); + const [currentProjectId, setCurrentProjectId] = useState(null); const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId } = router.query; - - const type = cycleId ? "cycle" : moduleId ? "module" : "issue"; - - const { spreadsheetIssues, mutateIssues } = useSpreadsheetIssuesView(); + const { workspaceSlug, projectId, cycleId, moduleId, viewId, workspaceViewId } = router.query; const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); - const columnData = SPREADSHEET_COLUMN.map((column) => ({ - ...column, - isActive: properties - ? column.propertyName === "labels" - ? properties[column.propertyName as keyof Properties] - : column.propertyName === "title" - ? true - : properties[column.propertyName as keyof Properties] - : false, - })); + const workspaceIssuesPath = [ + { + params: { + sub_issue: false, + }, + path: "workspace-views/all-issues", + }, + { + params: { + assignees: user?.id ?? undefined, + sub_issue: false, + }, + path: "workspace-views/assigned", + }, + { + params: { + created_by: user?.id ?? undefined, + sub_issue: false, + }, + path: "workspace-views/created", + }, + { + params: { + subscriber: user?.id ?? undefined, + sub_issue: false, + }, + path: "workspace-views/subscribed", + }, + ]; - const gridTemplateColumns = columnData - .filter((column) => column.isActive) - .map((column) => column.colSize) - .join(" "); + const currentWorkspaceIssuePath = workspaceIssuesPath.find((path) => + router.pathname.includes(path.path) + ); + + const { params: workspaceViewParams } = useWorkspaceIssuesFilters( + workspaceSlug?.toString(), + workspaceViewId?.toString() + ); + + const { params } = useSpreadsheetIssuesView(); + + const partialUpdateIssue = useCallback( + (formData: Partial, issue: IIssue) => { + if (!workspaceSlug || !issue) return; + + const fetchKey = cycleId + ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params) + : moduleId + ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params) + : viewId + ? VIEW_ISSUES(viewId.toString(), params) + : workspaceViewId + ? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), workspaceViewParams) + : currentWorkspaceIssuePath + ? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), currentWorkspaceIssuePath?.params) + : PROJECT_ISSUES_LIST_WITH_PARAMS(issue.project_detail.id, params); + + if (issue.parent) + mutate( + SUB_ISSUES(issue.parent.toString()), + (prevData) => { + if (!prevData) return prevData; + + return { + ...prevData, + sub_issues: (prevData.sub_issues ?? []).map((i) => { + if (i.id === issue.id) { + return { + ...i, + ...formData, + }; + } + return i; + }), + }; + }, + false + ); + else + mutate( + fetchKey, + (prevData) => + (prevData ?? []).map((p) => { + if (p.id === issue.id) { + return { + ...p, + ...formData, + }; + } + return p; + }), + false + ); + + projectIssuesServices + .patchIssue( + workspaceSlug as string, + issue.project_detail.id, + issue.id as string, + formData, + user + ) + .then(() => { + if (issue.parent) { + mutate(SUB_ISSUES(issue.parent as string)); + } else { + mutate(fetchKey); + + if (cycleId) mutate(CYCLE_DETAILS(cycleId as string)); + if (moduleId) mutate(MODULE_DETAILS(moduleId as string)); + } + }) + .catch((error) => { + console.log(error); + }); + }, + [ + workspaceSlug, + cycleId, + moduleId, + viewId, + workspaceViewId, + currentWorkspaceIssuePath, + workspaceViewParams, + params, + user, + ] + ); + + const isNotAllowed = userAuth.isGuest || userAuth.isViewer; + + const renderColumn = (header: string, Component: React.ComponentType) => ( +
    +
    + {header} +
    +
    + {spreadsheetIssues.map((issue: IIssue, index) => ( + + ))} +
    +
    + ); return ( <> mutateIssues()} - projectId={projectId?.toString() ?? ""} + projectId={currentProjectId ?? ""} workspaceSlug={workspaceSlug?.toString() ?? ""} readOnly={disableUserActions} /> -
    -
    - -
    +
    {spreadsheetIssues ? ( -
    - {spreadsheetIssues.map((issue: IIssue, index) => ( - - ))} -
    + <> +
    +
    +
    + + ID + + + Issue + +
    + + {spreadsheetIssues.map((issue: IIssue, index) => ( + + ))} +
    +
    + {renderColumn("State", SpreadsheetStateColumn)} + {renderColumn("Priority", SpreadsheetPriorityColumn)} + {renderColumn("Assignees", SpreadsheetAssigneeColumn)} + {renderColumn("Label", SpreadsheetLabelColumn)} + {renderColumn("Start Date", SpreadsheetStartDateColumn)} + {renderColumn("Due Date", SpreadsheetDueDateColumn)} + {renderColumn("Estimate", SpreadsheetEstimateColumn)} + {renderColumn("Created On", SpreadsheetCreatedOnColumn)} + {renderColumn("Updated On", SpreadsheetUpdatedOnColumn)} + ) : ( - +
    + +
    )}
    - -
    - setIsInlineCreateIssueFormOpen(false)} - prePopulatedData={{ - ...(cycleId && { cycle: cycleId.toString() }), - ...(moduleId && { module: moduleId.toString() }), - }} - /> - - {type === "issue" - ? !disableUserActions && - !isInlineCreateIssueFormOpen && ( - - ) - : !disableUserActions && - !isInlineCreateIssueFormOpen && ( - - - Add Issue - - } - position="left" - verticalPosition="top" - optionsClassName="left-5 !w-36" - noBorder - > - setIsInlineCreateIssueFormOpen(true)}> - Create new - - {openIssuesListModal && ( - - Add an existing issue - - )} - - )} -
    ); }; diff --git a/web/components/core/views/spreadsheet-view/start-date-column/index.ts b/web/components/core/views/spreadsheet-view/start-date-column/index.ts new file mode 100644 index 000000000..94f229498 --- /dev/null +++ b/web/components/core/views/spreadsheet-view/start-date-column/index.ts @@ -0,0 +1,2 @@ +export * from "./spreadsheet-start-date-column"; +export * from "./start-date-column"; diff --git a/web/components/core/views/spreadsheet-view/start-date-column/spreadsheet-start-date-column.tsx b/web/components/core/views/spreadsheet-view/start-date-column/spreadsheet-start-date-column.tsx new file mode 100644 index 000000000..064506ca2 --- /dev/null +++ b/web/components/core/views/spreadsheet-view/start-date-column/spreadsheet-start-date-column.tsx @@ -0,0 +1,62 @@ +import React from "react"; + +// components +import { StartDateColumn } from "components/core"; +// hooks +import useSubIssue from "hooks/use-sub-issue"; +// types +import { ICurrentUserResponse, IIssue, Properties } from "types"; + +type Props = { + issue: IIssue; + projectId: string; + partialUpdateIssue: (formData: Partial, issue: IIssue) => void; + expandedIssues: string[]; + properties: Properties; + user: ICurrentUserResponse | undefined; + isNotAllowed: boolean; +}; + +export const SpreadsheetStartDateColumn: React.FC = ({ + issue, + projectId, + partialUpdateIssue, + expandedIssues, + properties, + user, + isNotAllowed, +}) => { + const isExpanded = expandedIssues.indexOf(issue.id) > -1; + + const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded); + + return ( +
    + + + {isExpanded && + !isLoading && + subIssues && + subIssues.length > 0 && + subIssues.map((subIssue: IIssue) => ( + + ))} +
    + ); +}; diff --git a/web/components/core/views/spreadsheet-view/start-date-column/start-date-column.tsx b/web/components/core/views/spreadsheet-view/start-date-column/start-date-column.tsx new file mode 100644 index 000000000..3b4b9a0f7 --- /dev/null +++ b/web/components/core/views/spreadsheet-view/start-date-column/start-date-column.tsx @@ -0,0 +1,38 @@ +import React from "react"; + +// components +import { ViewStartDateSelect } from "components/issues"; +// types +import { ICurrentUserResponse, IIssue, Properties } from "types"; + +type Props = { + issue: IIssue; + projectId: string; + partialUpdateIssue: (formData: Partial, issue: IIssue) => void; + properties: Properties; + user: ICurrentUserResponse | undefined; + isNotAllowed: boolean; +}; + +export const StartDateColumn: React.FC = ({ + issue, + projectId, + partialUpdateIssue, + properties, + user, + isNotAllowed, +}) => ( +
    + + {properties.due_date && ( + + )} + +
    +); diff --git a/web/components/core/views/spreadsheet-view/state-column/index.ts b/web/components/core/views/spreadsheet-view/state-column/index.ts new file mode 100644 index 000000000..f3cbef871 --- /dev/null +++ b/web/components/core/views/spreadsheet-view/state-column/index.ts @@ -0,0 +1,2 @@ +export * from "./spreadsheet-state-column"; +export * from "./state-column"; diff --git a/web/components/core/views/spreadsheet-view/state-column/spreadsheet-state-column.tsx b/web/components/core/views/spreadsheet-view/state-column/spreadsheet-state-column.tsx new file mode 100644 index 000000000..606f3e28a --- /dev/null +++ b/web/components/core/views/spreadsheet-view/state-column/spreadsheet-state-column.tsx @@ -0,0 +1,62 @@ +import React from "react"; + +// components +import { StateColumn } from "components/core"; +// hooks +import useSubIssue from "hooks/use-sub-issue"; +// types +import { ICurrentUserResponse, IIssue, Properties } from "types"; + +type Props = { + issue: IIssue; + projectId: string; + partialUpdateIssue: (formData: Partial, issue: IIssue) => void; + expandedIssues: string[]; + properties: Properties; + user: ICurrentUserResponse | undefined; + isNotAllowed: boolean; +}; + +export const SpreadsheetStateColumn: React.FC = ({ + issue, + projectId, + partialUpdateIssue, + expandedIssues, + properties, + user, + isNotAllowed, +}) => { + const isExpanded = expandedIssues.indexOf(issue.id) > -1; + + const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded); + + return ( +
    + + + {isExpanded && + !isLoading && + subIssues && + subIssues.length > 0 && + subIssues.map((subIssue: IIssue) => ( + + ))} +
    + ); +}; diff --git a/web/components/core/views/spreadsheet-view/state-column/state-column.tsx b/web/components/core/views/spreadsheet-view/state-column/state-column.tsx new file mode 100644 index 000000000..6b3d3c696 --- /dev/null +++ b/web/components/core/views/spreadsheet-view/state-column/state-column.tsx @@ -0,0 +1,87 @@ +import React from "react"; + +import { useRouter } from "next/router"; + +// components +import { StateSelect } from "components/states"; +// services +import trackEventServices from "services/track-event.service"; +// types +import { ICurrentUserResponse, IIssue, IState, Properties } from "types"; + +type Props = { + issue: IIssue; + projectId: string; + partialUpdateIssue: (formData: Partial, issue: IIssue) => void; + properties: Properties; + user: ICurrentUserResponse | undefined; + isNotAllowed: boolean; +}; + +export const StateColumn: React.FC = ({ + issue, + projectId, + partialUpdateIssue, + properties, + user, + isNotAllowed, +}) => { + const router = useRouter(); + + const { workspaceSlug } = router.query; + + const handleStateChange = (data: string, states: IState[] | undefined) => { + const oldState = states?.find((s) => s.id === issue.state); + const newState = states?.find((s) => s.id === data); + + partialUpdateIssue( + { + state: data, + state_detail: newState, + }, + issue + ); + trackEventServices.trackIssuePartialPropertyUpdateEvent( + { + workspaceSlug, + workspaceId: issue.workspace, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + "ISSUE_PROPERTY_UPDATE_STATE", + user + ); + if (oldState?.group !== "completed" && newState?.group !== "completed") { + trackEventServices.trackIssueMarkedAsDoneEvent( + { + workspaceSlug: issue.workspace_detail.slug, + workspaceId: issue.workspace_detail.id, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + user + ); + } + }; + + return ( +
    + + {properties.state && ( + + )} + +
    + ); +}; diff --git a/web/components/core/views/spreadsheet-view/updated-on-column/index.ts b/web/components/core/views/spreadsheet-view/updated-on-column/index.ts new file mode 100644 index 000000000..af1337a7f --- /dev/null +++ b/web/components/core/views/spreadsheet-view/updated-on-column/index.ts @@ -0,0 +1,2 @@ +export * from "./spreadsheet-updated-on-column"; +export * from "./updated-on-column"; diff --git a/web/components/core/views/spreadsheet-view/updated-on-column/spreadsheet-updated-on-column.tsx b/web/components/core/views/spreadsheet-view/updated-on-column/spreadsheet-updated-on-column.tsx new file mode 100644 index 000000000..bb29e460d --- /dev/null +++ b/web/components/core/views/spreadsheet-view/updated-on-column/spreadsheet-updated-on-column.tsx @@ -0,0 +1,62 @@ +import React from "react"; + +// components +import { UpdatedOnColumn } from "components/core"; +// hooks +import useSubIssue from "hooks/use-sub-issue"; +// types +import { ICurrentUserResponse, IIssue, Properties } from "types"; + +type Props = { + issue: IIssue; + projectId: string; + partialUpdateIssue: (formData: Partial, issue: IIssue) => void; + expandedIssues: string[]; + properties: Properties; + user: ICurrentUserResponse | undefined; + isNotAllowed: boolean; +}; + +export const SpreadsheetUpdatedOnColumn: React.FC = ({ + issue, + projectId, + partialUpdateIssue, + expandedIssues, + properties, + user, + isNotAllowed, +}) => { + const isExpanded = expandedIssues.indexOf(issue.id) > -1; + + const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded); + + return ( +
    + + + {isExpanded && + !isLoading && + subIssues && + subIssues.length > 0 && + subIssues.map((subIssue: IIssue) => ( + + ))} +
    + ); +}; diff --git a/web/components/core/views/spreadsheet-view/updated-on-column/updated-on-column.tsx b/web/components/core/views/spreadsheet-view/updated-on-column/updated-on-column.tsx new file mode 100644 index 000000000..b63519095 --- /dev/null +++ b/web/components/core/views/spreadsheet-view/updated-on-column/updated-on-column.tsx @@ -0,0 +1,34 @@ +import React from "react"; + +// types +import { ICurrentUserResponse, IIssue, Properties } from "types"; +// helper +import { renderLongDetailDateFormat } from "helpers/date-time.helper"; + +type Props = { + issue: IIssue; + projectId: string; + partialUpdateIssue: (formData: Partial, issue: IIssue) => void; + properties: Properties; + user: ICurrentUserResponse | undefined; + isNotAllowed: boolean; +}; + +export const UpdatedOnColumn: React.FC = ({ + issue, + projectId, + partialUpdateIssue, + properties, + user, + isNotAllowed, +}) => ( +
    + + {properties.updated_on && ( +
    + {renderLongDetailDateFormat(issue.updated_at)} +
    + )} +
    +
    +); diff --git a/web/components/issues/my-issues/my-issues-select-filters.tsx b/web/components/issues/my-issues/my-issues-select-filters.tsx index ce8e03797..8085b5e78 100644 --- a/web/components/issues/my-issues/my-issues-select-filters.tsx +++ b/web/components/issues/my-issues/my-issues-select-filters.tsx @@ -4,18 +4,21 @@ import { useRouter } from "next/router"; import useSWR from "swr"; +// hook +import useProjects from "hooks/use-projects"; +import useWorkspaceMembers from "hooks/use-workspace-members"; // services import issuesService from "services/issues.service"; // components import { DateFilterModal } from "components/core"; // ui -import { MultiLevelDropdown } from "components/ui"; +import { Avatar, MultiLevelDropdown } from "components/ui"; // icons import { PriorityIcon, StateGroupIcon } from "components/icons"; // helpers import { checkIfArraysHaveSameElements } from "helpers/array.helper"; // types -import { IIssueFilterOptions, IQuery, TStateGroups } from "types"; +import { IIssueFilterOptions, TStateGroups } from "types"; // fetch-keys import { WORKSPACE_LABELS } from "constants/fetch-keys"; // constants @@ -23,7 +26,7 @@ import { GROUP_CHOICES, PRIORITIES } from "constants/project"; import { DATE_FILTER_OPTIONS } from "constants/filters"; type Props = { - filters: Partial | IQuery; + filters: Partial; onSelect: (option: any) => void; direction?: "left" | "right"; height?: "sm" | "md" | "rg" | "lg"; @@ -55,6 +58,11 @@ export const MyIssuesSelectFilters: React.FC = ({ : null ); + const { projects: allProjects } = useProjects(); + const joinedProjects = allProjects?.filter((p) => p.is_member); + + const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? ""); + return ( <> {isDateFilterModalOpen && ( @@ -74,25 +82,19 @@ export const MyIssuesSelectFilters: React.FC = ({ height={height} options={[ { - id: "priority", - label: "Priority", - value: PRIORITIES, + id: "project", + label: "Project", + value: joinedProjects, hasChildren: true, - children: [ - ...PRIORITIES.map((priority) => ({ - id: priority === null ? "null" : priority, - label: ( -
    - {priority ?? "None"} -
    - ), - value: { - key: "priority", - value: priority === null ? "null" : priority, - }, - selected: filters?.priority?.includes(priority === null ? "null" : priority), - })), - ], + children: joinedProjects?.map((project) => ({ + id: project.id, + label:
    {project.name}
    , + value: { + key: "project", + value: project.id, + }, + selected: filters?.project?.includes(project.id), + })), }, { id: "state_group", @@ -142,6 +144,87 @@ export const MyIssuesSelectFilters: React.FC = ({ selected: filters?.labels?.includes(label.id), })), }, + { + id: "priority", + label: "Priority", + value: PRIORITIES, + hasChildren: true, + children: [ + ...PRIORITIES.map((priority) => ({ + id: priority === null ? "null" : priority, + label: ( +
    + {priority ?? "None"} +
    + ), + value: { + key: "priority", + value: priority === null ? "null" : priority, + }, + selected: filters?.priority?.includes(priority === null ? "null" : priority), + })), + ], + }, + { + id: "created_by", + label: "Created by", + value: workspaceMembers, + hasChildren: true, + children: workspaceMembers?.map((member) => ({ + id: member.member.id, + label: ( +
    + + {member.member.display_name} +
    + ), + value: { + key: "created_by", + value: member.member.id, + }, + selected: filters?.created_by?.includes(member.member.id), + })), + }, + { + id: "assignees", + label: "Assignees", + value: workspaceMembers, + hasChildren: true, + children: workspaceMembers?.map((member) => ({ + id: member.member.id, + label: ( +
    + + {member.member.display_name} +
    + ), + value: { + key: "assignees", + value: member.member.id, + }, + selected: filters?.assignees?.includes(member.member.id), + })), + }, + { + id: "subscriber", + label: "Subscriber", + value: workspaceMembers, + hasChildren: true, + children: workspaceMembers?.map((member) => ({ + id: member.member.id, + label: ( +
    + + {member.member.display_name} +
    + ), + value: { + key: "subscriber", + value: member.member.id, + }, + selected: filters?.subscriber?.includes(member.member.id), + })), + }, { id: "start_date", label: "Start date", diff --git a/web/components/issues/my-issues/my-issues-view-options.tsx b/web/components/issues/my-issues/my-issues-view-options.tsx index 8c8bd9dc7..1cbc467a8 100644 --- a/web/components/issues/my-issues/my-issues-view-options.tsx +++ b/web/components/issues/my-issues/my-issues-view-options.tsx @@ -2,25 +2,20 @@ import React from "react"; import { useRouter } from "next/router"; -// headless ui -import { Popover, Transition } from "@headlessui/react"; // hooks import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter"; -import useEstimateOption from "hooks/use-estimate-option"; // components import { MyIssuesSelectFilters } from "components/issues"; // ui -import { CustomMenu, ToggleSwitch, Tooltip } from "components/ui"; +import { Tooltip } from "components/ui"; // icons -import { ChevronDownIcon } from "@heroicons/react/24/outline"; -import { FormatListBulletedOutlined, GridViewOutlined } from "@mui/icons-material"; +import { FormatListBulletedOutlined } from "@mui/icons-material"; +import { CreditCard } from "lucide-react"; // helpers import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; import { checkIfArraysHaveSameElements } from "helpers/array.helper"; // types -import { Properties, TIssueViewOptions } from "types"; -// constants -import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue"; +import { TIssueViewOptions } from "types"; const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [ { @@ -28,19 +23,26 @@ const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [ Icon: FormatListBulletedOutlined, }, { - type: "kanban", - Icon: GridViewOutlined, + type: "spreadsheet", + Icon: CreditCard, }, ]; export const MyIssuesViewOptions: React.FC = () => { const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug, workspaceViewId } = router.query; - const { displayFilters, setDisplayFilters, properties, setProperty, filters, setFilters } = - useMyIssuesFilters(workspaceSlug?.toString()); + const { displayFilters, setDisplayFilters, filters, setFilters } = useMyIssuesFilters( + workspaceSlug?.toString() + ); - const { isEstimateActive } = useEstimateOption(); + const workspaceViewPathName = ["workspace-views/all-issues"]; + + const isWorkspaceViewPath = workspaceViewPathName.some((pathname) => + router.pathname.includes(pathname) + ); + + const showFilters = isWorkspaceViewPath || workspaceViewId; return (
    @@ -49,250 +51,65 @@ export const MyIssuesViewOptions: React.FC = () => { {replaceUnderscoreIfSnakeCase(option.type)} Layout + {replaceUnderscoreIfSnakeCase(option.type)} View } position="bottom" > ))}
    - { - const key = option.key as keyof typeof filters; + {showFilters && ( + { + const key = option.key as keyof typeof filters; - if (key === "start_date" || key === "target_date") { - const valueExists = checkIfArraysHaveSameElements(filters?.[key] ?? [], option.value); + if (key === "start_date" || key === "target_date") { + const valueExists = checkIfArraysHaveSameElements(filters?.[key] ?? [], option.value); - setFilters({ - [key]: valueExists ? null : option.value, - }); - } else { - const valueExists = filters[key]?.includes(option.value); - - if (valueExists) setFilters({ - [option.key]: ((filters[key] ?? []) as any[])?.filter( - (val) => val !== option.value - ), + [key]: valueExists ? null : option.value, }); - else - setFilters({ - [option.key]: [...((filters[key] ?? []) as any[]), option.value], - }); - } - }} - direction="left" - height="rg" - /> - - {({ open }) => ( - <> - - Display - + } else { + const valueExists = filters[key]?.includes(option.value); - - -
    -
    - {displayFilters?.layout !== "calendar" && - displayFilters?.layout !== "spreadsheet" && ( - <> -
    -

    Group by

    -
    - option.key === displayFilters?.group_by - )?.name ?? "Select" - } - className="!w-full" - buttonClassName="w-full" - > - {GROUP_BY_OPTIONS.map((option) => { - if (displayFilters?.layout === "kanban" && option.key === null) - return null; - if ( - option.key === "state" || - option.key === "created_by" || - option.key === "assignees" - ) - return null; - - return ( - setDisplayFilters({ group_by: option.key })} - > - {option.name} - - ); - })} - -
    -
    -
    -

    Order by

    -
    - option.key === displayFilters?.order_by - )?.name ?? "Select" - } - className="!w-full" - buttonClassName="w-full" - > - {ORDER_BY_OPTIONS.map((option) => { - if ( - displayFilters?.group_by === "priority" && - option.key === "priority" - ) - return null; - if (option.key === "sort_order") return null; - - return ( - { - setDisplayFilters({ order_by: option.key }); - }} - > - {option.name} - - ); - })} - -
    -
    - - )} -
    -

    Issue type

    -
    - option.key === displayFilters?.type - )?.name ?? "Select" - } - className="!w-full" - buttonClassName="w-full" - > - {FILTER_ISSUE_OPTIONS.map((option) => ( - - setDisplayFilters({ - type: option.key, - }) - } - > - {option.name} - - ))} - -
    -
    - - {displayFilters?.layout !== "calendar" && - displayFilters?.layout !== "spreadsheet" && ( - <> -
    -

    Show empty groups

    -
    - - setDisplayFilters({ - show_empty_groups: !displayFilters?.show_empty_groups, - }) - } - /> -
    -
    - - )} -
    - -
    -

    Display Properties

    -
    - {Object.keys(properties).map((key) => { - if (key === "estimate" && !isEstimateActive) return null; - - if ( - displayFilters?.layout === "spreadsheet" && - (key === "attachment_count" || - key === "link" || - key === "sub_issue_count") - ) - return null; - - if ( - displayFilters?.layout !== "spreadsheet" && - (key === "created_on" || key === "updated_on") - ) - return null; - - return ( - - ); - })} -
    -
    -
    -
    -
    - - )} -
    + if (valueExists) + setFilters({ + [option.key]: ((filters[key] ?? []) as any[])?.filter( + (val) => val !== option.value + ), + }); + else + setFilters({ + [option.key]: [...((filters[key] ?? []) as any[]), option.value], + }); + } + }} + direction="left" + height="rg" + /> + )}
    ); }; diff --git a/web/components/issues/workspace-views/workspace-issue-view-option.tsx b/web/components/issues/workspace-views/workspace-issue-view-option.tsx new file mode 100644 index 000000000..4e98cce92 --- /dev/null +++ b/web/components/issues/workspace-views/workspace-issue-view-option.tsx @@ -0,0 +1,121 @@ +import React from "react"; + +import { useRouter } from "next/router"; + +// hooks +import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter"; +import useWorkspaceIssuesFilters from "hooks/use-worskpace-issue-filter"; +// components +import { MyIssuesSelectFilters } from "components/issues"; +// ui +import { Tooltip } from "components/ui"; +// icons +import { FormatListBulletedOutlined } from "@mui/icons-material"; +import { CreditCard } from "lucide-react"; +// helpers +import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; +import { checkIfArraysHaveSameElements } from "helpers/array.helper"; +// types +import { TIssueViewOptions } from "types"; + +const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [ + { + type: "list", + Icon: FormatListBulletedOutlined, + }, + { + type: "spreadsheet", + Icon: CreditCard, + }, +]; + +export const WorkspaceIssuesViewOptions: React.FC = () => { + const router = useRouter(); + const { workspaceSlug, workspaceViewId } = router.query; + + const { displayFilters, setDisplayFilters } = useMyIssuesFilters(workspaceSlug?.toString()); + + const { filters, setFilters } = useWorkspaceIssuesFilters( + workspaceSlug?.toString(), + workspaceViewId?.toString() + ); + + const isWorkspaceViewPath = router.pathname.includes("workspace-views/all-issues"); + + const showFilters = isWorkspaceViewPath || workspaceViewId; + + return ( +
    +
    + {issueViewOptions.map((option) => ( + {replaceUnderscoreIfSnakeCase(option.type)} View + } + position="bottom" + > + + + ))} +
    + + {showFilters && ( + <> + { + const key = option.key as keyof typeof filters; + + if (key === "start_date" || key === "target_date") { + const valueExists = checkIfArraysHaveSameElements( + filters?.[key] ?? [], + option.value + ); + + setFilters({ + [key]: valueExists ? null : option.value, + }); + } else { + const valueExists = filters[key]?.includes(option.value); + + if (valueExists) + setFilters({ + [option.key]: ((filters[key] ?? []) as any[])?.filter( + (val) => val !== option.value + ), + }); + else + setFilters({ + [option.key]: [...((filters[key] ?? []) as any[]), option.value], + }); + } + }} + direction="left" + height="rg" + /> + + )} +
    + ); +}; diff --git a/web/components/views/delete-view-modal.tsx b/web/components/views/delete-view-modal.tsx index c65f7ba29..0d49c62cc 100644 --- a/web/components/views/delete-view-modal.tsx +++ b/web/components/views/delete-view-modal.tsx @@ -8,6 +8,7 @@ import { mutate } from "swr"; import { Dialog, Transition } from "@headlessui/react"; // services import viewsService from "services/views.service"; +import workspaceService from "services/workspace.service"; // hooks import useToast from "hooks/use-toast"; // ui @@ -17,16 +18,17 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // types import type { ICurrentUserResponse, IView } from "types"; // fetch-keys -import { VIEWS_LIST } from "constants/fetch-keys"; +import { VIEWS_LIST, WORKSPACE_VIEWS_LIST } from "constants/fetch-keys"; type Props = { isOpen: boolean; + viewType: "project" | "workspace"; setIsOpen: React.Dispatch>; data: IView | null; user: ICurrentUserResponse | undefined; }; -export const DeleteViewModal: React.FC = ({ isOpen, data, setIsOpen, user }) => { +export const DeleteViewModal: React.FC = ({ isOpen, data, setIsOpen, viewType, user }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const router = useRouter(); @@ -41,34 +43,64 @@ export const DeleteViewModal: React.FC = ({ isOpen, data, setIsOpen, user const handleDeletion = async () => { setIsDeleteLoading(true); - if (!workspaceSlug || !data || !projectId) return; - await viewsService - .deleteView(workspaceSlug as string, projectId as string, data.id, user) - .then(() => { - mutate( - VIEWS_LIST(projectId as string), - (views) => views?.filter((view) => view.id !== data.id) - ); + if (viewType === "project") { + if (!workspaceSlug || !data || !projectId) return; - handleClose(); + await viewsService + .deleteView(workspaceSlug as string, projectId as string, data.id, user) + .then(() => { + mutate(VIEWS_LIST(projectId as string), (views) => + views?.filter((view) => view.id !== data.id) + ); - setToastAlert({ - type: "success", - title: "Success!", - message: "View deleted successfully.", + handleClose(); + + setToastAlert({ + type: "success", + title: "Success!", + message: "View deleted successfully.", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "View could not be deleted. Please try again.", + }); + }) + .finally(() => { + setIsDeleteLoading(false); }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "View could not be deleted. Please try again.", + } else { + if (!workspaceSlug || !data) return; + + await workspaceService + .deleteView(workspaceSlug as string, data.id) + .then(() => { + mutate(WORKSPACE_VIEWS_LIST(workspaceSlug as string), (views) => + views?.filter((view) => view.id !== data.id) + ); + + handleClose(); + + setToastAlert({ + type: "success", + title: "Success!", + message: "View deleted successfully.", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "View could not be deleted. Please try again.", + }); + }) + .finally(() => { + setIsDeleteLoading(false); }); - }) - .finally(() => { - setIsDeleteLoading(false); - }); + } }; return ( diff --git a/web/components/views/form.tsx b/web/components/views/form.tsx index 0c57a9542..29d16ca9c 100644 --- a/web/components/views/form.tsx +++ b/web/components/views/form.tsx @@ -10,6 +10,8 @@ import { useForm } from "react-hook-form"; import stateService from "services/state.service"; // hooks import useProjectMembers from "hooks/use-project-members"; +import useProjects from "hooks/use-projects"; +import useWorkspaceMembers from "hooks/use-workspace-members"; // components import { FiltersList } from "components/core"; import { SelectFilters } from "components/views"; @@ -22,13 +24,14 @@ import { getStatesList } from "helpers/state.helper"; import { IQuery, IView } from "types"; import issuesService from "services/issues.service"; // fetch-keys -import { PROJECT_ISSUE_LABELS, STATES_LIST } from "constants/fetch-keys"; +import { PROJECT_ISSUE_LABELS, STATES_LIST, WORKSPACE_LABELS } from "constants/fetch-keys"; type Props = { handleFormSubmit: (values: IView) => Promise; handleClose: () => void; status: boolean; data?: IView | null; + viewType?: "workspace" | "project"; preLoadedData?: Partial | null; }; @@ -42,6 +45,7 @@ export const ViewForm: React.FC = ({ handleClose, status, data, + viewType, preLoadedData, }) => { const router = useRouter(); @@ -77,8 +81,26 @@ export const ViewForm: React.FC = ({ ? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString()) : null ); + + const { data: workspaceLabels } = useSWR( + workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null, + workspaceSlug ? () => issuesService.getWorkspaceLabels(workspaceSlug.toString()) : null + ); + + const labelOptions = viewType === "workspace" ? workspaceLabels : labels; + const { members } = useProjectMembers(workspaceSlug?.toString(), projectId?.toString()); + const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? ""); + + const memberOptions = + viewType === "workspace" + ? workspaceMembers?.map((m) => m.member) + : members?.map((m) => m.member); + + const { projects: allProjects } = useProjects(); + const joinedProjects = allProjects?.filter((p) => p.is_member); + const handleCreateUpdateView = async (formData: IView) => { await handleFormSubmit(formData); @@ -91,12 +113,14 @@ export const ViewForm: React.FC = ({ setValue("query", { assignees: null, created_by: null, + subscriber: null, labels: null, priority: null, state: null, + state_group: null, start_date: null, target_date: null, - type: null, + project: null, }); }; @@ -185,9 +209,10 @@ export const ViewForm: React.FC = ({
    m.member)} + labels={labelOptions} + members={memberOptions} states={states} + project={joinedProjects} clearAllFilters={clearAllFilters} setFilters={(query: any) => { setValue("query", { diff --git a/web/components/views/modal.tsx b/web/components/views/modal.tsx index c1ff54231..03f4f0b60 100644 --- a/web/components/views/modal.tsx +++ b/web/components/views/modal.tsx @@ -8,6 +8,7 @@ import { mutate } from "swr"; import { Dialog, Transition } from "@headlessui/react"; // services import viewsService from "services/views.service"; +import workspaceService from "services/workspace.service"; // hooks import useToast from "hooks/use-toast"; // components @@ -15,10 +16,11 @@ import { ViewForm } from "components/views"; // types import { ICurrentUserResponse, IView } from "types"; // fetch-keys -import { VIEWS_LIST } from "constants/fetch-keys"; +import { VIEWS_LIST, WORKSPACE_VIEWS_LIST } from "constants/fetch-keys"; type Props = { isOpen: boolean; + viewType: "project" | "workspace"; handleClose: () => void; data?: IView | null; preLoadedData?: Partial | null; @@ -27,6 +29,7 @@ type Props = { export const CreateUpdateViewModal: React.FC = ({ isOpen, + viewType, handleClose, data, preLoadedData, @@ -46,25 +49,48 @@ export const CreateUpdateViewModal: React.FC = ({ ...payload, query_data: payload.query, }; - await viewsService - .createView(workspaceSlug as string, projectId as string, payload, user) - .then(() => { - mutate(VIEWS_LIST(projectId as string)); - handleClose(); - setToastAlert({ - type: "success", - title: "Success!", - message: "View created successfully.", + if (viewType === "project") { + await viewsService + .createView(workspaceSlug as string, projectId as string, payload, user) + .then(() => { + mutate(VIEWS_LIST(projectId as string)); + handleClose(); + + setToastAlert({ + type: "success", + title: "Success!", + message: "View created successfully.", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "View could not be created. Please try again.", + }); }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "View could not be created. Please try again.", + } else { + await workspaceService + .createView(workspaceSlug as string, payload) + .then(() => { + mutate(WORKSPACE_VIEWS_LIST(workspaceSlug as string)); + handleClose(); + + setToastAlert({ + type: "success", + title: "Success!", + message: "View created successfully.", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "View could not be created. Please try again.", + }); }); - }); + } }; const updateView = async (payload: IView) => { @@ -72,41 +98,79 @@ export const CreateUpdateViewModal: React.FC = ({ ...payload, query_data: payload.query, }; - await viewsService - .updateView(workspaceSlug as string, projectId as string, data?.id ?? "", payloadData, user) - .then((res) => { - mutate( - VIEWS_LIST(projectId as string), - (prevData) => - prevData?.map((p) => { - if (p.id === res.id) return { ...p, ...payloadData }; + if (viewType === "project") { + await viewsService + .updateView(workspaceSlug as string, projectId as string, data?.id ?? "", payloadData, user) + .then((res) => { + mutate( + VIEWS_LIST(projectId as string), + (prevData) => + prevData?.map((p) => { + if (p.id === res.id) return { ...p, ...payloadData }; - return p; - }), - false - ); - onClose(); + return p; + }), + false + ); + onClose(); - setToastAlert({ - type: "success", - title: "Success!", - message: "View updated successfully.", + setToastAlert({ + type: "success", + title: "Success!", + message: "View updated successfully.", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "View could not be updated. Please try again.", + }); }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "View could not be updated. Please try again.", + } else { + await workspaceService + .updateView(workspaceSlug as string, data?.id ?? "", payloadData) + .then((res) => { + mutate( + WORKSPACE_VIEWS_LIST(workspaceSlug as string), + (prevData) => + prevData?.map((p) => { + if (p.id === res.id) return { ...p, ...payloadData }; + + return p; + }), + false + ); + onClose(); + + setToastAlert({ + type: "success", + title: "Success!", + message: "View updated successfully.", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "View could not be updated. Please try again.", + }); }); - }); + } }; const handleFormSubmit = async (formData: IView) => { - if (!workspaceSlug || !projectId) return; + if (viewType === "project") { + if (!workspaceSlug || !projectId) return; - if (!data) await createView(formData); - else await updateView(formData); + if (!data) await createView(formData); + else await updateView(formData); + } else { + if (!workspaceSlug) return; + + if (!data) await createView(formData); + else await updateView(formData); + } }; return ( @@ -141,6 +205,7 @@ export const CreateUpdateViewModal: React.FC = ({ handleClose={handleClose} status={data ? true : false} data={data} + viewType={viewType} preLoadedData={preLoadedData} /> diff --git a/web/components/views/select-filters.tsx b/web/components/views/select-filters.tsx index 52671f41f..7b1324f5f 100644 --- a/web/components/views/select-filters.tsx +++ b/web/components/views/select-filters.tsx @@ -4,6 +4,9 @@ import { useRouter } from "next/router"; import useSWR from "swr"; +// hook +import useProjects from "hooks/use-projects"; +import useWorkspaceMembers from "hooks/use-workspace-members"; // services import stateService from "services/state.service"; import projectService from "services/project.service"; @@ -18,15 +21,20 @@ import { PriorityIcon, StateGroupIcon } from "components/icons"; import { getStatesList } from "helpers/state.helper"; import { checkIfArraysHaveSameElements } from "helpers/array.helper"; // types -import { IIssueFilterOptions, IQuery } from "types"; +import { IIssueFilterOptions, TStateGroups } from "types"; // fetch-keys -import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys"; +import { + PROJECT_ISSUE_LABELS, + PROJECT_MEMBERS, + STATES_LIST, + WORKSPACE_LABELS, +} from "constants/fetch-keys"; // constants -import { PRIORITIES } from "constants/project"; +import { GROUP_CHOICES, PRIORITIES } from "constants/project"; import { DATE_FILTER_OPTIONS } from "constants/filters"; type Props = { - filters: Partial | IQuery; + filters: Partial; onSelect: (option: any) => void; direction?: "left" | "right"; height?: "sm" | "md" | "rg" | "lg"; @@ -48,7 +56,7 @@ export const SelectFilters: React.FC = ({ }); const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId, workspaceViewId } = router.query; const { data: states } = useSWR( workspaceSlug && projectId ? STATES_LIST(projectId as string) : null, @@ -58,6 +66,20 @@ export const SelectFilters: React.FC = ({ ); const statesList = getStatesList(states); + const workspaceViewPathName = [ + "workspace-views", + "workspace-views/all-issues", + "workspace-views/assigned", + "workspace-views/created", + "workspace-views/subscribed", + ]; + + const isWorkspaceViewPath = workspaceViewPathName.some((pathname) => + router.pathname.includes(pathname) + ); + + const isWorkspaceView = isWorkspaceViewPath || workspaceViewId; + const { data: members } = useSWR( projectId ? PROJECT_MEMBERS(projectId as string) : null, workspaceSlug && projectId @@ -65,6 +87,8 @@ export const SelectFilters: React.FC = ({ : null ); + const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? ""); + const { data: issueLabels } = useSWR( projectId ? PROJECT_ISSUE_LABELS(projectId.toString()) : null, workspaceSlug && projectId @@ -72,6 +96,413 @@ export const SelectFilters: React.FC = ({ : null ); + const { data: workspaceLabels } = useSWR( + workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null, + workspaceSlug ? () => issuesService.getWorkspaceLabels(workspaceSlug.toString()) : null + ); + + const { projects: allProjects } = useProjects(); + const joinedProjects = allProjects?.filter((p) => p.is_member); + + const projectFilterOption = [ + { + id: "priority", + label: "Priority", + value: PRIORITIES, + hasChildren: true, + children: PRIORITIES.map((priority) => ({ + id: priority === null ? "null" : priority, + label: ( +
    + + {priority ?? "None"} +
    + ), + value: { + key: "priority", + value: priority === null ? "null" : priority, + }, + selected: filters?.priority?.includes(priority === null ? "null" : priority), + })), + }, + { + id: "state", + label: "State", + value: statesList, + hasChildren: true, + children: statesList?.map((state) => ({ + id: state.id, + label: ( +
    + + {state.name} +
    + ), + value: { + key: "state", + value: state.id, + }, + selected: filters?.state?.includes(state.id), + })), + }, + { + id: "assignees", + label: "Assignees", + value: members, + hasChildren: true, + children: members?.map((member) => ({ + id: member.member.id, + label: ( +
    + + {member.member.display_name} +
    + ), + value: { + key: "assignees", + value: member.member.id, + }, + selected: filters?.assignees?.includes(member.member.id), + })), + }, + { + id: "created_by", + label: "Created by", + value: members, + hasChildren: true, + children: members?.map((member) => ({ + id: member.member.id, + label: ( +
    + + {member.member.display_name} +
    + ), + value: { + key: "created_by", + value: member.member.id, + }, + selected: filters?.created_by?.includes(member.member.id), + })), + }, + { + id: "labels", + label: "Labels", + value: issueLabels, + hasChildren: true, + children: issueLabels?.map((label) => ({ + id: label.id, + label: ( +
    +
    + {label.name} +
    + ), + value: { + key: "labels", + value: label.id, + }, + selected: filters?.labels?.includes(label.id), + })), + }, + { + id: "start_date", + label: "Start date", + value: DATE_FILTER_OPTIONS, + hasChildren: true, + children: [ + ...DATE_FILTER_OPTIONS.map((option) => ({ + id: option.name, + label: option.name, + value: { + key: "start_date", + value: option.value, + }, + selected: checkIfArraysHaveSameElements(filters?.start_date ?? [], option.value), + })), + { + id: "custom", + label: "Custom", + value: "custom", + element: ( + + ), + }, + ], + }, + { + id: "target_date", + label: "Due date", + value: DATE_FILTER_OPTIONS, + hasChildren: true, + children: [ + ...DATE_FILTER_OPTIONS.map((option) => ({ + id: option.name, + label: option.name, + value: { + key: "target_date", + value: option.value, + }, + selected: checkIfArraysHaveSameElements(filters?.target_date ?? [], option.value), + })), + { + id: "custom", + label: "Custom", + value: "custom", + element: ( + + ), + }, + ], + }, + ]; + + const workspaceFilterOption = [ + { + id: "project", + label: "Project", + value: joinedProjects, + hasChildren: true, + children: joinedProjects?.map((project) => ({ + id: project.id, + label:
    {project.name}
    , + value: { + key: "project", + value: project.id, + }, + selected: filters?.project?.includes(project.id), + })), + }, + { + id: "state_group", + label: "State groups", + value: GROUP_CHOICES, + hasChildren: true, + children: [ + ...Object.keys(GROUP_CHOICES).map((key) => ({ + id: key, + label: ( +
    + + {GROUP_CHOICES[key as keyof typeof GROUP_CHOICES]} +
    + ), + value: { + key: "state_group", + value: key, + }, + selected: filters?.state?.includes(key), + })), + ], + }, + { + id: "labels", + label: "Labels", + value: workspaceLabels, + hasChildren: true, + children: workspaceLabels?.map((label) => ({ + id: label.id, + label: ( +
    +
    + {label.name} +
    + ), + value: { + key: "labels", + value: label.id, + }, + selected: filters?.labels?.includes(label.id), + })), + }, + { + id: "priority", + label: "Priority", + value: PRIORITIES, + hasChildren: true, + children: PRIORITIES.map((priority) => ({ + id: priority === null ? "null" : priority, + label: ( +
    + + {priority ?? "None"} +
    + ), + value: { + key: "priority", + value: priority === null ? "null" : priority, + }, + selected: filters?.priority?.includes(priority === null ? "null" : priority), + })), + }, + { + id: "created_by", + label: "Created by", + value: workspaceMembers, + hasChildren: true, + children: workspaceMembers?.map((member) => ({ + id: member.member.id, + label: ( +
    + + {member.member.display_name} +
    + ), + value: { + key: "created_by", + value: member.member.id, + }, + selected: filters?.created_by?.includes(member.member.id), + })), + }, + { + id: "assignees", + label: "Assignees", + value: workspaceMembers, + hasChildren: true, + children: workspaceMembers?.map((member) => ({ + id: member.member.id, + label: ( +
    + + {member.member.display_name} +
    + ), + value: { + key: "assignees", + value: member.member.id, + }, + selected: filters?.assignees?.includes(member.member.id), + })), + }, + { + id: "subscriber", + label: "Subscriber", + value: workspaceMembers, + hasChildren: true, + children: workspaceMembers?.map((member) => ({ + id: member.member.id, + label: ( +
    + + {member.member.display_name} +
    + ), + value: { + key: "subscriber", + value: member.member.id, + }, + selected: filters?.subscriber?.includes(member.member.id), + })), + }, + { + id: "start_date", + label: "Start date", + value: DATE_FILTER_OPTIONS, + hasChildren: true, + children: [ + ...DATE_FILTER_OPTIONS.map((option) => ({ + id: option.name, + label: option.name, + value: { + key: "start_date", + value: option.value, + }, + selected: checkIfArraysHaveSameElements(filters?.start_date ?? [], option.value), + })), + { + id: "custom", + label: "Custom", + value: "custom", + element: ( + + ), + }, + ], + }, + { + id: "target_date", + label: "Due date", + value: DATE_FILTER_OPTIONS, + hasChildren: true, + children: [ + ...DATE_FILTER_OPTIONS.map((option) => ({ + id: option.name, + label: option.name, + value: { + key: "target_date", + value: option.value, + }, + selected: checkIfArraysHaveSameElements(filters?.target_date ?? [], option.value), + })), + { + id: "custom", + label: "Custom", + value: "custom", + element: ( + + ), + }, + ], + }, + ]; + + const filterOption = isWorkspaceView ? workspaceFilterOption : projectFilterOption; + return ( <> {isDateFilterModalOpen && ( @@ -89,185 +520,7 @@ export const SelectFilters: React.FC = ({ onSelect={onSelect} direction={direction} height={height} - options={[ - { - id: "priority", - label: "Priority", - value: PRIORITIES, - hasChildren: true, - children: PRIORITIES.map((priority) => ({ - id: priority === null ? "null" : priority, - label: ( -
    - - {priority ?? "None"} -
    - ), - value: { - key: "priority", - value: priority === null ? "null" : priority, - }, - selected: filters?.priority?.includes(priority === null ? "null" : priority), - })), - }, - { - id: "state", - label: "State", - value: statesList, - hasChildren: true, - children: statesList?.map((state) => ({ - id: state.id, - label: ( -
    - - {state.name} -
    - ), - value: { - key: "state", - value: state.id, - }, - selected: filters?.state?.includes(state.id), - })), - }, - { - id: "assignees", - label: "Assignees", - value: members, - hasChildren: true, - children: members?.map((member) => ({ - id: member.member.id, - label: ( -
    - - {member.member.display_name} -
    - ), - value: { - key: "assignees", - value: member.member.id, - }, - selected: filters?.assignees?.includes(member.member.id), - })), - }, - { - id: "created_by", - label: "Created by", - value: members, - hasChildren: true, - children: members?.map((member) => ({ - id: member.member.id, - label: ( -
    - - {member.member.display_name} -
    - ), - value: { - key: "created_by", - value: member.member.id, - }, - selected: filters?.created_by?.includes(member.member.id), - })), - }, - { - id: "labels", - label: "Labels", - value: issueLabels, - hasChildren: true, - children: issueLabels?.map((label) => ({ - id: label.id, - label: ( -
    -
    - {label.name} -
    - ), - value: { - key: "labels", - value: label.id, - }, - selected: filters?.labels?.includes(label.id), - })), - }, - { - id: "start_date", - label: "Start date", - value: DATE_FILTER_OPTIONS, - hasChildren: true, - children: [ - ...DATE_FILTER_OPTIONS.map((option) => ({ - id: option.name, - label: option.name, - value: { - key: "start_date", - value: option.value, - }, - selected: checkIfArraysHaveSameElements(filters?.start_date ?? [], option.value), - })), - { - id: "custom", - label: "Custom", - value: "custom", - element: ( - - ), - }, - ], - }, - { - id: "target_date", - label: "Due date", - value: DATE_FILTER_OPTIONS, - hasChildren: true, - children: [ - ...DATE_FILTER_OPTIONS.map((option) => ({ - id: option.name, - label: option.name, - value: { - key: "target_date", - value: option.value, - }, - selected: checkIfArraysHaveSameElements(filters?.target_date ?? [], option.value), - })), - { - id: "custom", - label: "Custom", - value: "custom", - element: ( - - ), - }, - ], - }, - ]} + options={filterOption} /> ); diff --git a/web/components/views/single-view-item.tsx b/web/components/views/single-view-item.tsx index a6f81912c..d27eb3cf1 100644 --- a/web/components/views/single-view-item.tsx +++ b/web/components/views/single-view-item.tsx @@ -5,9 +5,9 @@ import { useRouter } from "next/router"; // icons import { TrashIcon, StarIcon, PencilIcon } from "@heroicons/react/24/outline"; -import { StackedLayersIcon } from "components/icons"; +import { PhotoFilterOutlined } from "@mui/icons-material"; //components -import { CustomMenu, Tooltip } from "components/ui"; +import { CustomMenu } from "components/ui"; // services import viewsService from "services/views.service"; // types @@ -18,15 +18,20 @@ import { VIEWS_LIST } from "constants/fetch-keys"; import useToast from "hooks/use-toast"; // helpers import { truncateText } from "helpers/string.helper"; -import { renderShortDateWithYearFormat, render24HourFormatTime } from "helpers/date-time.helper"; type Props = { view: IView; + viewType: "project" | "workspace"; handleEditView: () => void; handleDeleteView: () => void; }; -export const SingleViewItem: React.FC = ({ view, handleEditView, handleDeleteView }) => { +export const SingleViewItem: React.FC = ({ + view, + viewType, + handleEditView, + handleDeleteView, +}) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -82,38 +87,46 @@ export const SingleViewItem: React.FC = ({ view, handleEditView, handleDe }); }; + const viewRedirectionUrl = + viewType === "project" + ? `/${workspaceSlug}/projects/${projectId}/views/${view.id}` + : `/${workspaceSlug}/workspace-views/${view.id}`; + return ( -
    - - -
    -
    -
    - -

    {truncateText(view.name, 75)}

    +
    + + +
    +
    +
    +
    -
    -
    -

    - {Object.keys(view.query_data) - .map((key: string) => - view.query_data[key as keyof typeof view.query_data] !== null - ? (view.query_data[key as keyof typeof view.query_data] as any).length - : 0 - ) - .reduce((curr, prev) => curr + prev, 0)}{" "} - filters -

    - -

    - {render24HourFormatTime(view.updated_at)} -

    -
    - {view.is_favorite ? ( +
    +

    + {truncateText(view.name, 75)} +

    + {view?.description && ( +

    {view.description}

    + )} +
    +
    +
    +
    +

    + {Object.keys(view.query_data) + .map((key: string) => + view.query_data[key as keyof typeof view.query_data] !== null + ? (view.query_data[key as keyof typeof view.query_data] as any).length + : 0 + ) + .reduce((curr, prev) => curr + prev, 0)}{" "} + filters +

    + + {viewType === "project" ? ( + view.is_favorite ? ( - )} - - { - e.preventDefault(); - e.stopPropagation(); - handleEditView(); - }} - > - - - Edit View - - - { - e.preventDefault(); - e.stopPropagation(); - handleDeleteView(); - }} - > - - - Delete View - - - -
    + ) + ) : null} + + { + e.preventDefault(); + e.stopPropagation(); + handleEditView(); + }} + > + + + Edit View + + + { + e.preventDefault(); + e.stopPropagation(); + handleDeleteView(); + }} + > + + + Delete View + + +
    - {view?.description && ( -

    - {view.description} -

    - )}
    diff --git a/web/components/workspace/sidebar-menu.tsx b/web/components/workspace/sidebar-menu.tsx index 946f4b708..df38ea8af 100644 --- a/web/components/workspace/sidebar-menu.tsx +++ b/web/components/workspace/sidebar-menu.tsx @@ -34,8 +34,8 @@ const workspaceLinks = (workspaceSlug: string) => [ }, { Icon: TaskAltOutlined, - name: "My Issues", - href: `/${workspaceSlug}/me/my-issues`, + name: "Issues", + href: `/${workspaceSlug}/workspace-views/all-issues`, }, ]; diff --git a/web/components/workspace/views/workpace-view-navigation.tsx b/web/components/workspace/views/workpace-view-navigation.tsx new file mode 100644 index 000000000..c15943e8a --- /dev/null +++ b/web/components/workspace/views/workpace-view-navigation.tsx @@ -0,0 +1,92 @@ +import React from "react"; + +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// icon +import { PlusIcon } from "lucide-react"; +// constant +import { WORKSPACE_VIEWS_LIST } from "constants/fetch-keys"; +// service +import workspaceService from "services/workspace.service"; + +type Props = { + handleAddView: () => void; +}; + +export const WorkspaceViewsNavigation: React.FC = ({ handleAddView }) => { + const router = useRouter(); + const { workspaceSlug, workspaceViewId } = router.query; + + const { data: workspaceViews } = useSWR( + workspaceSlug ? WORKSPACE_VIEWS_LIST(workspaceSlug.toString()) : null, + workspaceSlug ? () => workspaceService.getAllViews(workspaceSlug.toString()) : null + ); + + const isSelected = (pathName: string) => router.pathname.includes(pathName); + + const tabsList = [ + { + key: "all", + label: "All Issues", + selected: isSelected("workspace-views/all-issues"), + onClick: () => router.push(`/${workspaceSlug}/workspace-views/all-issues`), + }, + { + key: "assigned", + label: "Assigned", + selected: isSelected("workspace-views/assigned"), + onClick: () => router.push(`/${workspaceSlug}/workspace-views/assigned`), + }, + { + key: "created", + label: "Created", + selected: isSelected("workspace-views/created"), + onClick: () => router.push(`/${workspaceSlug}/workspace-views/created`), + }, + { + key: "subscribed", + label: "Subscribed", + selected: isSelected("workspace-views/subscribed"), + onClick: () => router.push(`/${workspaceSlug}/workspace-views/subscribed`), + }, + ]; + + return ( +
    + {tabsList.map((tab) => ( + + ))} + {workspaceViews && + workspaceViews.length > 0 && + workspaceViews?.map((view) => ( + + ))} + + +
    + ); +}; diff --git a/web/constants/fetch-keys.ts b/web/constants/fetch-keys.ts index 0f0643c66..75107a0bb 100644 --- a/web/constants/fetch-keys.ts +++ b/web/constants/fetch-keys.ts @@ -4,6 +4,7 @@ import { IAnalyticsParams, IJiraMetadata, INotificationParams } from "types"; const paramsToKey = (params: any) => { const { state, + state_group, priority, assignees, created_by, @@ -12,9 +13,12 @@ const paramsToKey = (params: any) => { target_date, sub_issue, start_target_date, + project, } = params; + let projectKey = project ? project.split(",") : []; let stateKey = state ? state.split(",") : []; + let stateGroupKey = state_group ? state_group.split(",") : []; let priorityKey = priority ? priority.split(",") : []; let assigneesKey = assignees ? assignees.split(",") : []; let createdByKey = created_by ? created_by.split(",") : []; @@ -27,13 +31,15 @@ const paramsToKey = (params: any) => { const orderBy = params.order_by ? params.order_by.toUpperCase() : "NULL"; // sorting each keys in ascending order + projectKey = projectKey.sort().join("_"); stateKey = stateKey.sort().join("_"); + stateGroupKey = stateGroupKey.sort().join("_"); priorityKey = priorityKey.sort().join("_"); assigneesKey = assigneesKey.sort().join("_"); createdByKey = createdByKey.sort().join("_"); labelsKey = labelsKey.sort().join("_"); - return `${stateKey}_${priorityKey}_${assigneesKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${startDateKey}_${targetDateKey}_${sub_issue}_${startTargetDate}`; + return `${projectKey}_${stateGroupKey}_${stateKey}_${priorityKey}_${assigneesKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${startDateKey}_${targetDateKey}_${sub_issue}_${startTargetDate}`; }; const inboxParamsToKey = (params: any) => { @@ -149,6 +155,18 @@ export const PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS = (projectId: string, params? return `PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS${projectId.toUpperCase()}_${paramsKey}`; }; +export const WORKSPACE_VIEWS_LIST = (workspaceSlug: string) => + `WORKSPACE_VIEWS_LIST_${workspaceSlug.toUpperCase()}`; +export const WORKSPACE_VIEW_DETAILS = (workspaceViewId: string) => + `WORKSPACE_VIEW_DETAILS_${workspaceViewId.toUpperCase()}`; +export const WORKSPACE_VIEW_ISSUES = (workspaceViewId: string, params?: any) => { + if (!params) return `WORKSPACE_VIEW_ISSUES_${workspaceViewId.toUpperCase()}`; + + const paramsKey = paramsToKey(params); + + return `WORKSPACE_VIEW_ISSUES_${workspaceViewId.toUpperCase()}_${paramsKey.toUpperCase()}`; +}; + export const PROJECT_ISSUES_DETAILS = (issueId: string) => `PROJECT_ISSUES_DETAILS_${issueId.toUpperCase()}`; export const PROJECT_ISSUES_PROPERTIES = (projectId: string) => diff --git a/web/constants/project.ts b/web/constants/project.ts index b8e3be9d6..2f15b74bc 100644 --- a/web/constants/project.ts +++ b/web/constants/project.ts @@ -21,6 +21,8 @@ export const GROUP_CHOICES = { cancelled: "Cancelled", }; +export const STATE_GROUP = ["Backlog", "Unstarted", "Started", "Completed", "Cancelled"]; + export const PRIORITIES: TIssuePriorities[] = ["urgent", "high", "medium", "low", "none"]; export const MONTHS = [ diff --git a/web/hooks/use-sub-issue.tsx b/web/hooks/use-sub-issue.tsx index 8eb30fd0b..26b84ba5a 100644 --- a/web/hooks/use-sub-issue.tsx +++ b/web/hooks/use-sub-issue.tsx @@ -11,9 +11,9 @@ import { ISubIssueResponse } from "types"; // fetch-keys import { SUB_ISSUES } from "constants/fetch-keys"; -const useSubIssue = (issueId: string, isExpanded: boolean) => { +const useSubIssue = (projectId: string, issueId: string, isExpanded: boolean) => { const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug } = router.query; const shouldFetch = workspaceSlug && projectId && issueId && isExpanded; diff --git a/web/hooks/use-worskpace-issue-filter.tsx b/web/hooks/use-worskpace-issue-filter.tsx new file mode 100644 index 000000000..00af0a9f8 --- /dev/null +++ b/web/hooks/use-worskpace-issue-filter.tsx @@ -0,0 +1,113 @@ +import { useEffect, useCallback } from "react"; + +import useSWR, { mutate } from "swr"; + +// services +import workspaceService from "services/workspace.service"; +// types +import { IIssueFilterOptions, IView } from "types"; +// fetch-keys +import { WORKSPACE_VIEW_DETAILS } from "constants/fetch-keys"; + +const initialValues: IIssueFilterOptions = { + assignees: null, + created_by: null, + labels: null, + priority: null, + state: null, + state_group: null, + subscriber: null, + start_date: null, + target_date: null, + project: null, +}; + +const useWorkspaceIssuesFilters = ( + workspaceSlug: string | undefined, + workspaceViewId: string | undefined +) => { + const { data: workspaceViewDetails } = useSWR( + workspaceSlug && workspaceViewId ? WORKSPACE_VIEW_DETAILS(workspaceViewId) : null, + workspaceSlug && workspaceViewId + ? () => workspaceService.getViewDetails(workspaceSlug, workspaceViewId) + : null + ); + + const saveData = useCallback( + (data: Partial) => { + if (!workspaceSlug || !workspaceViewId || !workspaceViewDetails) return; + + const oldData = { ...workspaceViewDetails }; + + mutate( + WORKSPACE_VIEW_DETAILS(workspaceViewId), + (prevData) => { + if (!prevData) return; + return { + ...prevData, + query_data: { + ...prevData?.query_data, + ...data, + }, + }; + }, + false + ); + + workspaceService.updateView(workspaceSlug, workspaceViewId, { + query_data: { + ...oldData.query_data, + ...data, + }, + }); + }, + [workspaceViewDetails, workspaceSlug, workspaceViewId] + ); + + const filters = workspaceViewDetails?.query_data ?? initialValues; + + const setFilters = useCallback( + (updatedFilter: Partial) => { + if (!workspaceViewDetails) return; + + saveData({ + ...workspaceViewDetails?.query_data, + ...updatedFilter, + }); + }, + [workspaceViewDetails, saveData] + ); + + useEffect(() => { + if (!workspaceViewDetails || !workspaceSlug || !workspaceViewId) return; + + if (!workspaceViewDetails.query_data) { + workspaceService.updateView(workspaceSlug, workspaceViewId, { + query_data: { ...initialValues }, + }); + } + }, [workspaceViewDetails, workspaceViewId, workspaceSlug]); + + const params: any = { + assignees: filters?.assignees ? filters?.assignees.join(",") : undefined, + subscriber: filters?.subscriber ? filters?.subscriber.join(",") : undefined, + state: filters?.state ? filters?.state.join(",") : undefined, + state_group: filters?.state_group ? filters?.state_group.join(",") : undefined, + priority: filters?.priority ? filters?.priority.join(",") : undefined, + labels: filters?.labels ? filters?.labels.join(",") : undefined, + created_by: filters?.created_by ? filters?.created_by.join(",") : undefined, + start_date: filters?.start_date ? filters?.start_date.join(",") : undefined, + target_date: filters?.target_date ? filters?.target_date.join(",") : undefined, + project: filters?.project ? filters?.project.join(",") : undefined, + sub_issue: false, + type: undefined, + }; + + return { + params, + filters, + setFilters, + }; +}; + +export default useWorkspaceIssuesFilters; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx index 9a5511037..ea37a777a 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx @@ -88,12 +88,14 @@ const ProjectViews: NextPage = () => { > setCreateUpdateViewModal(false)} data={selectedViewToUpdate} user={user} /> { handleEditView(view)} handleDeleteView={() => handleDeleteView(view)} /> diff --git a/web/pages/[workspaceSlug]/workspace-views/[workspaceViewId].tsx b/web/pages/[workspaceSlug]/workspace-views/[workspaceViewId].tsx new file mode 100644 index 000000000..cba4d802b --- /dev/null +++ b/web/pages/[workspaceSlug]/workspace-views/[workspaceViewId].tsx @@ -0,0 +1,322 @@ +import { useCallback, useState } from "react"; + +import { useRouter } from "next/router"; + +import useSWR, { mutate } from "swr"; + +// hook +import useToast from "hooks/use-toast"; +import useWorkspaceIssuesFilters from "hooks/use-worskpace-issue-filter"; +import useProjects from "hooks/use-projects"; +import useUser from "hooks/use-user"; +import useWorkspaceMembers from "hooks/use-workspace-members"; +// context +import { useProjectMyMembership } from "contexts/project-member.context"; +// services +import workspaceService from "services/workspace.service"; +import projectIssuesServices from "services/issues.service"; +// layouts +import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; +// components +import { FiltersList, SpreadsheetView } from "components/core"; +import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation"; +import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option"; +import { CreateUpdateViewModal } from "components/views"; +import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +// ui +import { EmptyState, PrimaryButton } from "components/ui"; +// icons +import { PlusIcon } from "@heroicons/react/24/outline"; +import { CheckCircle } from "lucide-react"; +// images +import emptyView from "public/empty-state/view.svg"; +// fetch-keys +import { + WORKSPACE_LABELS, + WORKSPACE_VIEWS_LIST, + WORKSPACE_VIEW_DETAILS, + WORKSPACE_VIEW_ISSUES, +} from "constants/fetch-keys"; +// constant +import { STATE_GROUP } from "constants/project"; +// types +import { IIssue, IIssueFilterOptions, IView } from "types"; + +const WorkspaceView: React.FC = () => { + const [createViewModal, setCreateViewModal] = useState(null); + + // create issue modal + const [createIssueModal, setCreateIssueModal] = useState(false); + const [preloadedData, setPreloadedData] = useState< + (Partial & { actionType: "createIssue" | "edit" | "delete" }) | undefined + >(undefined); + + // update issue modal + const [editIssueModal, setEditIssueModal] = useState(false); + const [issueToEdit, setIssueToEdit] = useState< + (IIssue & { actionType: "edit" | "delete" }) | undefined + >(undefined); + + // delete issue modal + const [deleteIssueModal, setDeleteIssueModal] = useState(false); + const [issueToDelete, setIssueToDelete] = useState(null); + + const router = useRouter(); + const { workspaceSlug, workspaceViewId } = router.query; + + const { memberRole } = useProjectMyMembership(); + + const { user } = useUser(); + const { setToastAlert } = useToast(); + + const { data: viewDetails, error } = useSWR( + workspaceSlug && workspaceViewId ? WORKSPACE_VIEW_DETAILS(workspaceViewId.toString()) : null, + workspaceSlug && workspaceViewId + ? () => workspaceService.getViewDetails(workspaceSlug.toString(), workspaceViewId.toString()) + : null + ); + + const { params, filters, setFilters } = useWorkspaceIssuesFilters( + workspaceSlug?.toString(), + workspaceViewId?.toString() + ); + + const { isGuest, isViewer } = useWorkspaceMembers( + workspaceSlug?.toString(), + Boolean(workspaceSlug) + ); + + const { data: viewIssues, mutate: mutateIssues } = useSWR( + workspaceSlug && viewDetails ? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), params) : null, + workspaceSlug && viewDetails + ? () => workspaceService.getViewIssues(workspaceSlug.toString(), params) + : null + ); + + const { projects: allProjects } = useProjects(); + const joinedProjects = allProjects?.filter((p) => p.is_member); + + const { data: workspaceLabels } = useSWR( + workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null, + workspaceSlug ? () => projectIssuesServices.getWorkspaceLabels(workspaceSlug.toString()) : null + ); + + const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? ""); + + const updateView = async (payload: IIssueFilterOptions) => { + const payloadData = { + query_data: payload, + }; + + await workspaceService + .updateView(workspaceSlug as string, workspaceViewId as string, payloadData) + .then((res) => { + mutate( + WORKSPACE_VIEWS_LIST(workspaceSlug as string), + (prevData) => + prevData?.map((p) => { + if (p.id === res.id) return { ...p, ...payloadData }; + + return p; + }), + false + ); + setToastAlert({ + type: "success", + title: "Success!", + message: "View updated successfully.", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "View could not be updated. Please try again.", + }); + }); + }; + + const makeIssueCopy = useCallback( + (issue: IIssue) => { + setCreateIssueModal(true); + + setPreloadedData({ ...issue, name: `${issue.name} (Copy)`, actionType: "createIssue" }); + }, + [setCreateIssueModal, setPreloadedData] + ); + + const handleEditIssue = useCallback( + (issue: IIssue) => { + setEditIssueModal(true); + setIssueToEdit({ + ...issue, + actionType: "edit", + cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null, + module: issue.issue_module ? issue.issue_module.module : null, + }); + }, + [setEditIssueModal, setIssueToEdit] + ); + + const handleDeleteIssue = useCallback( + (issue: IIssue) => { + setDeleteIssueModal(true); + setIssueToDelete(issue); + }, + [setDeleteIssueModal, setIssueToDelete] + ); + + const handleIssueAction = useCallback( + (issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => { + if (action === "copy") makeIssueCopy(issue); + else if (action === "edit") handleEditIssue(issue); + else if (action === "delete") handleDeleteIssue(issue); + }, + [makeIssueCopy, handleEditIssue, handleDeleteIssue] + ); + + const nullFilters = + filters && + Object.keys(filters).filter((key) => filters[key as keyof IIssueFilterOptions] === null); + + const areFiltersApplied = + filters && + Object.keys(filters).length > 0 && + nullFilters.length !== Object.keys(filters).length; + + const isNotAllowed = isGuest || isViewer; + + return ( + + + + {viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"} + +
    + } + right={ +
    + + { + const e = new KeyboardEvent("keydown", { key: "c" }); + document.dispatchEvent(e); + }} + > + + Add Issue + +
    + } + > + setCreateIssueModal(false)} + prePopulateData={{ + ...preloadedData, + }} + onSubmit={async () => { + mutateIssues(); + }} + /> + setEditIssueModal(false)} + data={issueToEdit} + onSubmit={async () => { + mutateIssues(); + }} + /> + setDeleteIssueModal(false)} + isOpen={deleteIssueModal} + data={issueToDelete} + user={user} + onSubmit={async () => { + mutateIssues(); + }} + /> + setCreateViewModal(null)} + viewType="workspace" + preLoadedData={createViewModal} + user={user} + /> +
    +
    + setCreateViewModal(true)} /> + {error ? ( + router.push(`/${workspaceSlug}/workspace-views`), + }} + /> + ) : ( +
    + {areFiltersApplied && ( + <> +
    + setFilters(updatedFilter)} + labels={workspaceLabels} + members={workspaceMembers?.map((m) => m.member)} + stateGroup={STATE_GROUP} + project={joinedProjects} + clearAllFilters={() => + setFilters({ + assignees: null, + created_by: null, + labels: null, + priority: null, + state_group: null, + start_date: null, + target_date: null, + subscriber: null, + project: null, + }) + } + /> + { + if (workspaceViewId) { + updateView(filters); + } else + setCreateViewModal({ + query: filters, + }); + }} + className="flex items-center gap-2 text-sm" + > + {!workspaceViewId && } + {workspaceViewId ? "Update" : "Save"} view + +
    + {
    } + + )} + +
    + )} +
    +
    + + ); +}; + +export default WorkspaceView; diff --git a/web/pages/[workspaceSlug]/workspace-views/all-issues.tsx b/web/pages/[workspaceSlug]/workspace-views/all-issues.tsx new file mode 100644 index 000000000..763784b8e --- /dev/null +++ b/web/pages/[workspaceSlug]/workspace-views/all-issues.tsx @@ -0,0 +1,283 @@ +import { useCallback, useState } from "react"; +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// hook +import useUser from "hooks/use-user"; +import useWorkspaceMembers from "hooks/use-workspace-members"; +import useProjects from "hooks/use-projects"; +import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter"; +// context +import { useProjectMyMembership } from "contexts/project-member.context"; +// services +import workspaceService from "services/workspace.service"; +import projectIssuesServices from "services/issues.service"; +// layouts +import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; +// components +import { FiltersList, SpreadsheetView } from "components/core"; +import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation"; +import { CreateUpdateViewModal } from "components/views"; +import { CreateUpdateIssueModal, DeleteIssueModal, MyIssuesViewOptions } from "components/issues"; +// ui +import { EmptyState, PrimaryButton } from "components/ui"; +// icons +import { PlusIcon } from "@heroicons/react/24/outline"; +import { CheckCircle } from "lucide-react"; +// images +import emptyView from "public/empty-state/view.svg"; +// fetch-keys +import { + WORKSPACE_LABELS, + WORKSPACE_VIEW_DETAILS, + WORKSPACE_VIEW_ISSUES, +} from "constants/fetch-keys"; +// constants +import { STATE_GROUP } from "constants/project"; +// types +import { IIssue, IIssueFilterOptions } from "types"; + +const WorkspaceViewAllIssue: React.FC = () => { + const [createViewModal, setCreateViewModal] = useState(null); + + // create issue modal + const [createIssueModal, setCreateIssueModal] = useState(false); + const [preloadedData, setPreloadedData] = useState< + (Partial & { actionType: "createIssue" | "edit" | "delete" }) | undefined + >(undefined); + + // update issue modal + const [editIssueModal, setEditIssueModal] = useState(false); + const [issueToEdit, setIssueToEdit] = useState< + (IIssue & { actionType: "edit" | "delete" }) | undefined + >(undefined); + + // delete issue modal + const [deleteIssueModal, setDeleteIssueModal] = useState(false); + const [issueToDelete, setIssueToDelete] = useState(null); + + const router = useRouter(); + const { workspaceSlug, workspaceViewId } = router.query; + + const { filters, setFilters } = useMyIssuesFilters(workspaceSlug?.toString()); + + const { user } = useUser(); + const { memberRole } = useProjectMyMembership(); + + const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? ""); + + const { data: viewDetails, error } = useSWR( + workspaceSlug && workspaceViewId ? WORKSPACE_VIEW_DETAILS(workspaceViewId.toString()) : null, + workspaceSlug && workspaceViewId + ? () => workspaceService.getViewDetails(workspaceSlug.toString(), workspaceViewId.toString()) + : null + ); + + const params: any = { + assignees: filters?.assignees ? filters?.assignees.join(",") : undefined, + subscriber: filters?.subscriber ? filters?.subscriber.join(",") : undefined, + state: filters?.state ? filters?.state.join(",") : undefined, + state_group: filters?.state_group ? filters?.state_group.join(",") : undefined, + priority: filters?.priority ? filters?.priority.join(",") : undefined, + labels: filters?.labels ? filters?.labels.join(",") : undefined, + created_by: filters?.created_by ? filters?.created_by.join(",") : undefined, + start_date: filters?.start_date ? filters?.start_date.join(",") : undefined, + target_date: filters?.target_date ? filters?.target_date.join(",") : undefined, + project: filters?.project ? filters?.project.join(",") : undefined, + sub_issue: false, + type: undefined, + }; + + const { data: viewIssues, mutate: mutateIssues } = useSWR( + workspaceSlug ? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), params) : null, + workspaceSlug ? () => workspaceService.getViewIssues(workspaceSlug.toString(), params) : null + ); + + const makeIssueCopy = useCallback( + (issue: IIssue) => { + setCreateIssueModal(true); + + setPreloadedData({ ...issue, name: `${issue.name} (Copy)`, actionType: "createIssue" }); + }, + [setCreateIssueModal, setPreloadedData] + ); + + const handleEditIssue = useCallback( + (issue: IIssue) => { + setEditIssueModal(true); + setIssueToEdit({ + ...issue, + actionType: "edit", + cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null, + module: issue.issue_module ? issue.issue_module.module : null, + }); + }, + [setEditIssueModal, setIssueToEdit] + ); + + const handleDeleteIssue = useCallback( + (issue: IIssue) => { + setDeleteIssueModal(true); + setIssueToDelete(issue); + }, + [setDeleteIssueModal, setIssueToDelete] + ); + + const handleIssueAction = useCallback( + (issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => { + if (action === "copy") makeIssueCopy(issue); + else if (action === "edit") handleEditIssue(issue); + else if (action === "delete") handleDeleteIssue(issue); + }, + [makeIssueCopy, handleEditIssue, handleDeleteIssue] + ); + + const nullFilters = + filters && + Object.keys(filters).filter((key) => filters[key as keyof IIssueFilterOptions] === null); + + const areFiltersApplied = + filters && + Object.keys(filters).length > 0 && + nullFilters.length !== Object.keys(filters).length; + + const { projects: allProjects } = useProjects(); + const joinedProjects = allProjects?.filter((p) => p.is_member); + + const { data: workspaceLabels } = useSWR( + workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null, + workspaceSlug ? () => projectIssuesServices.getWorkspaceLabels(workspaceSlug.toString()) : null + ); + + return ( + + + + {viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"} + +
    + } + right={ +
    + + { + const e = new KeyboardEvent("keydown", { key: "c" }); + document.dispatchEvent(e); + }} + > + + Add Issue + +
    + } + > + setCreateIssueModal(false)} + prePopulateData={{ + ...preloadedData, + }} + onSubmit={async () => { + mutateIssues(); + }} + /> + setEditIssueModal(false)} + data={issueToEdit} + onSubmit={async () => { + mutateIssues(); + }} + /> + setDeleteIssueModal(false)} + isOpen={deleteIssueModal} + data={issueToDelete} + user={user} + onSubmit={async () => { + mutateIssues(); + }} + /> + setCreateViewModal(null)} + viewType="workspace" + preLoadedData={createViewModal} + user={user} + /> +
    +
    + setCreateViewModal(true)} /> + {error ? ( + router.push(`/${workspaceSlug}/workspace-views`), + }} + /> + ) : ( +
    + {areFiltersApplied && ( + <> +
    + setFilters(updatedFilter)} + labels={workspaceLabels} + members={workspaceMembers?.map((m) => m.member)} + stateGroup={STATE_GROUP} + project={joinedProjects} + clearAllFilters={() => + setFilters({ + assignees: null, + created_by: null, + labels: null, + priority: null, + state_group: null, + start_date: null, + target_date: null, + subscriber: null, + project: null, + }) + } + /> + { + setCreateViewModal({ + query: filters, + }); + }} + className="flex items-center gap-2 text-sm" + > + {!workspaceViewId && } + {workspaceViewId ? "Update" : "Save"} view + +
    + {
    } + + )} + +
    + )} +
    +
    + + ); +}; + +export default WorkspaceViewAllIssue; diff --git a/web/pages/[workspaceSlug]/workspace-views/assigned.tsx b/web/pages/[workspaceSlug]/workspace-views/assigned.tsx new file mode 100644 index 000000000..0cfd5a534 --- /dev/null +++ b/web/pages/[workspaceSlug]/workspace-views/assigned.tsx @@ -0,0 +1,205 @@ +import { useCallback, useState } from "react"; +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// hook +import useUser from "hooks/use-user"; +// context +import { useProjectMyMembership } from "contexts/project-member.context"; +// services +import workspaceService from "services/workspace.service"; +// layouts +import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; +// components +import { SpreadsheetView } from "components/core"; +import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation"; +import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option"; +import { CreateUpdateViewModal } from "components/views"; +import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +// ui +import { EmptyState, PrimaryButton } from "components/ui"; +// icons +import { PlusIcon } from "@heroicons/react/24/outline"; +import { CheckCircle } from "lucide-react"; +// images +import emptyView from "public/empty-state/view.svg"; +// fetch-keys +import { WORKSPACE_VIEW_DETAILS, WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys"; +// types +import { IIssue } from "types"; + +const WorkspaceViewAssignedIssue: React.FC = () => { + const [createViewModal, setCreateViewModal] = useState(null); + + // create issue modal + const [createIssueModal, setCreateIssueModal] = useState(false); + const [preloadedData, setPreloadedData] = useState< + (Partial & { actionType: "createIssue" | "edit" | "delete" }) | undefined + >(undefined); + + // update issue modal + const [editIssueModal, setEditIssueModal] = useState(false); + const [issueToEdit, setIssueToEdit] = useState< + (IIssue & { actionType: "edit" | "delete" }) | undefined + >(undefined); + + // delete issue modal + const [deleteIssueModal, setDeleteIssueModal] = useState(false); + const [issueToDelete, setIssueToDelete] = useState(null); + + const router = useRouter(); + const { workspaceSlug, workspaceViewId } = router.query; + + const { user } = useUser(); + + const { memberRole } = useProjectMyMembership(); + + const params: any = { + assignees: user?.id ?? undefined, + sub_issue: false, + }; + + const { data: viewDetails, error } = useSWR( + workspaceSlug && workspaceViewId ? WORKSPACE_VIEW_DETAILS(workspaceViewId.toString()) : null, + workspaceSlug && workspaceViewId + ? () => workspaceService.getViewDetails(workspaceSlug.toString(), workspaceViewId.toString()) + : null + ); + + const { data: viewIssues, mutate: mutateIssues } = useSWR( + workspaceSlug ? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), params) : null, + workspaceSlug ? () => workspaceService.getViewIssues(workspaceSlug.toString(), params) : null + ); + + const makeIssueCopy = useCallback( + (issue: IIssue) => { + setCreateIssueModal(true); + + setPreloadedData({ ...issue, name: `${issue.name} (Copy)`, actionType: "createIssue" }); + }, + [setCreateIssueModal, setPreloadedData] + ); + + const handleEditIssue = useCallback( + (issue: IIssue) => { + setEditIssueModal(true); + setIssueToEdit({ + ...issue, + actionType: "edit", + cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null, + module: issue.issue_module ? issue.issue_module.module : null, + }); + }, + [setEditIssueModal, setIssueToEdit] + ); + + const handleDeleteIssue = useCallback( + (issue: IIssue) => { + setDeleteIssueModal(true); + setIssueToDelete(issue); + }, + [setDeleteIssueModal, setIssueToDelete] + ); + + const handleIssueAction = useCallback( + (issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => { + if (action === "copy") makeIssueCopy(issue); + else if (action === "edit") handleEditIssue(issue); + else if (action === "delete") handleDeleteIssue(issue); + }, + [makeIssueCopy, handleEditIssue, handleDeleteIssue] + ); + + return ( + + + + {viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"} + +
    + } + right={ +
    + + { + const e = new KeyboardEvent("keydown", { key: "c" }); + document.dispatchEvent(e); + }} + > + + Add Issue + +
    + } + > + setCreateIssueModal(false)} + prePopulateData={{ + ...preloadedData, + }} + onSubmit={async () => { + mutateIssues(); + }} + /> + setEditIssueModal(false)} + data={issueToEdit} + onSubmit={async () => { + mutateIssues(); + }} + /> + setDeleteIssueModal(false)} + isOpen={deleteIssueModal} + data={issueToDelete} + user={user} + onSubmit={async () => { + mutateIssues(); + }} + /> + setCreateViewModal(null)} + viewType="workspace" + preLoadedData={createViewModal} + user={user} + /> +
    +
    + setCreateViewModal(true)} /> + {error ? ( + router.push(`/${workspaceSlug}/workspace-views`), + }} + /> + ) : ( +
    + +
    + )} +
    +
    + + ); +}; + +export default WorkspaceViewAssignedIssue; diff --git a/web/pages/[workspaceSlug]/workspace-views/created.tsx b/web/pages/[workspaceSlug]/workspace-views/created.tsx new file mode 100644 index 000000000..d41db871f --- /dev/null +++ b/web/pages/[workspaceSlug]/workspace-views/created.tsx @@ -0,0 +1,205 @@ +import { useCallback, useState } from "react"; + +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// hook +import useUser from "hooks/use-user"; +// context +import { useProjectMyMembership } from "contexts/project-member.context"; +// services +import workspaceService from "services/workspace.service"; +// layouts +import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; +// components +import { SpreadsheetView } from "components/core"; +import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation"; +import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option"; +import { CreateUpdateViewModal } from "components/views"; +import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +// ui +import { EmptyState, PrimaryButton } from "components/ui"; +// icons +import { PlusIcon } from "@heroicons/react/24/outline"; +import { CheckCircle } from "lucide-react"; +// images +import emptyView from "public/empty-state/view.svg"; +// fetch-keys +import { WORKSPACE_VIEW_DETAILS, WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys"; +// types +import { IIssue } from "types"; + +const WorkspaceViewCreatedIssue: React.FC = () => { + const [createViewModal, setCreateViewModal] = useState(null); + + // create issue modal + const [createIssueModal, setCreateIssueModal] = useState(false); + const [preloadedData, setPreloadedData] = useState< + (Partial & { actionType: "createIssue" | "edit" | "delete" }) | undefined + >(undefined); + + // update issue modal + const [editIssueModal, setEditIssueModal] = useState(false); + const [issueToEdit, setIssueToEdit] = useState< + (IIssue & { actionType: "edit" | "delete" }) | undefined + >(undefined); + + // delete issue modal + const [deleteIssueModal, setDeleteIssueModal] = useState(false); + const [issueToDelete, setIssueToDelete] = useState(null); + + const router = useRouter(); + const { workspaceSlug, workspaceViewId } = router.query; + + const { user } = useUser(); + const { memberRole } = useProjectMyMembership(); + + const params: any = { + created_by: user?.id ?? undefined, + sub_issue: false, + }; + + const { data: viewDetails, error } = useSWR( + workspaceSlug && workspaceViewId ? WORKSPACE_VIEW_DETAILS(workspaceViewId.toString()) : null, + workspaceSlug && workspaceViewId + ? () => workspaceService.getViewDetails(workspaceSlug.toString(), workspaceViewId.toString()) + : null + ); + + const { data: viewIssues, mutate: mutateIssues } = useSWR( + workspaceSlug ? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), params) : null, + workspaceSlug ? () => workspaceService.getViewIssues(workspaceSlug.toString(), params) : null + ); + + const makeIssueCopy = useCallback( + (issue: IIssue) => { + setCreateIssueModal(true); + + setPreloadedData({ ...issue, name: `${issue.name} (Copy)`, actionType: "createIssue" }); + }, + [setCreateIssueModal, setPreloadedData] + ); + + const handleEditIssue = useCallback( + (issue: IIssue) => { + setEditIssueModal(true); + setIssueToEdit({ + ...issue, + actionType: "edit", + cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null, + module: issue.issue_module ? issue.issue_module.module : null, + }); + }, + [setEditIssueModal, setIssueToEdit] + ); + + const handleDeleteIssue = useCallback( + (issue: IIssue) => { + setDeleteIssueModal(true); + setIssueToDelete(issue); + }, + [setDeleteIssueModal, setIssueToDelete] + ); + + const handleIssueAction = useCallback( + (issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => { + if (action === "copy") makeIssueCopy(issue); + else if (action === "edit") handleEditIssue(issue); + else if (action === "delete") handleDeleteIssue(issue); + }, + [makeIssueCopy, handleEditIssue, handleDeleteIssue] + ); + + return ( + + + + {viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"} + +
    + } + right={ +
    + + { + const e = new KeyboardEvent("keydown", { key: "c" }); + document.dispatchEvent(e); + }} + > + + Add Issue + +
    + } + > + setCreateIssueModal(false)} + prePopulateData={{ + ...preloadedData, + }} + onSubmit={async () => { + mutateIssues(); + }} + /> + setEditIssueModal(false)} + data={issueToEdit} + onSubmit={async () => { + mutateIssues(); + }} + /> + setDeleteIssueModal(false)} + isOpen={deleteIssueModal} + data={issueToDelete} + user={user} + onSubmit={async () => { + mutateIssues(); + }} + /> + setCreateViewModal(null)} + viewType="workspace" + preLoadedData={createViewModal} + user={user} + /> +
    +
    + setCreateViewModal(true)} /> + {error ? ( + router.push(`/${workspaceSlug}/workspace-views`), + }} + /> + ) : ( +
    + +
    + )} +
    +
    + + ); +}; + +export default WorkspaceViewCreatedIssue; diff --git a/web/pages/[workspaceSlug]/workspace-views/index.tsx b/web/pages/[workspaceSlug]/workspace-views/index.tsx new file mode 100644 index 000000000..30d8925b6 --- /dev/null +++ b/web/pages/[workspaceSlug]/workspace-views/index.tsx @@ -0,0 +1,213 @@ +import React, { useState } from "react"; + +import Link from "next/link"; + +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// services +import workspaceService from "services/workspace.service"; +// hooks +import useUser from "hooks/use-user"; +// layouts +import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; +// components +import { CreateUpdateViewModal, DeleteViewModal, SingleViewItem } from "components/views"; +import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option"; +// ui +import { EmptyState, Input, Loader, PrimaryButton } from "components/ui"; +// icons +import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { PlusIcon } from "lucide-react"; +import { PhotoFilterOutlined } from "@mui/icons-material"; +// image +import emptyView from "public/empty-state/view.svg"; +// types +import type { NextPage } from "next"; +import { IView } from "types"; +// constants +import { WORKSPACE_VIEWS_LIST } from "constants/fetch-keys"; +// helper +import { truncateText } from "helpers/string.helper"; + +const WorkspaceViews: NextPage = () => { + const [query, setQuery] = useState(""); + + const [createUpdateViewModal, setCreateUpdateViewModal] = useState(false); + const [selectedViewToUpdate, setSelectedViewToUpdate] = useState(null); + + const [deleteViewModal, setDeleteViewModal] = useState(false); + const [selectedViewToDelete, setSelectedViewToDelete] = useState(null); + + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { user } = useUser(); + + const { data: workspaceViews } = useSWR( + workspaceSlug ? WORKSPACE_VIEWS_LIST(workspaceSlug as string) : null, + workspaceSlug ? () => workspaceService.getAllViews(workspaceSlug as string) : null + ); + + const defaultWorkspaceViewsList = [ + { + key: "all", + label: "All Issues", + href: `/${workspaceSlug}/workspace-views/all-issues`, + }, + { + key: "assigned", + label: "Assigned", + href: `/${workspaceSlug}/workspace-views/assigned`, + }, + { + key: "created", + label: "Created", + href: `/${workspaceSlug}/workspace-views/created`, + }, + { + key: "subscribed", + label: "Subscribed", + href: `/${workspaceSlug}/workspace-views/subscribed`, + }, + ]; + + const filteredDefaultOptions = + query === "" + ? defaultWorkspaceViewsList + : defaultWorkspaceViewsList?.filter((option) => + option.label.toLowerCase().includes(query.toLowerCase()) + ); + + const filteredOptions = + query === "" + ? workspaceViews + : workspaceViews?.filter((option) => option.name.toLowerCase().includes(query.toLowerCase())); + + const handleEditView = (view: IView) => { + setSelectedViewToUpdate(view); + setCreateUpdateViewModal(true); + }; + + const handleDeleteView = (view: IView) => { + setSelectedViewToDelete(view); + setDeleteViewModal(true); + }; + + return ( + + Workspace Views +
    + } + right={ +
    + + + setCreateUpdateViewModal(true)} + > + + New View + +
    + } + > + { + setCreateUpdateViewModal(false); + setSelectedViewToUpdate(null); + }} + data={selectedViewToUpdate} + viewType="workspace" + user={user} + /> + +
    +
    +
    + + setQuery(e.target.value)} + placeholder="Search" + mode="trueTransparent" + /> +
    +
    + {filteredDefaultOptions && + filteredDefaultOptions.length > 0 && + filteredDefaultOptions.map((option) => ( + + ))} + + {filteredOptions ? ( + filteredOptions.length > 0 ? ( +
    + {filteredOptions.map((view) => ( + handleEditView(view)} + handleDeleteView={() => handleDeleteView(view)} + /> + ))} +
    + ) : ( + , + text: "New View", + onClick: () => setCreateUpdateViewModal(true), + }} + /> + ) + ) : ( + + + + + + + + )} +
    + + ); +}; + +export default WorkspaceViews; diff --git a/web/pages/[workspaceSlug]/workspace-views/subscribed.tsx b/web/pages/[workspaceSlug]/workspace-views/subscribed.tsx new file mode 100644 index 000000000..7a96b41ed --- /dev/null +++ b/web/pages/[workspaceSlug]/workspace-views/subscribed.tsx @@ -0,0 +1,205 @@ +import { useCallback, useState } from "react"; + +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// hook +import useUser from "hooks/use-user"; +// context +import { useProjectMyMembership } from "contexts/project-member.context"; +// services +import workspaceService from "services/workspace.service"; +// layouts +import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; +// components +import { SpreadsheetView } from "components/core"; +import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation"; +import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option"; +import { CreateUpdateViewModal } from "components/views"; +import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +// ui +import { EmptyState, PrimaryButton } from "components/ui"; +// icons +import { PlusIcon } from "@heroicons/react/24/outline"; +import { CheckCircle } from "lucide-react"; +// images +import emptyView from "public/empty-state/view.svg"; +// fetch-keys +import { WORKSPACE_VIEW_DETAILS, WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys"; +// types +import { IIssue } from "types"; + +const WorkspaceViewSubscribedIssue: React.FC = () => { + const [createViewModal, setCreateViewModal] = useState(null); + + // create issue modal + const [createIssueModal, setCreateIssueModal] = useState(false); + const [preloadedData, setPreloadedData] = useState< + (Partial & { actionType: "createIssue" | "edit" | "delete" }) | undefined + >(undefined); + + // update issue modal + const [editIssueModal, setEditIssueModal] = useState(false); + const [issueToEdit, setIssueToEdit] = useState< + (IIssue & { actionType: "edit" | "delete" }) | undefined + >(undefined); + + // delete issue modal + const [deleteIssueModal, setDeleteIssueModal] = useState(false); + const [issueToDelete, setIssueToDelete] = useState(null); + + const router = useRouter(); + const { workspaceSlug, workspaceViewId } = router.query; + + const { user } = useUser(); + const { memberRole } = useProjectMyMembership(); + + const params: any = { + subscriber: user?.id ?? undefined, + sub_issue: false, + }; + + const { data: viewDetails, error } = useSWR( + workspaceSlug && workspaceViewId ? WORKSPACE_VIEW_DETAILS(workspaceViewId.toString()) : null, + workspaceSlug && workspaceViewId + ? () => workspaceService.getViewDetails(workspaceSlug.toString(), workspaceViewId.toString()) + : null + ); + + const { data: viewIssues, mutate: mutateIssues } = useSWR( + workspaceSlug ? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), params) : null, + workspaceSlug ? () => workspaceService.getViewIssues(workspaceSlug.toString(), params) : null + ); + + const makeIssueCopy = useCallback( + (issue: IIssue) => { + setCreateIssueModal(true); + + setPreloadedData({ ...issue, name: `${issue.name} (Copy)`, actionType: "createIssue" }); + }, + [setCreateIssueModal, setPreloadedData] + ); + + const handleEditIssue = useCallback( + (issue: IIssue) => { + setEditIssueModal(true); + setIssueToEdit({ + ...issue, + actionType: "edit", + cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null, + module: issue.issue_module ? issue.issue_module.module : null, + }); + }, + [setEditIssueModal, setIssueToEdit] + ); + + const handleDeleteIssue = useCallback( + (issue: IIssue) => { + setDeleteIssueModal(true); + setIssueToDelete(issue); + }, + [setDeleteIssueModal, setIssueToDelete] + ); + + const handleIssueAction = useCallback( + (issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => { + if (action === "copy") makeIssueCopy(issue); + else if (action === "edit") handleEditIssue(issue); + else if (action === "delete") handleDeleteIssue(issue); + }, + [makeIssueCopy, handleEditIssue, handleDeleteIssue] + ); + + return ( + + + + {viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"} + +
    + } + right={ +
    + + { + const e = new KeyboardEvent("keydown", { key: "c" }); + document.dispatchEvent(e); + }} + > + + Add Issue + +
    + } + > + setCreateIssueModal(false)} + prePopulateData={{ + ...preloadedData, + }} + onSubmit={async () => { + mutateIssues(); + }} + /> + setEditIssueModal(false)} + data={issueToEdit} + onSubmit={async () => { + mutateIssues(); + }} + /> + setDeleteIssueModal(false)} + isOpen={deleteIssueModal} + data={issueToDelete} + user={user} + onSubmit={async () => { + mutateIssues(); + }} + /> + setCreateViewModal(null)} + viewType="workspace" + preLoadedData={createViewModal} + user={user} + /> +
    +
    + setCreateViewModal(true)} /> + {error ? ( + router.push(`/${workspaceSlug}/workspace-views`), + }} + /> + ) : ( +
    + +
    + )} +
    +
    + + ); +}; + +export default WorkspaceViewSubscribedIssue; diff --git a/web/services/workspace.service.ts b/web/services/workspace.service.ts index 57b724fda..a3aa56507 100644 --- a/web/services/workspace.service.ts +++ b/web/services/workspace.service.ts @@ -14,6 +14,8 @@ import { ICurrentUserResponse, IWorkspaceBulkInviteFormData, IWorkspaceViewProps, + IView, + IIssueFilterOptions, } from "types"; class WorkspaceService extends APIService { @@ -261,6 +263,56 @@ class WorkspaceService extends APIService { throw error?.response?.data; }); } + + async createView(workspaceSlug: string, data: IView): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/views/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async updateView(workspaceSlug: string, viewId: string, data: Partial): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/views/${viewId}/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteView(workspaceSlug: string, viewId: string): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/views/${viewId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getAllViews(workspaceSlug: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/views/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getViewDetails(workspaceSlug: string, viewId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/views/${viewId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getViewIssues(workspaceSlug: string, params: IIssueFilterOptions): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/issues/`, { + params, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } } export default new WorkspaceService(); diff --git a/web/types/view-props.d.ts b/web/types/view-props.d.ts index a02fc5540..ca5ce47ab 100644 --- a/web/types/view-props.d.ts +++ b/web/types/view-props.d.ts @@ -35,9 +35,10 @@ export interface IIssueFilterOptions { priority?: string[] | null; start_date?: string[] | null; state?: string[] | null; - state_group?: TStateGroups[] | null; + state_group?: string[] | null; subscriber?: string[] | null; target_date?: string[] | null; + project?: string[] | null; } export interface IIssueDisplayFilterOptions { diff --git a/web/types/views.d.ts b/web/types/views.d.ts index e1246af5a..e9741a270 100644 --- a/web/types/views.d.ts +++ b/web/types/views.d.ts @@ -1,3 +1,5 @@ +import { IIssueFilterOptions } from "./view-props"; + export interface IView { id: string; access: string; @@ -8,10 +10,15 @@ export interface IView { updated_by: string; name: string; description: string; - query: IQuery; - query_data: IQuery; + query: IIssueFilterOptions; + query_data: IIssueFilterOptions; project: string; workspace: string; + workspace_detail: { + id: string; + name: string; + slug: string; + }; } export interface IQuery { @@ -23,4 +30,5 @@ export interface IQuery { start_date: string[] | null; target_date: string[] | null; type: "active" | "backlog" | null; + project: string[] | null; } diff --git a/web/types/workspace.d.ts b/web/types/workspace.d.ts index de0bc93e2..66e1e1273 100644 --- a/web/types/workspace.d.ts +++ b/web/types/workspace.d.ts @@ -1,13 +1,4 @@ -import type { - IIssueFilterOptions, - IProjectMember, - IUser, - IUserMemberLite, - IWorkspaceViewProps, - TIssueGroupByOptions, - TIssueOrderByOptions, - TIssueViewOptions, -} from "types"; +import type { IProjectMember, IUser, IUserMemberLite, IWorkspaceViewProps } from "types"; export interface IWorkspace { readonly id: string; From 2d8cbccfbca869f663e2ed2de58ea9b32875f0bc Mon Sep 17 00:00:00 2001 From: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Date: Wed, 27 Sep 2023 08:51:29 +0530 Subject: [PATCH 21/35] style: gantt layout quick-add padding (#2272) * fix: 'Last Drafted Issue' making sidebar look weird on collapsed * feat: scroll to the bottom when issue is created * fix: 'Add Issue' button overlapping issue card in spreadsheet view * fix: wrong placement of quick-add in calender layout * fix: spacing for issue card in spreadsheet view * style: gantt layout quick-add padding style: removed 'State group' from draft issue * style: decrese shadow, quick-add position on calender layout, and 'add issue' sticky * style: button color --- .../core/filters/issues-view-filter.tsx | 5 +- .../board-view/inline-create-issue-form.tsx | 2 +- .../core/views/calendar-view/calendar.tsx | 2 +- .../inline-create-issue-form.tsx | 6 +- .../core/views/calendar-view/single-date.tsx | 32 ++-- .../inline-create-issue-form.tsx | 2 +- .../list-view/inline-create-issue-form.tsx | 2 +- .../spreadsheet-view/spreadsheet-view.tsx | 147 +++++++++++++----- web/components/gantt-chart/chart/index.tsx | 48 ++++++ web/components/gantt-chart/sidebar.tsx | 47 +----- 10 files changed, 187 insertions(+), 106 deletions(-) diff --git a/web/components/core/filters/issues-view-filter.tsx b/web/components/core/filters/issues-view-filter.tsx index 2ad165dd8..02cdf2122 100644 --- a/web/components/core/filters/issues-view-filter.tsx +++ b/web/components/core/filters/issues-view-filter.tsx @@ -67,7 +67,7 @@ export const IssuesFilterView: React.FC = () => { const router = useRouter(); const { workspaceSlug, projectId, viewId } = router.query; const isArchivedIssues = router.pathname.includes("archived-issues"); - const isDraftIssues = router.pathname.includes("draft-issues"); + const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues"; const { displayFilters, @@ -230,6 +230,9 @@ export const IssuesFilterView: React.FC = () => { return null; if (option.key === "project") return null; + if (isDraftIssues && option.key === "state_detail.group") + return null; + return ( { export const BoardInlineCreateIssueForm: React.FC = (props) => ( <> diff --git a/web/components/core/views/calendar-view/calendar.tsx b/web/components/core/views/calendar-view/calendar.tsx index 8fbe35305..f339f7cf2 100644 --- a/web/components/core/views/calendar-view/calendar.tsx +++ b/web/components/core/views/calendar-view/calendar.tsx @@ -184,7 +184,7 @@ export const CalendarView: React.FC = ({
    = (props) => { <>
    diff --git a/web/components/core/views/calendar-view/single-date.tsx b/web/components/core/views/calendar-view/single-date.tsx index 02ea56678..3e38155f1 100644 --- a/web/components/core/views/calendar-view/single-date.tsx +++ b/web/components/core/views/calendar-view/single-date.tsx @@ -39,6 +39,8 @@ export const SingleCalendarDate: React.FC = (props) => { const [showAllIssues, setShowAllIssues] = useState(false); const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false); + const [formPosition, setFormPosition] = useState({ x: 0, y: 0 }); + const totalIssues = date.issues.length; return ( @@ -81,16 +83,23 @@ export const SingleCalendarDate: React.FC = (props) => { ))} - setIsCreateIssueFormOpen(false)} - prePopulatedData={{ - target_date: date.date, - ...(cycleId && { cycle: cycleId.toString() }), - ...(moduleId && { module: moduleId.toString() }), +
    + > + setIsCreateIssueFormOpen(false)} + prePopulatedData={{ + target_date: date.date, + ...(cycleId && { cycle: cycleId.toString() }), + ...(moduleId && { module: moduleId.toString() }), + }} + /> +
    {totalIssues > 4 && ( + ) + : !disableUserActions && + !isInlineCreateIssueFormOpen && ( + + + Add Issue + + } + position="left" + verticalPosition="top" + optionsClassName="left-5 !w-36" + noBorder + > + setIsInlineCreateIssueFormOpen(true)}> + Create new + + {openIssuesListModal && ( + + Add an existing issue + + )} + + )} +
    +
    ); diff --git a/web/components/gantt-chart/chart/index.tsx b/web/components/gantt-chart/chart/index.tsx index aa79ae19c..a8f541b0b 100644 --- a/web/components/gantt-chart/chart/index.tsx +++ b/web/components/gantt-chart/chart/index.tsx @@ -1,4 +1,6 @@ import { FC, useEffect, useState } from "react"; +// next +import { useRouter } from "next/router"; // icons import { ArrowsPointingInIcon, ArrowsPointingOutIcon } from "@heroicons/react/20/solid"; // components @@ -11,6 +13,8 @@ import { GanttSidebar } from "../sidebar"; import { MonthChartView } from "./month"; // import { QuarterChartView } from "./quarter"; // import { YearChartView } from "./year"; +// icons +import { PlusIcon } from "lucide-react"; // views import { // generateHourChart, @@ -25,6 +29,7 @@ import { getNumberOfDaysBetweenTwoDatesInYear, getMonthChartItemPositionWidthInMonth, } from "../views"; +import { GanttInlineCreateIssueForm } from "components/core/views/gantt-chart-view/inline-create-issue-form"; // types import { ChartDataType, IBlockUpdateData, IGanttBlock, TGanttViews } from "../types"; // data @@ -64,12 +69,17 @@ export const ChartViewRoot: FC = ({ const [itemsContainerWidth, setItemsContainerWidth] = useState(0); const [fullScreenMode, setFullScreenMode] = useState(false); + const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false); + // blocks state management starts const [chartBlocks, setChartBlocks] = useState(null); const { currentView, currentViewData, renderView, dispatch, allViews, updateScrollLeft } = useChart(); + const router = useRouter(); + const { cycleId, moduleId } = router.query; + const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) => blocks && blocks.length > 0 ? blocks.map((block: any) => ({ @@ -304,6 +314,44 @@ export const ChartViewRoot: FC = ({ SidebarBlockRender={SidebarBlockRender} enableReorder={enableReorder} /> + {chartBlocks && ( +
    + setIsCreateIssueFormOpen(false)} + onSuccess={() => { + const ganttSidebar = document.getElementById(`gantt-sidebar-${cycleId}`); + + const timeoutId = setTimeout(() => { + if (ganttSidebar) + ganttSidebar.scrollBy({ + top: ganttSidebar.scrollHeight, + left: 0, + behavior: "smooth", + }); + clearTimeout(timeoutId); + }, 10); + }} + prePopulatedData={{ + start_date: new Date(Date.now()).toISOString().split("T")[0], + target_date: new Date(Date.now() + 86400000).toISOString().split("T")[0], + ...(cycleId && { cycle: cycleId.toString() }), + ...(moduleId && { module: moduleId.toString() }), + }} + /> + + {!isCreateIssueFormOpen && ( + + )} +
    + )}
    = (props) => { const { title, blockUpdateHandler, blocks, SidebarBlockRender, enableReorder } = props; const router = useRouter(); - const { cycleId, moduleId } = router.query; + const { cycleId } = router.query; const { activeBlock, dispatch } = useChart(); - const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false); - // update the active block on hover const updateActiveBlock = (block: IGanttBlock | null) => { dispatch({ @@ -93,7 +86,7 @@ export const GanttSidebar: React.FC = (props) => { {(droppableProvided) => (
    @@ -159,42 +152,6 @@ export const GanttSidebar: React.FC = (props) => {
    )} -
    - setIsCreateIssueFormOpen(false)} - onSuccess={() => { - const ganttSidebar = document.getElementById(`gantt-sidebar-${cycleId}`); - - const timeoutId = setTimeout(() => { - if (ganttSidebar) - ganttSidebar.scrollBy({ - top: ganttSidebar.scrollHeight, - left: 0, - behavior: "smooth", - }); - clearTimeout(timeoutId); - }, 10); - }} - prePopulatedData={{ - start_date: new Date(Date.now()).toISOString().split("T")[0], - target_date: new Date(Date.now() + 86400000).toISOString().split("T")[0], - ...(cycleId && { cycle: cycleId.toString() }), - ...(moduleId && { module: moduleId.toString() }), - }} - /> - - {!isCreateIssueFormOpen && ( - - )} -
    ); }; From 5298f1e53c936c11ed268558112facba7ffda32a Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 27 Sep 2023 13:08:35 +0530 Subject: [PATCH 22/35] fix: block click happening while moving (#2275) --- web/components/gantt-chart/helpers/draggable.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/web/components/gantt-chart/helpers/draggable.tsx b/web/components/gantt-chart/helpers/draggable.tsx index b665bf5d3..79dc2a72a 100644 --- a/web/components/gantt-chart/helpers/draggable.tsx +++ b/web/components/gantt-chart/helpers/draggable.tsx @@ -179,11 +179,6 @@ export const ChartDraggable: React.FC = ({ if (e.button !== 0) return; - e.preventDefault(); - e.stopPropagation(); - - setIsMoving(true); - const resizableDiv = resizableRef.current; const columnWidth = currentViewData.data.width; @@ -193,6 +188,8 @@ export const ChartDraggable: React.FC = ({ let initialMarginLeft = parseInt(resizableDiv.style.marginLeft); const handleMouseMove = (e: MouseEvent) => { + setIsMoving(true); + let delWidth = 0; delWidth = checkScrollEnd(e); @@ -295,7 +292,9 @@ export const ChartDraggable: React.FC = ({ )}
    From b3be363b00c217297e536831a6280270ef311f00 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 27 Sep 2023 14:41:32 +0530 Subject: [PATCH 23/35] chore: handle calendar date range in frontend (#2277) --- .../views/calendar-view/calendar-header.tsx | 285 +++++++----------- .../core/views/calendar-view/calendar.tsx | 90 ++---- .../core/views/calendar-view/single-date.tsx | 126 ++++---- web/contexts/issue-view.context.tsx | 1 - web/helpers/calendar.helper.ts | 12 - web/hooks/use-calendar-issues-view.tsx | 18 +- web/types/view-props.d.ts | 1 - 7 files changed, 208 insertions(+), 325 deletions(-) diff --git a/web/components/core/views/calendar-view/calendar-header.tsx b/web/components/core/views/calendar-view/calendar-header.tsx index 271423cd4..fd69ed443 100644 --- a/web/components/core/views/calendar-view/calendar-header.tsx +++ b/web/components/core/views/calendar-view/calendar-header.tsx @@ -5,25 +5,12 @@ import { Popover, Transition } from "@headlessui/react"; // ui import { CustomMenu, ToggleSwitch } from "components/ui"; // icons -import { - CheckIcon, - ChevronDownIcon, - ChevronLeftIcon, - ChevronRightIcon, -} from "@heroicons/react/24/outline"; +import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline"; // helpers import { - addMonths, - addSevenDaysToDate, formatDate, - getCurrentWeekEndDate, - getCurrentWeekStartDate, isSameMonth, isSameYear, - lastDayOfWeek, - startOfWeek, - subtract7DaysToDate, - subtractMonths, updateDateWithMonth, updateDateWithYear, } from "helpers/calendar.helper"; @@ -31,190 +18,136 @@ import { import { MONTHS_LIST, YEARS_LIST } from "constants/calendar"; type Props = { - isMonthlyView: boolean; - setIsMonthlyView: React.Dispatch>; currentDate: Date; setCurrentDate: React.Dispatch>; showWeekEnds: boolean; setShowWeekEnds: React.Dispatch>; - changeDateRange: (startDate: Date, endDate: Date) => void; }; export const CalendarHeader: React.FC = ({ - setIsMonthlyView, - isMonthlyView, currentDate, setCurrentDate, showWeekEnds, setShowWeekEnds, - changeDateRange, -}) => { - const updateDate = (date: Date) => { - setCurrentDate(date); +}) => ( +
    +
    + + {({ open }) => ( + <> + +
    + {formatDate(currentDate, "Month")}{" "} + {formatDate(currentDate, "yyyy")} +
    +
    - changeDateRange(startOfWeek(date), lastDayOfWeek(date)); - }; - - return ( -
    -
    - - {({ open }) => ( - <> - -
    - {formatDate(currentDate, "Month")}{" "} - {formatDate(currentDate, "yyyy")} + + +
    + {YEARS_LIST.map((year) => ( + + ))}
    - +
    + {MONTHS_LIST.map((month) => ( + + ))} +
    +
    +
    + + )} + - - -
    - {YEARS_LIST.map((year) => ( - - ))} -
    -
    - {MONTHS_LIST.map((month) => ( - - ))} -
    -
    -
    - - )} - - -
    - - -
    -
    - -
    +
    +
    - } + const nextMonthFirstDate = new Date(nextMonthYear, nextMonthMonth, 1); + + setCurrentDate(nextMonthFirstDate); + }} > - { - setIsMonthlyView(true); - changeDateRange(startOfWeek(currentDate), lastDayOfWeek(currentDate)); - }} - className="w-52 text-sm text-custom-text-200" - > -
    - Monthly View - -
    -
    - { - setIsMonthlyView(false); - changeDateRange( - getCurrentWeekStartDate(currentDate), - getCurrentWeekEndDate(currentDate) - ); - }} - className="w-52 text-sm text-custom-text-200" - > -
    - Weekly View - -
    -
    -
    -

    Show weekends

    - setShowWeekEnds(!showWeekEnds)} /> -
    - + +
    - ); -}; + +
    + + + + Options +
    + } + > +
    +

    Show weekends

    + setShowWeekEnds(!showWeekEnds)} /> +
    + +
    +
    +); export default CalendarHeader; diff --git a/web/components/core/views/calendar-view/calendar.tsx b/web/components/core/views/calendar-view/calendar.tsx index f339f7cf2..7758f64cd 100644 --- a/web/components/core/views/calendar-view/calendar.tsx +++ b/web/components/core/views/calendar-view/calendar.tsx @@ -1,10 +1,6 @@ import React, { useEffect, useState } from "react"; - import { useRouter } from "next/router"; - import { mutate } from "swr"; - -// react-beautiful-dnd import { DragDropContext, DropResult } from "react-beautiful-dnd"; // services import issuesService from "services/issues.service"; @@ -50,31 +46,27 @@ export const CalendarView: React.FC = ({ userAuth, }) => { const [showWeekEnds, setShowWeekEnds] = useState(false); - const [currentDate, setCurrentDate] = useState(new Date()); - const [isMonthlyView, setIsMonthlyView] = useState(true); + + const { calendarIssues, mutateIssues, params, activeMonthDate, setActiveMonthDate } = + useCalendarIssuesView(); const [calendarDates, setCalendarDates] = useState({ - startDate: startOfWeek(currentDate), - endDate: lastDayOfWeek(currentDate), + startDate: startOfWeek(activeMonthDate), + endDate: lastDayOfWeek(activeMonthDate), }); const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; - const { calendarIssues, mutateIssues, params, displayFilters, setDisplayFilters } = - useCalendarIssuesView(); - - const totalDate = eachDayOfInterval({ - start: calendarDates.startDate, - end: calendarDates.endDate, - }); - - const onlyWeekDays = weekDayInterval({ - start: calendarDates.startDate, - end: calendarDates.endDate, - }); - - const currentViewDays = showWeekEnds ? totalDate : onlyWeekDays; + const currentViewDays = showWeekEnds + ? eachDayOfInterval({ + start: calendarDates.startDate, + end: calendarDates.endDate, + }) + : weekDayInterval({ + start: calendarDates.startDate, + end: calendarDates.endDate, + }); const currentViewDaysData = currentViewDays.map((date: Date) => { const filterIssue = @@ -148,27 +140,12 @@ export const CalendarView: React.FC = ({ .then(() => mutate(fetchKey)); }; - const changeDateRange = (startDate: Date, endDate: Date) => { - setCalendarDates({ - startDate, - endDate, - }); - - setDisplayFilters({ - calendar_date_range: `${renderDateFormat(startDate)};after,${renderDateFormat( - endDate - )};before`, - }); - }; - useEffect(() => { - if (!displayFilters || displayFilters.calendar_date_range === "") - setDisplayFilters({ - calendar_date_range: `${renderDateFormat( - startOfWeek(currentDate) - )};after,${renderDateFormat(lastDayOfWeek(currentDate))};before`, - }); - }, [currentDate, displayFilters, setDisplayFilters]); + setCalendarDates({ + startDate: startOfWeek(activeMonthDate), + endDate: lastDayOfWeek(activeMonthDate), + }); + }, [activeMonthDate]); const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions; @@ -188,13 +165,10 @@ export const CalendarView: React.FC = ({ className="h-full rounded-lg p-8 text-custom-text-200" >
    = ({ {weeks.map((date, index) => (
    - - {isMonthlyView - ? formatDate(date, "eee").substring(0, 3) - : formatDate(date, "eee")} - - {!isMonthlyView && {formatDate(date, "d")}} + {formatDate(date, "eee").substring(0, 3)}
    ))}
    @@ -239,7 +198,6 @@ export const CalendarView: React.FC = ({ date={date} handleIssueAction={handleIssueAction} addIssueToDate={addIssueToDate} - isMonthlyView={isMonthlyView} showWeekEnds={showWeekEnds} user={user} isNotAllowed={isNotAllowed} diff --git a/web/components/core/views/calendar-view/single-date.tsx b/web/components/core/views/calendar-view/single-date.tsx index 3e38155f1..bee67b5cd 100644 --- a/web/components/core/views/calendar-view/single-date.tsx +++ b/web/components/core/views/calendar-view/single-date.tsx @@ -24,14 +24,13 @@ type Props = { issues: IIssue[]; }; addIssueToDate: (date: string) => void; - isMonthlyView: boolean; showWeekEnds: boolean; user: ICurrentUserResponse | undefined; isNotAllowed: boolean; }; export const SingleCalendarDate: React.FC = (props) => { - const { handleIssueAction, date, index, isMonthlyView, showWeekEnds, user, isNotAllowed } = props; + const { handleIssueAction, date, index, showWeekEnds, user, isNotAllowed } = props; const router = useRouter(); const { cycleId, moduleId } = router.query; @@ -51,8 +50,6 @@ export const SingleCalendarDate: React.FC = (props) => { ref={provided.innerRef} {...provided.droppableProps} className={`group relative flex min-h-[150px] flex-col gap-1.5 border-t border-custom-border-200 p-2.5 text-left text-sm font-medium hover:bg-custom-background-90 ${ - isMonthlyView ? "" : "pt-9" - } ${ showWeekEnds ? (index + 1) % 7 === 0 ? "" @@ -62,71 +59,72 @@ export const SingleCalendarDate: React.FC = (props) => { : "border-r" }`} > - {isMonthlyView && {formatDate(new Date(date.date), "d")}} - {totalIssues > 0 && - date.issues.slice(0, showAllIssues ? totalIssues : 4).map((issue: IIssue, index) => ( - - {(provided, snapshot) => ( - handleIssueAction(issue, "edit")} - handleDeleteIssue={() => handleIssueAction(issue, "delete")} - user={user} - isNotAllowed={isNotAllowed} - /> - )} - - ))} - -
    - setIsCreateIssueFormOpen(false)} - prePopulatedData={{ - target_date: date.date, - ...(cycleId && { cycle: cycleId.toString() }), - ...(moduleId && { module: moduleId.toString() }), + <> + {formatDate(new Date(date.date), "d")} + {totalIssues > 0 && + date.issues.slice(0, showAllIssues ? totalIssues : 4).map((issue: IIssue, index) => ( + + {(provided, snapshot) => ( + handleIssueAction(issue, "edit")} + handleDeleteIssue={() => handleIssueAction(issue, "delete")} + user={user} + isNotAllowed={isNotAllowed} + /> + )} + + ))} +
    -
    - - {totalIssues > 4 && ( - - )} + setIsCreateIssueFormOpen(false)} + prePopulatedData={{ + target_date: date.date, + ...(cycleId && { cycle: cycleId.toString() }), + ...(moduleId && { module: moduleId.toString() }), + }} + /> +
    -
    - + )} + +
    - - Add issue - -
    + +
    - {provided.placeholder} + {provided.placeholder} +
    )} diff --git a/web/contexts/issue-view.context.tsx b/web/contexts/issue-view.context.tsx index 786d60413..6e961cd1e 100644 --- a/web/contexts/issue-view.context.tsx +++ b/web/contexts/issue-view.context.tsx @@ -48,7 +48,6 @@ type ReducerFunctionType = (state: StateType, action: ReducerActionType) => Stat export const initialState: StateType = { display_filters: { - calendar_date_range: "", group_by: null, layout: "list", order_by: "-created_at", diff --git a/web/helpers/calendar.helper.ts b/web/helpers/calendar.helper.ts index ea553e170..c037fca38 100644 --- a/web/helpers/calendar.helper.ts +++ b/web/helpers/calendar.helper.ts @@ -112,18 +112,6 @@ export const formatDate = (date: Date, format: string): string => { return formattedDate; }; -export const subtractMonths = (date: Date, numMonths: number) => { - const result = new Date(date); - result.setMonth(result.getMonth() - numMonths); - return result; -}; - -export const addMonths = (date: Date, numMonths: number) => { - const result = new Date(date); - result.setMonth(result.getMonth() + numMonths); - return result; -}; - export const updateDateWithYear = (yearString: string, date: Date) => { const year = parseInt(yearString); const month = date.getMonth(); diff --git a/web/hooks/use-calendar-issues-view.tsx b/web/hooks/use-calendar-issues-view.tsx index 289aed2ac..1f6f04ea8 100644 --- a/web/hooks/use-calendar-issues-view.tsx +++ b/web/hooks/use-calendar-issues-view.tsx @@ -1,4 +1,4 @@ -import { useContext } from "react"; +import { useContext, useState } from "react"; import { useRouter } from "next/router"; @@ -10,6 +10,8 @@ import { issueViewContext } from "contexts/issue-view.context"; import issuesService from "services/issues.service"; import cyclesService from "services/cycles.service"; import modulesService from "services/modules.service"; +// helpers +import { renderDateFormat } from "helpers/date-time.helper"; // types import { IIssue } from "types"; // fetch-keys @@ -23,13 +25,17 @@ import { const useCalendarIssuesView = () => { const { display_filters: displayFilters, - setDisplayFilters, filters, setFilters, resetFilterToDefault, setNewFilterDefaultView, } = useContext(issueViewContext); + const [activeMonthDate, setActiveMonthDate] = useState(new Date()); + + const firstDayOfMonth = new Date(activeMonthDate.getFullYear(), activeMonthDate.getMonth(), 1); + const lastDayOfMonth = new Date(activeMonthDate.getFullYear(), activeMonthDate.getMonth() + 1, 0); + const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; @@ -41,7 +47,9 @@ const useCalendarIssuesView = () => { labels: filters?.labels ? filters?.labels.join(",") : undefined, created_by: filters?.created_by ? filters?.created_by.join(",") : undefined, start_date: filters?.start_date ? filters?.start_date.join(",") : undefined, - target_date: displayFilters?.calendar_date_range, + target_date: `${renderDateFormat(firstDayOfMonth)};after,${renderDateFormat( + lastDayOfMonth + )};before`, }; const { data: projectCalendarIssues, mutate: mutateProjectCalendarIssues } = useSWR( @@ -101,8 +109,8 @@ const useCalendarIssuesView = () => { : (projectCalendarIssues as IIssue[]); return { - displayFilters, - setDisplayFilters, + activeMonthDate, + setActiveMonthDate, calendarIssues: calendarIssues ?? [], mutateIssues: cycleId ? mutateCycleCalendarIssues diff --git a/web/types/view-props.d.ts b/web/types/view-props.d.ts index ca5ce47ab..4cd6d63fd 100644 --- a/web/types/view-props.d.ts +++ b/web/types/view-props.d.ts @@ -42,7 +42,6 @@ export interface IIssueFilterOptions { } export interface IIssueDisplayFilterOptions { - calendar_date_range?: string; group_by?: TIssueGroupByOptions; layout?: TIssueViewOptions; order_by?: TIssueOrderByOptions; From a243bb6a159b13f9b03c44eabf8792d23c0a83a4 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 27 Sep 2023 14:53:26 +0530 Subject: [PATCH 24/35] chore: gantt chart empty state (#2279) * chore: gantt empty state * chore: Add heading to the gantt sidebar --- web/components/gantt-chart/chart/index.tsx | 9 ++++-- web/components/gantt-chart/sidebar.tsx | 31 +++++++++++++------- web/components/issues/gantt-chart/blocks.tsx | 11 ++----- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/web/components/gantt-chart/chart/index.tsx b/web/components/gantt-chart/chart/index.tsx index a8f541b0b..c564f69f2 100644 --- a/web/components/gantt-chart/chart/index.tsx +++ b/web/components/gantt-chart/chart/index.tsx @@ -304,9 +304,12 @@ export const ChartViewRoot: FC = ({ >
    -
    +
    +
    {title}
    +
    Duration
    +
    = ({ enableReorder={enableReorder} /> {chartBlocks && ( -
    +
    setIsCreateIssueFormOpen(false)} diff --git a/web/components/gantt-chart/sidebar.tsx b/web/components/gantt-chart/sidebar.tsx index 4a804f795..2aec274d9 100644 --- a/web/components/gantt-chart/sidebar.tsx +++ b/web/components/gantt-chart/sidebar.tsx @@ -8,6 +8,8 @@ import { useChart } from "./hooks"; import { Loader } from "components/ui"; // icons import { EllipsisVerticalIcon } from "@heroicons/react/24/outline"; +// helpers +import { findTotalDaysInRange } from "helpers/date-time.helper"; // types import { IBlockUpdateData, IGanttBlock } from "./types"; @@ -86,14 +88,20 @@ export const GanttSidebar: React.FC = (props) => { {(droppableProvided) => (
    <> {blocks ? ( - blocks.length > 0 ? ( - blocks.map((block, index) => ( + blocks.map((block, index) => { + const duration = findTotalDaysInRange( + block.start_date ?? "", + block.target_date ?? "", + true + ); + + return ( = (props) => { )} -
    - +
    +
    + +
    +
    + {duration} day{duration > 1 ? "s" : ""} +
    )} - )) - ) : ( -
    - No {title} found -
    - ) + ); + }) ) : ( diff --git a/web/components/issues/gantt-chart/blocks.tsx b/web/components/issues/gantt-chart/blocks.tsx index ef4919780..3364565a3 100644 --- a/web/components/issues/gantt-chart/blocks.tsx +++ b/web/components/issues/gantt-chart/blocks.tsx @@ -5,7 +5,7 @@ import { Tooltip } from "components/ui"; // icons import { StateGroupIcon } from "components/icons"; // helpers -import { findTotalDaysInRange, renderShortDate } from "helpers/date-time.helper"; +import { renderShortDate } from "helpers/date-time.helper"; // types import { IIssue } from "types"; @@ -52,8 +52,6 @@ export const IssueGanttBlock = ({ data }: { data: IIssue }) => { export const IssueGanttSidebarBlock = ({ data }: { data: IIssue }) => { const router = useRouter(); - const duration = findTotalDaysInRange(data?.start_date ?? "", data?.target_date ?? "", true); - const openPeekOverview = () => { const { query } = router; @@ -72,12 +70,7 @@ export const IssueGanttSidebarBlock = ({ data }: { data: IIssue }) => {
    {data?.project_detail?.identifier} {data?.sequence_id}
    -
    -
    {data?.name}
    - - {duration} day{duration > 1 ? "s" : ""} - -
    +
    {data?.name}
    ); }; From e00ae0b48a0b065acf0ea034df68552f62daf8dd Mon Sep 17 00:00:00 2001 From: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Date: Wed, 27 Sep 2023 15:02:35 +0530 Subject: [PATCH 25/35] style: calender quick-add same width as single date (#2280) * style: calender quick-add same width as single date * style: margin bottom in quick-add in spreadsheet view * fix: quick add opening in list-layout * style: reduced margin left --- .../core/views/board-view/single-board.tsx | 10 ++++--- .../inline-create-issue-form.tsx | 8 +++--- .../core/views/calendar-view/single-date.tsx | 26 +++++++------------ .../views/inline-issue-create-wrapper.tsx | 2 +- .../core/views/list-view/single-list.tsx | 6 ++++- .../spreadsheet-view/spreadsheet-view.tsx | 18 +++++++------ 6 files changed, 37 insertions(+), 33 deletions(-) diff --git a/web/components/core/views/board-view/single-board.tsx b/web/components/core/views/board-view/single-board.tsx index 8f851527d..bdbfc27c2 100644 --- a/web/components/core/views/board-view/single-board.tsx +++ b/web/components/core/views/board-view/single-board.tsx @@ -75,9 +75,7 @@ export const SingleBoard: React.FC = (props) => { const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions; - const onCreateClick = () => { - setIsInlineCreateIssueFormOpen(true); - + const scrollToBottom = () => { const boardListElement = document.getElementById(`board-list-${groupTitle}`); // timeout is needed because the animation @@ -93,6 +91,11 @@ export const SingleBoard: React.FC = (props) => { }, 10); }; + const onCreateClick = () => { + setIsInlineCreateIssueFormOpen(true); + scrollToBottom(); + }; + return (
    = (props) => { setIsInlineCreateIssueFormOpen(false)} + onSuccess={() => scrollToBottom()} prePopulatedData={{ ...(cycleId && { cycle: cycleId.toString() }), ...(moduleId && { module: moduleId.toString() }), diff --git a/web/components/core/views/calendar-view/inline-create-issue-form.tsx b/web/components/core/views/calendar-view/inline-create-issue-form.tsx index 832a6add9..8f070543b 100644 --- a/web/components/core/views/calendar-view/inline-create-issue-form.tsx +++ b/web/components/core/views/calendar-view/inline-create-issue-form.tsx @@ -67,7 +67,7 @@ const InlineInput = () => { {...register("name", { required: "Issue title is required.", })} - className="w-full px-2 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none" + className="w-full pr-2 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none" /> ); @@ -84,13 +84,13 @@ export const CalendarInlineCreateIssueForm: React.FC = (props) => { <>
    diff --git a/web/components/core/views/calendar-view/single-date.tsx b/web/components/core/views/calendar-view/single-date.tsx index bee67b5cd..a67ca762b 100644 --- a/web/components/core/views/calendar-view/single-date.tsx +++ b/web/components/core/views/calendar-view/single-date.tsx @@ -80,23 +80,17 @@ export const SingleCalendarDate: React.FC = (props) => { )} ))} -
    setIsCreateIssueFormOpen(false)} + prePopulatedData={{ + target_date: date.date, + ...(cycleId && { cycle: cycleId.toString() }), + ...(moduleId && { module: moduleId.toString() }), }} - > - setIsCreateIssueFormOpen(false)} - prePopulatedData={{ - target_date: date.date, - ...(cycleId && { cycle: cycleId.toString() }), - ...(moduleId && { module: moduleId.toString() }), - }} - /> -
    + /> {totalIssues > 4 && ( diff --git a/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx b/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx index 66dc97f2c..086a67fa4 100644 --- a/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx +++ b/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx @@ -288,14 +288,16 @@ export const SpreadsheetView: React.FC = ({
    - setIsInlineCreateIssueFormOpen(false)} - prePopulatedData={{ - ...(cycleId && { cycle: cycleId.toString() }), - ...(moduleId && { module: moduleId.toString() }), - }} - /> +
    + setIsInlineCreateIssueFormOpen(false)} + prePopulatedData={{ + ...(cycleId && { cycle: cycleId.toString() }), + ...(moduleId && { module: moduleId.toString() }), + }} + /> +
    {type === "issue" ? !disableUserActions && From 191aecaaac2c356cb4295cbfba7cf25145cc96bf Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Wed, 27 Sep 2023 15:05:49 +0530 Subject: [PATCH 26/35] chore: updated created at in draft issue (#2278) --- apiserver/plane/api/views/issue.py | 62 ++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 003a8ae32..844095434 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -2400,27 +2400,6 @@ class IssueDraftViewSet(BaseViewSet): ] serializer_class = IssueFlatSerializer model = Issue - - - def perform_update(self, serializer): - requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) - current_instance = ( - self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first() - ) - if current_instance is not None: - issue_activity.delay( - type="issue_draft.activity.updated", - requested_data=requested_data, - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("pk", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - IssueSerializer(current_instance).data, cls=DjangoJSONEncoder - ), - epoch=int(timezone.now().timestamp()) - ) - - return super().perform_update(serializer) def perform_destroy(self, instance): @@ -2614,6 +2593,47 @@ class IssueDraftViewSet(BaseViewSet): ) + def partial_update(self, request, slug, project_id, pk): + try: + issue = Issue.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) + serializer = IssueSerializer( + issue, data=request.data, partial=True + ) + + if serializer.is_valid(): + if(request.data.get("is_draft") is not None and not request.data.get("is_draft")): + serializer.save(created_at=timezone.now(), updated_at=timezone.now()) + else: + serializer.save() + issue_activity.delay( + type="issue_draft.activity.updated", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("pk", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + IssueSerializer(issue).data, + cls=DjangoJSONEncoder, + ), + epoch=int(timezone.now().timestamp()) + ) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except Issue.DoesNotExist: + return Response( + {"error": "Issue does not exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def retrieve(self, request, slug, project_id, pk=None): try: issue = Issue.objects.get( From 7b453dd6b52f2212e5526ece10443d07c72cfa30 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Wed, 27 Sep 2023 15:06:23 +0530 Subject: [PATCH 27/35] chore: make target dates inclusive when filtering (#2276) --- apiserver/plane/utils/issue_filters.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index 3a869113c..dae301c38 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -163,17 +163,17 @@ def filter_target_date(params, filter, method): for query in target_dates: target_date_query = query.split(";") if len(target_date_query) == 2 and "after" in target_date_query: - filter["target_date__gt"] = target_date_query[0] + filter["target_date__gte"] = target_date_query[0] else: - filter["target_date__lt"] = target_date_query[0] + filter["target_date__lte"] = target_date_query[0] else: if params.get("target_date", None) and len(params.get("target_date")): for query in params.get("target_date"): target_date_query = query.split(";") if len(target_date_query) == 2 and "after" in target_date_query: - filter["target_date__gt"] = target_date_query[0] + filter["target_date__gte"] = target_date_query[0] else: - filter["target_date__lt"] = target_date_query[0] + filter["target_date__lte"] = target_date_query[0] return filter From 7404fe71b1808dfd353169463d650f7db91c76ec Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Wed, 27 Sep 2023 17:30:52 +0530 Subject: [PATCH 28/35] chore: sort order and issue props for global views (#2283) --- ...er_workspacemember_issue_props_and_more.py | 24 +++++++++++++++++++ apiserver/plane/db/models/view.py | 11 +++++++++ apiserver/plane/db/models/workspace.py | 12 +++++++++- 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 apiserver/plane/db/migrations/0048_globalview_sort_order_workspacemember_issue_props_and_more.py diff --git a/apiserver/plane/db/migrations/0048_globalview_sort_order_workspacemember_issue_props_and_more.py b/apiserver/plane/db/migrations/0048_globalview_sort_order_workspacemember_issue_props_and_more.py new file mode 100644 index 000000000..3084ef637 --- /dev/null +++ b/apiserver/plane/db/migrations/0048_globalview_sort_order_workspacemember_issue_props_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.5 on 2023-09-27 11:18 + +from django.db import migrations, models +import plane.db.models.workspace + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0047_auto_20230926_1029'), + ] + + operations = [ + migrations.AddField( + model_name='globalview', + name='sort_order', + field=models.FloatField(default=65535), + ), + migrations.AddField( + model_name='workspacemember', + name='issue_props', + field=models.JSONField(default=plane.db.models.workspace.get_issue_props), + ), + ] diff --git a/apiserver/plane/db/models/view.py b/apiserver/plane/db/models/view.py index 6e0a47105..c19e444b3 100644 --- a/apiserver/plane/db/models/view.py +++ b/apiserver/plane/db/models/view.py @@ -17,12 +17,23 @@ class GlobalView(BaseModel): default=1, choices=((0, "Private"), (1, "Public")) ) query_data = models.JSONField(default=dict) + sort_order = models.FloatField(default=65535) class Meta: verbose_name = "Global View" verbose_name_plural = "Global Views" db_table = "global_views" ordering = ("-created_at",) + + def save(self, *args, **kwargs): + if self._state.adding: + largest_sort_order = GlobalView.objects.filter( + project=self.project + ).aggregate(largest=models.Max("sort_order"))["largest"] + if largest_sort_order is not None: + self.sort_order = largest_sort_order + 10000 + + super(GlobalView, self).save(*args, **kwargs) def __str__(self): """Return name of the View""" diff --git a/apiserver/plane/db/models/workspace.py b/apiserver/plane/db/models/workspace.py index c85268435..d1012f549 100644 --- a/apiserver/plane/db/models/workspace.py +++ b/apiserver/plane/db/models/workspace.py @@ -29,7 +29,7 @@ def get_default_props(): }, "display_filters": { "group_by": None, - "order_by": '-created_at', + "order_by": "-created_at", "type": None, "sub_issue": True, "show_empty_groups": True, @@ -54,6 +54,15 @@ def get_default_props(): } +def get_issue_props(): + return { + "subscribed": True, + "assigned": True, + "created": True, + "all_issues": True, + } + + class Workspace(BaseModel): name = models.CharField(max_length=80, verbose_name="Workspace Name") logo = models.URLField(verbose_name="Logo", blank=True, null=True) @@ -89,6 +98,7 @@ class WorkspaceMember(BaseModel): company_role = models.TextField(null=True, blank=True) view_props = models.JSONField(default=get_default_props) default_props = models.JSONField(default=get_default_props) + issue_props = models.JSONField(default=get_issue_props) class Meta: unique_together = ["workspace", "member"] From 9dd22f07f40f60ee764f87e7494403110e92bb22 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Wed, 27 Sep 2023 19:16:22 +0530 Subject: [PATCH 29/35] chore: removed project filter (#2284) --- apiserver/plane/db/models/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/db/models/view.py b/apiserver/plane/db/models/view.py index c19e444b3..44bc994d0 100644 --- a/apiserver/plane/db/models/view.py +++ b/apiserver/plane/db/models/view.py @@ -28,7 +28,7 @@ class GlobalView(BaseModel): def save(self, *args, **kwargs): if self._state.adding: largest_sort_order = GlobalView.objects.filter( - project=self.project + workspace=self.workspace ).aggregate(largest=models.Max("sort_order"))["largest"] if largest_sort_order is not None: self.sort_order = largest_sort_order + 10000 From 32d2f912f7ad719ecfaa1a02d3cf1fdb56123019 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Thu, 28 Sep 2023 13:14:37 +0530 Subject: [PATCH 30/35] fix: inbox issue deletes (#2290) --- apiserver/plane/api/views/inbox.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index 79294275e..4bfc32f01 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -370,6 +370,11 @@ class InboxIssueViewSet(BaseViewSet): if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id): return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST) + # Check the issue status + if inbox_issue.status in [-2, -1, 0, 2]: + # Delete the issue also + Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk=inbox_issue.issue_id).delete() + inbox_issue.delete() return Response(status=status.HTTP_204_NO_CONTENT) except InboxIssue.DoesNotExist: From 6afbd3f1ba434a022eeae92e96c49e784243f961 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Thu, 28 Sep 2023 13:16:31 +0530 Subject: [PATCH 31/35] chore: views (#2288) * chore: global views order by * chore: update permissions for global views --------- Co-authored-by: NarayanBavisetti --- apiserver/plane/api/permissions/workspace.py | 11 ++++++++++- apiserver/plane/api/views/issue.py | 2 -- apiserver/plane/api/views/view.py | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/apiserver/plane/api/permissions/workspace.py b/apiserver/plane/api/permissions/workspace.py index d01b545ee..66e836614 100644 --- a/apiserver/plane/api/permissions/workspace.py +++ b/apiserver/plane/api/permissions/workspace.py @@ -58,8 +58,17 @@ class WorkspaceEntityPermission(BasePermission): if request.user.is_anonymous: return False + ## Safe Methods -> Handle the filtering logic in queryset + if request.method in SAFE_METHODS: + return WorkspaceMember.objects.filter( + workspace__slug=view.workspace_slug, + member=request.user, + ).exists() + return WorkspaceMember.objects.filter( - member=request.user, workspace__slug=view.workspace_slug + member=request.user, + workspace__slug=view.workspace_slug, + role__in=[Owner, Admin], ).exists() diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 844095434..29f14e437 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -24,7 +24,6 @@ from django.core.serializers.json import DjangoJSONEncoder from django.utils.decorators import method_decorator from django.views.decorators.gzip import gzip_page from django.db import IntegrityError -from django.conf import settings from django.db import IntegrityError # Third Party imports @@ -58,7 +57,6 @@ from plane.api.serializers import ( IssuePublicSerializer, ) from plane.api.permissions import ( - WorkspaceEntityPermission, ProjectEntityPermission, WorkSpaceAdminPermission, ProjectMemberPermission, diff --git a/apiserver/plane/api/views/view.py b/apiserver/plane/api/views/view.py index b6f1d7c4b..435f8725a 100644 --- a/apiserver/plane/api/views/view.py +++ b/apiserver/plane/api/views/view.py @@ -61,7 +61,7 @@ class GlobalViewViewSet(BaseViewSet): .get_queryset() .filter(workspace__slug=self.kwargs.get("slug")) .select_related("workspace") - .order_by("-created_at") + .order_by(self.request.GET.get("order_by", "-created_at")) .distinct() ) From 34af666b5fbed6a6dd49292b6849ce6f1ea44d3c Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Thu, 28 Sep 2023 13:17:46 +0530 Subject: [PATCH 32/35] chore: fetch issues from previous and next month in the calendar view (#2282) --- web/hooks/use-calendar-issues-view.tsx | 28 ++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/web/hooks/use-calendar-issues-view.tsx b/web/hooks/use-calendar-issues-view.tsx index 1f6f04ea8..5f21d63c5 100644 --- a/web/hooks/use-calendar-issues-view.tsx +++ b/web/hooks/use-calendar-issues-view.tsx @@ -33,8 +33,28 @@ const useCalendarIssuesView = () => { const [activeMonthDate, setActiveMonthDate] = useState(new Date()); - const firstDayOfMonth = new Date(activeMonthDate.getFullYear(), activeMonthDate.getMonth(), 1); - const lastDayOfMonth = new Date(activeMonthDate.getFullYear(), activeMonthDate.getMonth() + 1, 0); + // previous month's first date + const previousMonthYear = + activeMonthDate.getMonth() === 0 + ? activeMonthDate.getFullYear() - 1 + : activeMonthDate.getFullYear(); + const previousMonthMonth = activeMonthDate.getMonth() === 0 ? 11 : activeMonthDate.getMonth() - 1; + + const previousMonthFirstDate = new Date(previousMonthYear, previousMonthMonth, 1); + + // next month's last date + const nextMonthYear = + activeMonthDate.getMonth() === 11 + ? activeMonthDate.getFullYear() + 1 + : activeMonthDate.getFullYear(); + const nextMonthMonth = (activeMonthDate.getMonth() + 1) % 12; + const nextMonthFirstDate = new Date(nextMonthYear, nextMonthMonth, 1); + + const nextMonthLastDate = new Date( + nextMonthFirstDate.getFullYear(), + nextMonthFirstDate.getMonth() + 1, + 0 + ); const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; @@ -47,8 +67,8 @@ const useCalendarIssuesView = () => { labels: filters?.labels ? filters?.labels.join(",") : undefined, created_by: filters?.created_by ? filters?.created_by.join(",") : undefined, start_date: filters?.start_date ? filters?.start_date.join(",") : undefined, - target_date: `${renderDateFormat(firstDayOfMonth)};after,${renderDateFormat( - lastDayOfMonth + target_date: `${renderDateFormat(previousMonthFirstDate)};after,${renderDateFormat( + nextMonthLastDate )};before`, }; From 60a69e28e3eb0ea3528df6c1c02ea94dd81f2f91 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Thu, 28 Sep 2023 13:18:35 +0530 Subject: [PATCH 33/35] fix: issue activity estimate value bug fix (#2281) * fix: issue activity estimate value bug fix * fix: activity typo fix --- web/components/core/activity.tsx | 17 +++++++++++++++-- web/hooks/use-estimate-option.tsx | 4 +++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/web/components/core/activity.tsx b/web/components/core/activity.tsx index c76f1aece..4e01c5ed8 100644 --- a/web/components/core/activity.tsx +++ b/web/components/core/activity.tsx @@ -2,6 +2,8 @@ import { useRouter } from "next/router"; import useSWR from "swr"; +// hook +import useEstimateOption from "hooks/use-estimate-option"; // services import issuesService from "services/issues.service"; // icons @@ -77,6 +79,18 @@ const LabelPill = ({ labelId }: { labelId: string }) => { /> ); }; +const EstimatePoint = ({ point }: { point: string }) => { + const { estimateValue, isEstimateActive } = useEstimateOption(Number(point)); + const currentPoint = Number(point) + 1; + + return ( + + {isEstimateActive + ? estimateValue + : `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`} + + ); +}; const activityDetails: { [key: string]: { @@ -324,8 +338,7 @@ const activityDetails: { else return ( <> - set the estimate point to{" "} - {activity.new_value} + set the estimate point to {showIssue && ( <> {" "} diff --git a/web/hooks/use-estimate-option.tsx b/web/hooks/use-estimate-option.tsx index 37b42b9e9..61a93ca59 100644 --- a/web/hooks/use-estimate-option.tsx +++ b/web/hooks/use-estimate-option.tsx @@ -32,7 +32,9 @@ const useEstimateOption = (estimateKey?: number | null) => { ); const estimateValue: any = - (estimateKey && estimateDetails?.points?.find((e) => e.key === estimateKey)?.value) ?? "None"; + estimateKey || estimateKey === 0 + ? estimateDetails?.points?.find((e) => e.key === estimateKey)?.value + : "None"; return { isEstimateActive: projectDetails?.estimate ? true : false, From ec91a0d2e50ec91b93ca7f8f79d1e23086621c86 Mon Sep 17 00:00:00 2001 From: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Date: Thu, 28 Sep 2023 14:02:03 +0530 Subject: [PATCH 34/35] fix: ui and bugs (#2289) * fix: 24 character limit on first & last name in onboarding page * fix: no option: 'Add Issue' in archive issue page * fix: in archive issue directly sending to issue detail page * fix: issue type showing in archive issue * fix: custom menu overflowing * fix: changing subscriber in filters has no effect * style: border in quick-add * fix: on onboarding member role overflowing * fix: inconsistent icons in issue detail * style: spacing, borders and shadows in quick-add * fix: custom menu truncate --- web/components/core/activity.tsx | 84 ++++--- web/components/core/filters/filters-list.tsx | 4 +- .../core/filters/issues-view-filter.tsx | 54 +++-- .../board-view/inline-create-issue-form.tsx | 2 +- .../inline-create-issue-form.tsx | 4 +- .../inline-create-issue-form.tsx | 2 +- web/components/core/views/issues-view.tsx | 5 +- .../list-view/inline-create-issue-form.tsx | 4 +- .../core/views/list-view/single-issue.tsx | 3 +- .../core/views/list-view/single-list.tsx | 5 +- .../assignee-column/assignee-column.tsx | 2 +- .../created-on-column/created-on-column.tsx | 2 +- .../due-date-column/due-date-column.tsx | 2 +- .../estimate-column/estimate-column.tsx | 2 +- .../issue-column/issue-column.tsx | 2 +- .../label-column/label-column.tsx | 2 +- .../priority-column/priority-column.tsx | 2 +- .../spreadsheet-view/spreadsheet-view.tsx | 10 +- .../start-date-column/start-date-column.tsx | 2 +- .../state-column/state-column.tsx | 2 +- .../updated-on-column/updated-on-column.tsx | 2 +- web/components/gantt-chart/chart/index.tsx | 2 +- web/components/onboarding/invite-members.tsx | 225 ++++++++++++------ web/components/onboarding/user-details.tsx | 8 + .../profile/profile-issues-view-options.tsx | 8 +- web/constants/fetch-keys.ts | 5 +- web/hooks/use-issues-view.tsx | 2 +- 27 files changed, 289 insertions(+), 158 deletions(-) diff --git a/web/components/core/activity.tsx b/web/components/core/activity.tsx index 4e01c5ed8..d5987384c 100644 --- a/web/components/core/activity.tsx +++ b/web/components/core/activity.tsx @@ -8,9 +8,27 @@ import useEstimateOption from "hooks/use-estimate-option"; import issuesService from "services/issues.service"; // icons import { Icon, Tooltip } from "components/ui"; -import { CopyPlus } from "lucide-react"; -import { Squares2X2Icon } from "@heroicons/react/24/outline"; -import { BlockedIcon, BlockerIcon, RelatedIcon } from "components/icons"; +import { + TagIcon, + CopyPlus, + Calendar, + Link2Icon, + RocketIcon, + Users2Icon, + ArchiveIcon, + PaperclipIcon, + ContrastIcon, + TriangleIcon, + LayoutGridIcon, + SignalMediumIcon, + MessageSquareIcon, +} from "lucide-react"; +import { + BlockedIcon, + BlockerIcon, + RelatedIcon, + StackedLayersHorizontalIcon, +} from "components/icons"; // helpers import { renderShortDateWithYearFormat } from "helpers/date-time.helper"; import { capitalizeFirstLetter } from "helpers/string.helper"; @@ -38,7 +56,7 @@ const IssueLink = ({ activity }: { activity: IIssueActivity }) => { {activity.issue_detail ? `${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}` : "Issue"} - + ); @@ -131,14 +149,14 @@ const activityDetails: { ); }, - icon:
    )} -
    -

    Issue type

    -
    - option.key === displayFilters.type - )?.name ?? "Select" - } - className="!w-full" - buttonClassName="w-full" - > - {FILTER_ISSUE_OPTIONS.map((option) => ( - - setDisplayFilters({ - type: option.key, - }) - } - > - {option.name} - - ))} - + {!isArchivedIssues && ( +
    +

    Issue type

    +
    + option.key === displayFilters.type + )?.name ?? "Select" + } + className="!w-full" + buttonClassName="w-full" + > + {FILTER_ISSUE_OPTIONS.map((option) => ( + + setDisplayFilters({ + type: option.key, + }) + } + > + {option.name} + + ))} + +
    -
    + )} {displayFilters.layout !== "calendar" && displayFilters.layout !== "spreadsheet" && ( diff --git a/web/components/core/views/board-view/inline-create-issue-form.tsx b/web/components/core/views/board-view/inline-create-issue-form.tsx index 651a0c0c1..1d6103d19 100644 --- a/web/components/core/views/board-view/inline-create-issue-form.tsx +++ b/web/components/core/views/board-view/inline-create-issue-form.tsx @@ -48,7 +48,7 @@ const InlineInput = () => { export const BoardInlineCreateIssueForm: React.FC = (props) => ( <> diff --git a/web/components/core/views/calendar-view/inline-create-issue-form.tsx b/web/components/core/views/calendar-view/inline-create-issue-form.tsx index 8f070543b..51b6c518b 100644 --- a/web/components/core/views/calendar-view/inline-create-issue-form.tsx +++ b/web/components/core/views/calendar-view/inline-create-issue-form.tsx @@ -67,7 +67,7 @@ const InlineInput = () => { {...register("name", { required: "Issue title is required.", })} - className="w-full pr-2 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none" + className="w-full pr-2 py-2.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none" /> ); @@ -90,7 +90,7 @@ export const CalendarInlineCreateIssueForm: React.FC = (props) => { > diff --git a/web/components/core/views/gantt-chart-view/inline-create-issue-form.tsx b/web/components/core/views/gantt-chart-view/inline-create-issue-form.tsx index a5bc85f6e..785eb3c5a 100644 --- a/web/components/core/views/gantt-chart-view/inline-create-issue-form.tsx +++ b/web/components/core/views/gantt-chart-view/inline-create-issue-form.tsx @@ -48,7 +48,7 @@ const InlineInput = () => { export const GanttInlineCreateIssueForm: React.FC = (props) => ( <> diff --git a/web/components/core/views/issues-view.tsx b/web/components/core/views/issues-view.tsx index c09e7c80b..62bcd5e58 100644 --- a/web/components/core/views/issues-view.tsx +++ b/web/components/core/views/issues-view.tsx @@ -81,7 +81,9 @@ export const IssuesView: React.FC = ({ const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; - const isDraftIssues = router.asPath.includes("draft-issues"); + + const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues"; + const isArchivedIssues = router.pathname?.split("/")?.[4] === "archived-issues"; const { user } = useUserAuth(); @@ -625,6 +627,7 @@ export const IssuesView: React.FC = ({ params, properties, }} + disableAddIssueOption={isArchivedIssues} /> ); diff --git a/web/components/core/views/list-view/inline-create-issue-form.tsx b/web/components/core/views/list-view/inline-create-issue-form.tsx index 2f7e1287e..b61420fc8 100644 --- a/web/components/core/views/list-view/inline-create-issue-form.tsx +++ b/web/components/core/views/list-view/inline-create-issue-form.tsx @@ -39,7 +39,7 @@ const InlineInput = () => { {...register("name", { required: "Issue title is required.", })} - className="w-full px-2 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none" + className="w-full px-2 py-3 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none" /> ); @@ -48,7 +48,7 @@ const InlineInput = () => { export const ListInlineCreateIssueForm: React.FC = (props) => ( <> diff --git a/web/components/core/views/list-view/single-issue.tsx b/web/components/core/views/list-view/single-issue.tsx index b0950a684..7fa54b852 100644 --- a/web/components/core/views/list-view/single-issue.tsx +++ b/web/components/core/views/list-view/single-issue.tsx @@ -328,7 +328,7 @@ export const SingleListIssue: React.FC = ({
    { e.preventDefault(); setContextMenu(true); @@ -352,6 +352,7 @@ export const SingleListIssue: React.FC = ({ type="button" className="truncate text-[0.825rem] text-custom-text-100" onClick={() => { + if (isArchivedIssues) return router.push(issuePath); if (!isDraftIssues) openPeekOverview(issue); if (isDraftIssues && handleDraftIssueSelect) handleDraftIssueSelect(issue); }} diff --git a/web/components/core/views/list-view/single-list.tsx b/web/components/core/views/list-view/single-list.tsx index 03401839c..9d3e9cb96 100644 --- a/web/components/core/views/list-view/single-list.tsx +++ b/web/components/core/views/list-view/single-list.tsx @@ -239,7 +239,7 @@ export const SingleList: React.FC = (props) => { !disableAddIssueOption && ( ) : !disableUserActions && @@ -316,11 +316,11 @@ export const SpreadsheetView: React.FC = ({ className="sticky left-0 z-10" customButton={ } position="left" diff --git a/web/components/core/views/spreadsheet-view/start-date-column/start-date-column.tsx b/web/components/core/views/spreadsheet-view/start-date-column/start-date-column.tsx index 3b4b9a0f7..6774ce1b9 100644 --- a/web/components/core/views/spreadsheet-view/start-date-column/start-date-column.tsx +++ b/web/components/core/views/spreadsheet-view/start-date-column/start-date-column.tsx @@ -23,7 +23,7 @@ export const StartDateColumn: React.FC = ({ isNotAllowed, }) => (
    - + {properties.due_date && ( = ({ return (
    - + {properties.state && ( = ({ isNotAllowed, }) => (
    - + {properties.updated_on && (
    {renderLongDetailDateFormat(issue.updated_at)} diff --git a/web/components/gantt-chart/chart/index.tsx b/web/components/gantt-chart/chart/index.tsx index c564f69f2..61e3078d6 100644 --- a/web/components/gantt-chart/chart/index.tsx +++ b/web/components/gantt-chart/chart/index.tsx @@ -347,7 +347,7 @@ export const ChartViewRoot: FC = ({ + )} +
    + ); +}; + +export const InviteMembers: React.FC = (props) => { + const { finishOnboarding, stepChange, user, workspace } = props; + const { setToastAlert } = useToast(); const { @@ -109,66 +245,15 @@ export const InviteMembers: React.FC = ({
    {fields.map((field, index) => ( -
    -
    - ( - <> - - {errors.emails?.[index]?.email && ( - - {errors.emails?.[index]?.email?.message} - - )} - - )} - /> -
    -
    - ( - {ROLE[value]}} - onChange={onChange} - width="w-full" - input - > - {Object.entries(ROLE).map(([key, value]) => ( - - {value} - - ))} - - )} - /> -
    - {fields.length > 1 && ( - - )} -
    + ))}