chore: issue action added and code refactor

This commit is contained in:
Anmol Singh Bhatia 2023-09-25 15:00:35 +05:30
parent 3b8a8cdfa4
commit 06722408b8
15 changed files with 686 additions and 275 deletions

View File

@ -20,6 +20,7 @@ import {
// hooks // hooks
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useWorkspaceIssuesFilters from "hooks/use-worskpace-issue-filter";
// services // services
import issuesService from "services/issues.service"; import issuesService from "services/issues.service";
import trackEventServices from "services/track-event.service"; import trackEventServices from "services/track-event.service";
@ -32,6 +33,7 @@ import {
PROJECT_ISSUES_LIST_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS,
SUB_ISSUES, SUB_ISSUES,
VIEW_ISSUES, VIEW_ISSUES,
WORKSPACE_VIEW_ISSUES,
} from "constants/fetch-keys"; } from "constants/fetch-keys";
// types // types
import { import {
@ -82,12 +84,51 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
const router = useRouter(); const router = useRouter();
const { workspaceSlug, cycleId, moduleId, viewId } = router.query; const { workspaceSlug, cycleId, moduleId, viewId, workspaceViewId } = router.query;
const { params } = useSpreadsheetIssuesView(); const { params } = useSpreadsheetIssuesView();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
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 currentWorkspaceIssuePath = workspaceIssuesPath.find((path) =>
router.pathname.includes(path.path)
);
const { params: workspaceViewParams } = useWorkspaceIssuesFilters(
workspaceSlug?.toString(),
workspaceViewId?.toString()
);
const partialUpdateIssue = useCallback( const partialUpdateIssue = useCallback(
(formData: Partial<IIssue>, issue: IIssue) => { (formData: Partial<IIssue>, issue: IIssue) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
@ -98,6 +139,10 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params) ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
: viewId : viewId
? VIEW_ISSUES(viewId.toString(), params) ? 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(projectId, params); : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId, params);
if (issue.parent) if (issue.parent)
@ -153,7 +198,18 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
console.log(error); console.log(error);
}); });
}, },
[workspaceSlug, projectId, cycleId, moduleId, viewId, params, user] [
workspaceSlug,
projectId,
cycleId,
moduleId,
viewId,
workspaceViewId,
currentWorkspaceIssuePath,
workspaceViewParams,
params,
user,
]
); );
const openPeekOverview = () => { const openPeekOverview = () => {

View File

@ -9,6 +9,7 @@ import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types";
type Props = { type Props = {
issue: IIssue; issue: IIssue;
projectId: string;
index: number; index: number;
expandedIssues: string[]; expandedIssues: string[];
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>; setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
@ -24,6 +25,7 @@ type Props = {
export const SpreadsheetIssues: React.FC<Props> = ({ export const SpreadsheetIssues: React.FC<Props> = ({
index, index,
issue, issue,
projectId,
expandedIssues, expandedIssues,
setExpandedIssues, setExpandedIssues,
gridTemplateColumns, gridTemplateColumns,
@ -55,7 +57,7 @@ export const SpreadsheetIssues: React.FC<Props> = ({
<div> <div>
<SingleSpreadsheetIssue <SingleSpreadsheetIssue
issue={issue} issue={issue}
projectId={issue.project_detail.id} projectId={projectId}
index={index} index={index}
expanded={isExpanded} expanded={isExpanded}
handleToggleExpand={handleToggleExpand} handleToggleExpand={handleToggleExpand}
@ -77,6 +79,7 @@ export const SpreadsheetIssues: React.FC<Props> = ({
<SpreadsheetIssues <SpreadsheetIssues
key={subIssue.id} key={subIssue.id}
issue={subIssue} issue={subIssue}
projectId={subIssue.project_detail.id}
index={index} index={index}
expandedIssues={expandedIssues} expandedIssues={expandedIssues}
setExpandedIssues={setExpandedIssues} setExpandedIssues={setExpandedIssues}

View File

@ -86,6 +86,7 @@ export const SpreadsheetView: React.FC<Props> = ({
key={`${issue.id}_${index}`} key={`${issue.id}_${index}`}
index={index} index={index}
issue={issue} issue={issue}
projectId={issue.project_detail.id}
expandedIssues={expandedIssues} expandedIssues={expandedIssues}
setExpandedIssues={setExpandedIssues} setExpandedIssues={setExpandedIssues}
gridTemplateColumns={gridTemplateColumns} gridTemplateColumns={gridTemplateColumns}

View File

@ -161,6 +161,7 @@ export const IssueProperty: React.FC<IIssueProperty> = ({
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<StateSelect <StateSelect
value={issue.state_detail} value={issue.state_detail}
projectId={issue.project_detail.id}
onChange={handleStateChange} onChange={handleStateChange}
hideDropdownArrow hideDropdownArrow
disabled={!editable} disabled={!editable}
@ -194,6 +195,7 @@ export const IssueProperty: React.FC<IIssueProperty> = ({
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<MembersSelect <MembersSelect
value={issue.assignees} value={issue.assignees}
projectId={issue.project_detail.id}
onChange={handleAssigneeChange} onChange={handleAssigneeChange}
membersDetails={issue.assignee_details} membersDetails={issue.assignee_details}
hideDropdownArrow hideDropdownArrow

View File

@ -17,7 +17,6 @@ import { DangerButton, SecondaryButton } from "components/ui";
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
// types // types
import type { ICurrentUserResponse, IView } from "types"; import type { ICurrentUserResponse, IView } from "types";
import { IWorkspaceView } from "types/workspace-view";
// fetch-keys // fetch-keys
import { VIEWS_LIST, WORKSPACE_VIEWS_LIST } from "constants/fetch-keys"; import { VIEWS_LIST, WORKSPACE_VIEWS_LIST } from "constants/fetch-keys";
@ -25,7 +24,7 @@ type Props = {
isOpen: boolean; isOpen: boolean;
viewType: "project" | "workspace"; viewType: "project" | "workspace";
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>; setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
data: IView | IWorkspaceView | null; data: IView | null;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
}; };

View File

@ -12,7 +12,6 @@ import { CustomMenu } from "components/ui";
import viewsService from "services/views.service"; import viewsService from "services/views.service";
// types // types
import { IView } from "types"; import { IView } from "types";
import { IWorkspaceView } from "types/workspace-view";
// fetch keys // fetch keys
import { VIEWS_LIST } from "constants/fetch-keys"; import { VIEWS_LIST } from "constants/fetch-keys";
// hooks // hooks
@ -21,7 +20,7 @@ import useToast from "hooks/use-toast";
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
type Props = { type Props = {
view: IView | IWorkspaceView; view: IView;
viewType: "project" | "workspace"; viewType: "project" | "workspace";
handleEditView: () => void; handleEditView: () => void;
handleDeleteView: () => void; handleDeleteView: () => void;

View File

@ -159,7 +159,7 @@ export const WORKSPACE_VIEWS_LIST = (workspaceSlug: string) =>
`WORKSPACE_VIEWS_LIST_${workspaceSlug.toUpperCase()}`; `WORKSPACE_VIEWS_LIST_${workspaceSlug.toUpperCase()}`;
export const WORKSPACE_VIEW_DETAILS = (workspaceViewId: string) => export const WORKSPACE_VIEW_DETAILS = (workspaceViewId: string) =>
`WORKSPACE_VIEW_DETAILS_${workspaceViewId.toUpperCase()}`; `WORKSPACE_VIEW_DETAILS_${workspaceViewId.toUpperCase()}`;
export const WORKSPACE_VIEW_ISSUES = (workspaceViewId: string, params: any) => { export const WORKSPACE_VIEW_ISSUES = (workspaceViewId: string, params?: any) => {
if (!params) return `WORKSPACE_VIEW_ISSUES_${workspaceViewId.toUpperCase()}`; if (!params) return `WORKSPACE_VIEW_ISSUES_${workspaceViewId.toUpperCase()}`;
const paramsKey = paramsToKey(params); const paramsKey = paramsToKey(params);

View File

@ -1,3 +1,4 @@
import { useCallback, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
@ -9,12 +10,12 @@ import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
import workspaceService from "services/workspace.service"; import workspaceService from "services/workspace.service";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// contexts
import { IssueViewContextProvider } from "contexts/issue-view.context";
// components // components
import { SpreadsheetView } from "components/core"; import { SpreadsheetView } from "components/core";
import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation"; import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation";
import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option"; import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option";
import { CreateUpdateViewModal } from "components/views";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
// ui // ui
import { EmptyState, PrimaryButton } from "components/ui"; import { EmptyState, PrimaryButton } from "components/ui";
// icons // icons
@ -24,8 +25,28 @@ import { CheckCircle } from "lucide-react";
import emptyView from "public/empty-state/view.svg"; import emptyView from "public/empty-state/view.svg";
// fetch-keys // fetch-keys
import { WORKSPACE_VIEW_DETAILS, WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys"; import { WORKSPACE_VIEW_DETAILS, WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys";
// types
import { IIssue } from "types";
const WorkspaceViewAllIssue: React.FC = () => { const WorkspaceViewAllIssue: React.FC = () => {
const [createViewModal, setCreateViewModal] = useState<any>(null);
// create issue modal
const [createIssueModal, setCreateIssueModal] = useState(false);
const [preloadedData, setPreloadedData] = useState<
(Partial<IIssue> & { 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<IIssue | null>(null);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, workspaceViewId } = router.query; const { workspaceSlug, workspaceViewId } = router.query;
@ -58,67 +79,138 @@ const WorkspaceViewAllIssue: React.FC = () => {
workspaceSlug ? () => workspaceService.getViewIssues(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 ( return (
<IssueViewContextProvider> <WorkspaceAuthorizationLayout
<WorkspaceAuthorizationLayout breadcrumbs={
breadcrumbs={ <div className="flex gap-2 items-center">
<div className="flex gap-2 items-center"> <CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" />
<CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" /> <span className="text-sm font-medium">
<span className="text-sm font-medium"> {viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"}
{viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"} </span>
</span> </div>
</div> }
} right={
right={ <div className="flex items-center gap-2">
<div className="flex items-center gap-2"> <WorkspaceIssuesViewOptions />
<WorkspaceIssuesViewOptions /> <PrimaryButton
<PrimaryButton className="flex items-center gap-2"
className="flex items-center gap-2" onClick={() => {
onClick={() => { const e = new KeyboardEvent("keydown", { key: "c" });
const e = new KeyboardEvent("keydown", { key: "c" }); document.dispatchEvent(e);
document.dispatchEvent(e); }}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</PrimaryButton>
</div>
}
>
<CreateUpdateIssueModal
isOpen={createIssueModal && preloadedData?.actionType === "createIssue"}
handleClose={() => setCreateIssueModal(false)}
prePopulateData={{
...preloadedData,
}}
onSubmit={async () => {
mutateIssues();
}}
/>
<CreateUpdateIssueModal
isOpen={editIssueModal && issueToEdit?.actionType !== "delete"}
handleClose={() => setEditIssueModal(false)}
data={issueToEdit}
onSubmit={async () => {
mutateIssues();
}}
/>
<DeleteIssueModal
handleClose={() => setDeleteIssueModal(false)}
isOpen={deleteIssueModal}
data={issueToDelete}
user={user}
onSubmit={async () => {
mutateIssues();
}}
/>
<CreateUpdateViewModal
isOpen={createViewModal !== null}
handleClose={() => setCreateViewModal(null)}
viewType="workspace"
preLoadedData={createViewModal}
user={user}
/>
<div className="h-full flex flex-col overflow-hidden bg-custom-background-100">
<div className="h-full w-full border-b border-custom-border-300">
<WorkspaceViewsNavigation handleAddView={() => setCreateViewModal(true)} />
{error ? (
<EmptyState
image={emptyView}
title="View does not exist"
description="The view you are looking for does not exist or has been deleted."
primaryButton={{
text: "View other views",
onClick: () => router.push(`/${workspaceSlug}/workspace-views`),
}} }}
> />
<PlusIcon className="h-4 w-4" /> ) : (
Add Issue <div className="h-full w-full flex flex-col">
</PrimaryButton> <SpreadsheetView
</div> spreadsheetIssues={viewIssues}
} mutateIssues={mutateIssues}
> handleIssueAction={handleIssueAction}
<div className="h-full flex flex-col overflow-hidden bg-custom-background-100"> disableUserActions={false}
<div className="h-full w-full border-b border-custom-border-300"> user={user}
<WorkspaceViewsNavigation user={user} /> userAuth={{
{error ? ( isGuest: false,
<EmptyState isMember: false,
image={emptyView} isOwner: false,
title="View does not exist" isViewer: false,
description="The view you are looking for does not exist or has been deleted."
primaryButton={{
text: "View other views",
onClick: () => router.push(`/${workspaceSlug}/workspace-views`),
}} }}
/> />
) : ( </div>
<div className="h-full w-full flex flex-col"> )}
<SpreadsheetView
spreadsheetIssues={viewIssues}
mutateIssues={mutateIssues}
handleIssueAction={(...args) => {}}
disableUserActions={false}
user={user}
userAuth={{
isGuest: false,
isMember: false,
isOwner: false,
isViewer: false,
}}
/>
</div>
)}
</div>
</div> </div>
</WorkspaceAuthorizationLayout> </div>
</IssueViewContextProvider> </WorkspaceAuthorizationLayout>
); );
}; };

View File

@ -1,3 +1,4 @@
import { useCallback, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
@ -9,12 +10,12 @@ import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
import workspaceService from "services/workspace.service"; import workspaceService from "services/workspace.service";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// contexts
import { IssueViewContextProvider } from "contexts/issue-view.context";
// components // components
import { SpreadsheetView } from "components/core"; import { SpreadsheetView } from "components/core";
import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation"; import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation";
import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option"; import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option";
import { CreateUpdateViewModal } from "components/views";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
// ui // ui
import { EmptyState, PrimaryButton } from "components/ui"; import { EmptyState, PrimaryButton } from "components/ui";
// icons // icons
@ -24,8 +25,28 @@ import { CheckCircle } from "lucide-react";
import emptyView from "public/empty-state/view.svg"; import emptyView from "public/empty-state/view.svg";
// fetch-keys // fetch-keys
import { WORKSPACE_VIEW_DETAILS, WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys"; import { WORKSPACE_VIEW_DETAILS, WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys";
// types
import { IIssue } from "types";
const WorkspaceViewAssignedIssue: React.FC = () => { const WorkspaceViewAssignedIssue: React.FC = () => {
const [createViewModal, setCreateViewModal] = useState<any>(null);
// create issue modal
const [createIssueModal, setCreateIssueModal] = useState(false);
const [preloadedData, setPreloadedData] = useState<
(Partial<IIssue> & { 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<IIssue | null>(null);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, workspaceViewId } = router.query; const { workspaceSlug, workspaceViewId } = router.query;
@ -58,67 +79,138 @@ const WorkspaceViewAssignedIssue: React.FC = () => {
workspaceSlug ? () => workspaceService.getViewIssues(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 ( return (
<IssueViewContextProvider> <WorkspaceAuthorizationLayout
<WorkspaceAuthorizationLayout breadcrumbs={
breadcrumbs={ <div className="flex gap-2 items-center">
<div className="flex gap-2 items-center"> <CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" />
<CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" /> <span className="text-sm font-medium">
<span className="text-sm font-medium"> {viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"}
{viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"} </span>
</span> </div>
</div> }
} right={
right={ <div className="flex items-center gap-2">
<div className="flex items-center gap-2"> <WorkspaceIssuesViewOptions />
<WorkspaceIssuesViewOptions /> <PrimaryButton
<PrimaryButton className="flex items-center gap-2"
className="flex items-center gap-2" onClick={() => {
onClick={() => { const e = new KeyboardEvent("keydown", { key: "c" });
const e = new KeyboardEvent("keydown", { key: "c" }); document.dispatchEvent(e);
document.dispatchEvent(e); }}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</PrimaryButton>
</div>
}
>
<CreateUpdateIssueModal
isOpen={createIssueModal && preloadedData?.actionType === "createIssue"}
handleClose={() => setCreateIssueModal(false)}
prePopulateData={{
...preloadedData,
}}
onSubmit={async () => {
mutateIssues();
}}
/>
<CreateUpdateIssueModal
isOpen={editIssueModal && issueToEdit?.actionType !== "delete"}
handleClose={() => setEditIssueModal(false)}
data={issueToEdit}
onSubmit={async () => {
mutateIssues();
}}
/>
<DeleteIssueModal
handleClose={() => setDeleteIssueModal(false)}
isOpen={deleteIssueModal}
data={issueToDelete}
user={user}
onSubmit={async () => {
mutateIssues();
}}
/>
<CreateUpdateViewModal
isOpen={createViewModal !== null}
handleClose={() => setCreateViewModal(null)}
viewType="workspace"
preLoadedData={createViewModal}
user={user}
/>
<div className="h-full flex flex-col overflow-hidden bg-custom-background-100">
<div className="h-full w-full border-b border-custom-border-300">
<WorkspaceViewsNavigation handleAddView={() => setCreateViewModal(true)} />
{error ? (
<EmptyState
image={emptyView}
title="View does not exist"
description="The view you are looking for does not exist or has been deleted."
primaryButton={{
text: "View other views",
onClick: () => router.push(`/${workspaceSlug}/workspace-views`),
}} }}
> />
<PlusIcon className="h-4 w-4" /> ) : (
Add Issue <div className="h-full w-full flex flex-col">
</PrimaryButton> <SpreadsheetView
</div> spreadsheetIssues={viewIssues}
} mutateIssues={mutateIssues}
> handleIssueAction={handleIssueAction}
<div className="h-full flex flex-col overflow-hidden bg-custom-background-100"> disableUserActions={false}
<div className="h-full w-full border-b border-custom-border-300"> user={user}
<WorkspaceViewsNavigation user={user} /> userAuth={{
{error ? ( isGuest: false,
<EmptyState isMember: false,
image={emptyView} isOwner: false,
title="View does not exist" isViewer: false,
description="The view you are looking for does not exist or has been deleted."
primaryButton={{
text: "View other views",
onClick: () => router.push(`/${workspaceSlug}/workspace-views`),
}} }}
/> />
) : ( </div>
<div className="h-full w-full flex flex-col"> )}
<SpreadsheetView
spreadsheetIssues={viewIssues}
mutateIssues={mutateIssues}
handleIssueAction={(...args) => {}}
disableUserActions={false}
user={user}
userAuth={{
isGuest: false,
isMember: false,
isOwner: false,
isViewer: false,
}}
/>
</div>
)}
</div>
</div> </div>
</WorkspaceAuthorizationLayout> </div>
</IssueViewContextProvider> </WorkspaceAuthorizationLayout>
); );
}; };

View File

@ -1,3 +1,5 @@
import { useCallback, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
@ -9,12 +11,12 @@ import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
import workspaceService from "services/workspace.service"; import workspaceService from "services/workspace.service";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// contexts
import { IssueViewContextProvider } from "contexts/issue-view.context";
// components // components
import { SpreadsheetView } from "components/core"; import { SpreadsheetView } from "components/core";
import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation"; import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation";
import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option"; import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option";
import { CreateUpdateViewModal } from "components/views";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
// ui // ui
import { EmptyState, PrimaryButton } from "components/ui"; import { EmptyState, PrimaryButton } from "components/ui";
// icons // icons
@ -24,8 +26,28 @@ import { CheckCircle } from "lucide-react";
import emptyView from "public/empty-state/view.svg"; import emptyView from "public/empty-state/view.svg";
// fetch-keys // fetch-keys
import { WORKSPACE_VIEW_DETAILS, WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys"; import { WORKSPACE_VIEW_DETAILS, WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys";
// types
import { IIssue } from "types";
const WorkspaceViewCreatedIssue: React.FC = () => { const WorkspaceViewCreatedIssue: React.FC = () => {
const [createViewModal, setCreateViewModal] = useState<any>(null);
// create issue modal
const [createIssueModal, setCreateIssueModal] = useState(false);
const [preloadedData, setPreloadedData] = useState<
(Partial<IIssue> & { 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<IIssue | null>(null);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, workspaceViewId } = router.query; const { workspaceSlug, workspaceViewId } = router.query;
@ -58,67 +80,138 @@ const WorkspaceViewCreatedIssue: React.FC = () => {
workspaceSlug ? () => workspaceService.getViewIssues(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 ( return (
<IssueViewContextProvider> <WorkspaceAuthorizationLayout
<WorkspaceAuthorizationLayout breadcrumbs={
breadcrumbs={ <div className="flex gap-2 items-center">
<div className="flex gap-2 items-center"> <CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" />
<CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" /> <span className="text-sm font-medium">
<span className="text-sm font-medium"> {viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"}
{viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"} </span>
</span> </div>
</div> }
} right={
right={ <div className="flex items-center gap-2">
<div className="flex items-center gap-2"> <WorkspaceIssuesViewOptions />
<WorkspaceIssuesViewOptions /> <PrimaryButton
<PrimaryButton className="flex items-center gap-2"
className="flex items-center gap-2" onClick={() => {
onClick={() => { const e = new KeyboardEvent("keydown", { key: "c" });
const e = new KeyboardEvent("keydown", { key: "c" }); document.dispatchEvent(e);
document.dispatchEvent(e); }}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</PrimaryButton>
</div>
}
>
<CreateUpdateIssueModal
isOpen={createIssueModal && preloadedData?.actionType === "createIssue"}
handleClose={() => setCreateIssueModal(false)}
prePopulateData={{
...preloadedData,
}}
onSubmit={async () => {
mutateIssues();
}}
/>
<CreateUpdateIssueModal
isOpen={editIssueModal && issueToEdit?.actionType !== "delete"}
handleClose={() => setEditIssueModal(false)}
data={issueToEdit}
onSubmit={async () => {
mutateIssues();
}}
/>
<DeleteIssueModal
handleClose={() => setDeleteIssueModal(false)}
isOpen={deleteIssueModal}
data={issueToDelete}
user={user}
onSubmit={async () => {
mutateIssues();
}}
/>
<CreateUpdateViewModal
isOpen={createViewModal !== null}
handleClose={() => setCreateViewModal(null)}
viewType="workspace"
preLoadedData={createViewModal}
user={user}
/>
<div className="h-full flex flex-col overflow-hidden bg-custom-background-100">
<div className="h-full w-full border-b border-custom-border-300">
<WorkspaceViewsNavigation handleAddView={() => setCreateViewModal(true)} />
{error ? (
<EmptyState
image={emptyView}
title="View does not exist"
description="The view you are looking for does not exist or has been deleted."
primaryButton={{
text: "View other views",
onClick: () => router.push(`/${workspaceSlug}/workspace-views`),
}} }}
> />
<PlusIcon className="h-4 w-4" /> ) : (
Add Issue <div className="h-full w-full flex flex-col">
</PrimaryButton> <SpreadsheetView
</div> spreadsheetIssues={viewIssues}
} mutateIssues={mutateIssues}
> handleIssueAction={handleIssueAction}
<div className="h-full flex flex-col overflow-hidden bg-custom-background-100"> disableUserActions={false}
<div className="h-full w-full border-b border-custom-border-300"> user={user}
<WorkspaceViewsNavigation user={user} /> userAuth={{
{error ? ( isGuest: false,
<EmptyState isMember: false,
image={emptyView} isOwner: false,
title="View does not exist" isViewer: false,
description="The view you are looking for does not exist or has been deleted."
primaryButton={{
text: "View other views",
onClick: () => router.push(`/${workspaceSlug}/workspace-views`),
}} }}
/> />
) : ( </div>
<div className="h-full w-full flex flex-col"> )}
<SpreadsheetView
spreadsheetIssues={viewIssues}
mutateIssues={mutateIssues}
handleIssueAction={(...args) => {}}
disableUserActions={false}
user={user}
userAuth={{
isGuest: false,
isMember: false,
isOwner: false,
isViewer: false,
}}
/>
</div>
)}
</div>
</div> </div>
</WorkspaceAuthorizationLayout> </div>
</IssueViewContextProvider> </WorkspaceAuthorizationLayout>
); );
}; };

View File

@ -25,7 +25,7 @@ import { PhotoFilterOutlined } from "@mui/icons-material";
import emptyView from "public/empty-state/view.svg"; import emptyView from "public/empty-state/view.svg";
// types // types
import type { NextPage } from "next"; import type { NextPage } from "next";
import { IWorkspaceView } from "types/workspace-view"; import { IView } from "types";
// constants // constants
import { WORKSPACE_VIEWS_LIST } from "constants/fetch-keys"; import { WORKSPACE_VIEWS_LIST } from "constants/fetch-keys";
// helper // helper
@ -33,17 +33,17 @@ import { truncateText } from "helpers/string.helper";
const WorkspaceViews: NextPage = () => { const WorkspaceViews: NextPage = () => {
const [createUpdateViewModal, setCreateUpdateViewModal] = useState(false); const [createUpdateViewModal, setCreateUpdateViewModal] = useState(false);
const [selectedViewToUpdate, setSelectedViewToUpdate] = useState<IWorkspaceView | null>(null); const [selectedViewToUpdate, setSelectedViewToUpdate] = useState<IView | null>(null);
const [deleteViewModal, setDeleteViewModal] = useState(false); const [deleteViewModal, setDeleteViewModal] = useState(false);
const [selectedViewToDelete, setSelectedViewToDelete] = useState<IWorkspaceView | null>(null); const [selectedViewToDelete, setSelectedViewToDelete] = useState<IView | null>(null);
const handleEditView = (view: IWorkspaceView) => { const handleEditView = (view: IView) => {
setSelectedViewToUpdate(view); setSelectedViewToUpdate(view);
setCreateUpdateViewModal(true); setCreateUpdateViewModal(true);
}; };
const handleDeleteView = (view: IWorkspaceView) => { const handleDeleteView = (view: IView) => {
setSelectedViewToDelete(view); setSelectedViewToDelete(view);
setDeleteViewModal(true); setDeleteViewModal(true);
}; };

View File

@ -1,3 +1,5 @@
import { useCallback, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
@ -9,12 +11,12 @@ import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
import workspaceService from "services/workspace.service"; import workspaceService from "services/workspace.service";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// contexts
import { IssueViewContextProvider } from "contexts/issue-view.context";
// components // components
import { SpreadsheetView } from "components/core"; import { SpreadsheetView } from "components/core";
import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation"; import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation";
import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option"; import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option";
import { CreateUpdateViewModal } from "components/views";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
// ui // ui
import { EmptyState, PrimaryButton } from "components/ui"; import { EmptyState, PrimaryButton } from "components/ui";
// icons // icons
@ -24,8 +26,28 @@ import { CheckCircle } from "lucide-react";
import emptyView from "public/empty-state/view.svg"; import emptyView from "public/empty-state/view.svg";
// fetch-keys // fetch-keys
import { WORKSPACE_VIEW_DETAILS, WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys"; import { WORKSPACE_VIEW_DETAILS, WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys";
// types
import { IIssue } from "types";
const WorkspaceViewSubscribedIssue: React.FC = () => { const WorkspaceViewSubscribedIssue: React.FC = () => {
const [createViewModal, setCreateViewModal] = useState<any>(null);
// create issue modal
const [createIssueModal, setCreateIssueModal] = useState(false);
const [preloadedData, setPreloadedData] = useState<
(Partial<IIssue> & { 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<IIssue | null>(null);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, workspaceViewId } = router.query; const { workspaceSlug, workspaceViewId } = router.query;
@ -58,67 +80,138 @@ const WorkspaceViewSubscribedIssue: React.FC = () => {
workspaceSlug ? () => workspaceService.getViewIssues(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 ( return (
<IssueViewContextProvider> <WorkspaceAuthorizationLayout
<WorkspaceAuthorizationLayout breadcrumbs={
breadcrumbs={ <div className="flex gap-2 items-center">
<div className="flex gap-2 items-center"> <CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" />
<CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" /> <span className="text-sm font-medium">
<span className="text-sm font-medium"> {viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"}
{viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"} </span>
</span> </div>
</div> }
} right={
right={ <div className="flex items-center gap-2">
<div className="flex items-center gap-2"> <WorkspaceIssuesViewOptions />
<WorkspaceIssuesViewOptions /> <PrimaryButton
<PrimaryButton className="flex items-center gap-2"
className="flex items-center gap-2" onClick={() => {
onClick={() => { const e = new KeyboardEvent("keydown", { key: "c" });
const e = new KeyboardEvent("keydown", { key: "c" }); document.dispatchEvent(e);
document.dispatchEvent(e); }}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</PrimaryButton>
</div>
}
>
<CreateUpdateIssueModal
isOpen={createIssueModal && preloadedData?.actionType === "createIssue"}
handleClose={() => setCreateIssueModal(false)}
prePopulateData={{
...preloadedData,
}}
onSubmit={async () => {
mutateIssues();
}}
/>
<CreateUpdateIssueModal
isOpen={editIssueModal && issueToEdit?.actionType !== "delete"}
handleClose={() => setEditIssueModal(false)}
data={issueToEdit}
onSubmit={async () => {
mutateIssues();
}}
/>
<DeleteIssueModal
handleClose={() => setDeleteIssueModal(false)}
isOpen={deleteIssueModal}
data={issueToDelete}
user={user}
onSubmit={async () => {
mutateIssues();
}}
/>
<CreateUpdateViewModal
isOpen={createViewModal !== null}
handleClose={() => setCreateViewModal(null)}
viewType="workspace"
preLoadedData={createViewModal}
user={user}
/>
<div className="h-full flex flex-col overflow-hidden bg-custom-background-100">
<div className="h-full w-full border-b border-custom-border-300">
<WorkspaceViewsNavigation handleAddView={() => setCreateViewModal(true)} />
{error ? (
<EmptyState
image={emptyView}
title="View does not exist"
description="The view you are looking for does not exist or has been deleted."
primaryButton={{
text: "View other views",
onClick: () => router.push(`/${workspaceSlug}/workspace-views`),
}} }}
> />
<PlusIcon className="h-4 w-4" /> ) : (
Add Issue <div className="h-full w-full flex flex-col">
</PrimaryButton> <SpreadsheetView
</div> spreadsheetIssues={viewIssues}
} mutateIssues={mutateIssues}
> handleIssueAction={handleIssueAction}
<div className="h-full flex flex-col overflow-hidden bg-custom-background-100"> disableUserActions={false}
<div className="h-full w-full border-b border-custom-border-300"> user={user}
<WorkspaceViewsNavigation user={user} /> userAuth={{
{error ? ( isGuest: false,
<EmptyState isMember: false,
image={emptyView} isOwner: false,
title="View does not exist" isViewer: false,
description="The view you are looking for does not exist or has been deleted."
primaryButton={{
text: "View other views",
onClick: () => router.push(`/${workspaceSlug}/workspace-views`),
}} }}
/> />
) : ( </div>
<div className="h-full w-full flex flex-col"> )}
<SpreadsheetView
spreadsheetIssues={viewIssues}
mutateIssues={mutateIssues}
handleIssueAction={(...args) => {}}
disableUserActions={false}
user={user}
userAuth={{
isGuest: false,
isMember: false,
isOwner: false,
isViewer: false,
}}
/>
</div>
)}
</div>
</div> </div>
</WorkspaceAuthorizationLayout> </div>
</IssueViewContextProvider> </WorkspaceAuthorizationLayout>
); );
}; };

View File

@ -14,9 +14,9 @@ import {
ICurrentUserResponse, ICurrentUserResponse,
IWorkspaceBulkInviteFormData, IWorkspaceBulkInviteFormData,
IWorkspaceViewProps, IWorkspaceViewProps,
IView,
IIssueFilterOptions, IIssueFilterOptions,
} from "types"; } from "types";
import { IWorkspaceView } from "types/workspace-view";
class WorkspaceService extends APIService { class WorkspaceService extends APIService {
constructor() { constructor() {
@ -264,7 +264,7 @@ class WorkspaceService extends APIService {
}); });
} }
async createView(workspaceSlug: string, data: IWorkspaceView): Promise<any> { async createView(workspaceSlug: string, data: IView): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/views/`, data) return this.post(`/api/workspaces/${workspaceSlug}/views/`, data)
.then((response) => response?.data) .then((response) => response?.data)
.catch((error) => { .catch((error) => {
@ -272,11 +272,7 @@ class WorkspaceService extends APIService {
}); });
} }
async updateView( async updateView(workspaceSlug: string, viewId: string, data: Partial<IView>): Promise<any> {
workspaceSlug: string,
viewId: string,
data: Partial<IWorkspaceView>
): Promise<any> {
return this.patch(`/api/workspaces/${workspaceSlug}/views/${viewId}/`, data) return this.patch(`/api/workspaces/${workspaceSlug}/views/${viewId}/`, data)
.then((response) => response?.data) .then((response) => response?.data)
.catch((error) => { .catch((error) => {
@ -292,7 +288,7 @@ class WorkspaceService extends APIService {
}); });
} }
async getAllViews(workspaceSlug: string): Promise<IWorkspaceView[]> { async getAllViews(workspaceSlug: string): Promise<IView[]> {
return this.get(`/api/workspaces/${workspaceSlug}/views/`) return this.get(`/api/workspaces/${workspaceSlug}/views/`)
.then((response) => response?.data) .then((response) => response?.data)
.catch((error) => { .catch((error) => {
@ -300,7 +296,7 @@ class WorkspaceService extends APIService {
}); });
} }
async getViewDetails(workspaceSlug: string, viewId: string): Promise<IWorkspaceView> { async getViewDetails(workspaceSlug: string, viewId: string): Promise<IView> {
return this.get(`/api/workspaces/${workspaceSlug}/views/${viewId}/`) return this.get(`/api/workspaces/${workspaceSlug}/views/${viewId}/`)
.then((response) => response?.data) .then((response) => response?.data)
.catch((error) => { .catch((error) => {

View File

@ -14,6 +14,11 @@ export interface IView {
query_data: IIssueFilterOptions; query_data: IIssueFilterOptions;
project: string; project: string;
workspace: string; workspace: string;
workspace_detail: {
id: string;
name: string;
slug: string;
};
} }
export interface IQuery { export interface IQuery {
@ -25,4 +30,5 @@ export interface IQuery {
start_date: string[] | null; start_date: string[] | null;
target_date: string[] | null; target_date: string[] | null;
type: "active" | "backlog" | null; type: "active" | "backlog" | null;
projects: string[] | null;
} }

View File

@ -1,21 +0,0 @@
import { IIssueFilterOptions } from "./view-props";
export interface IWorkspaceView {
access: string;
created_at: Date;
created_by: string;
description: string;
id: string;
name: string;
query: IIssueFilterOptions;
query_data: IIssueFilterOptions;
updated_at: Date;
updated_by: string;
is_favorite: boolean;
workspace: string;
workspace_detail: {
id: string;
name: string;
slug: string;
};
}