forked from github/plane
feat: workspace global view, style: spreadsheet view revamp (#2273)
* chore: workspace view types, services and hooks added * style: spreadsheet view revamp and code refactor * feat: workspace view * fix: build fix * chore: sidebar workspace issues redirection updated
This commit is contained in:
parent
a187e7765c
commit
3a6d72e4b6
@ -161,6 +161,7 @@ export const CommandPalette: React.FC = observer(() => {
|
|||||||
/>
|
/>
|
||||||
<CreateUpdateViewModal
|
<CreateUpdateViewModal
|
||||||
handleClose={() => setIsCreateViewModalOpen(false)}
|
handleClose={() => setIsCreateViewModalOpen(false)}
|
||||||
|
viewType="project"
|
||||||
isOpen={isCreateViewModalOpen}
|
isOpen={isCreateViewModalOpen}
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
|
@ -10,7 +10,14 @@ import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
|||||||
// helpers
|
// helpers
|
||||||
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssueFilterOptions, IIssueLabels, IState, IUserLite, TStateGroups } from "types";
|
import {
|
||||||
|
IIssueFilterOptions,
|
||||||
|
IIssueLabels,
|
||||||
|
IProject,
|
||||||
|
IState,
|
||||||
|
IUserLite,
|
||||||
|
TStateGroups,
|
||||||
|
} from "types";
|
||||||
// constants
|
// constants
|
||||||
import { STATE_GROUP_COLORS } from "constants/state";
|
import { STATE_GROUP_COLORS } from "constants/state";
|
||||||
|
|
||||||
@ -20,7 +27,9 @@ type Props = {
|
|||||||
clearAllFilters: (...args: any) => void;
|
clearAllFilters: (...args: any) => void;
|
||||||
labels: IIssueLabels[] | undefined;
|
labels: IIssueLabels[] | undefined;
|
||||||
members: IUserLite[] | undefined;
|
members: IUserLite[] | undefined;
|
||||||
states: IState[] | undefined;
|
states?: IState[] | undefined;
|
||||||
|
stateGroup?: string[] | undefined;
|
||||||
|
project?: IProject[] | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FiltersList: React.FC<Props> = ({
|
export const FiltersList: React.FC<Props> = ({
|
||||||
@ -30,6 +39,7 @@ export const FiltersList: React.FC<Props> = ({
|
|||||||
labels,
|
labels,
|
||||||
members,
|
members,
|
||||||
states,
|
states,
|
||||||
|
project,
|
||||||
}) => {
|
}) => {
|
||||||
if (!filters) return <></>;
|
if (!filters) return <></>;
|
||||||
|
|
||||||
@ -155,6 +165,29 @@ export const FiltersList: React.FC<Props> = ({
|
|||||||
: key === "assignees"
|
: key === "assignees"
|
||||||
? filters.assignees?.map((memberId: string) => {
|
? filters.assignees?.map((memberId: string) => {
|
||||||
const member = members?.find((m) => m.id === memberId);
|
const member = members?.find((m) => m.id === memberId);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={memberId}
|
||||||
|
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1"
|
||||||
|
>
|
||||||
|
<Avatar user={member} />
|
||||||
|
<span>{member?.display_name}</span>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
assignees: filters.assignees?.filter((p: any) => p !== memberId),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: key === "subscriber"
|
||||||
|
? filters.subscriber?.map((memberId: string) => {
|
||||||
|
const member = members?.find((m) => m.id === memberId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -298,6 +331,30 @@ export const FiltersList: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
: key === "project"
|
||||||
|
? filters.project?.map((projectId) => {
|
||||||
|
const currentProject = project?.find((p) => p.id === projectId);
|
||||||
|
console.log("currentProject", currentProject);
|
||||||
|
console.log("currentProject", projectId);
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
key={currentProject?.id}
|
||||||
|
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize"
|
||||||
|
>
|
||||||
|
<span>{currentProject?.name}</span>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
project: filters.project?.filter((p) => p !== projectId),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
})
|
||||||
: (filters[key] as any)?.join(", ")}
|
: (filters[key] as any)?.join(", ")}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -12,6 +12,7 @@ import stateService from "services/state.service";
|
|||||||
// hooks
|
// hooks
|
||||||
import useUser from "hooks/use-user";
|
import useUser from "hooks/use-user";
|
||||||
import { useProjectMyMembership } from "contexts/project-member.context";
|
import { useProjectMyMembership } from "contexts/project-member.context";
|
||||||
|
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
AllLists,
|
AllLists,
|
||||||
@ -86,6 +87,8 @@ export const AllViews: React.FC<Props> = ({
|
|||||||
|
|
||||||
const { groupedIssues, isEmpty, displayFilters } = viewProps;
|
const { groupedIssues, isEmpty, displayFilters } = viewProps;
|
||||||
|
|
||||||
|
const { spreadsheetIssues, mutateIssues } = useSpreadsheetIssuesView();
|
||||||
|
|
||||||
const { data: stateGroups } = useSWR(
|
const { data: stateGroups } = useSWR(
|
||||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||||
workspaceSlug
|
workspaceSlug
|
||||||
@ -174,6 +177,8 @@ export const AllViews: React.FC<Props> = ({
|
|||||||
) : displayFilters?.layout === "spreadsheet" ? (
|
) : displayFilters?.layout === "spreadsheet" ? (
|
||||||
<SpreadsheetView
|
<SpreadsheetView
|
||||||
handleIssueAction={handleIssueAction}
|
handleIssueAction={handleIssueAction}
|
||||||
|
spreadsheetIssues={spreadsheetIssues}
|
||||||
|
mutateIssues={mutateIssues}
|
||||||
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
||||||
disableUserActions={disableUserActions}
|
disableUserActions={disableUserActions}
|
||||||
user={user}
|
user={user}
|
||||||
|
@ -481,6 +481,7 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
<CreateUpdateViewModal
|
<CreateUpdateViewModal
|
||||||
isOpen={createViewModal !== null}
|
isOpen={createViewModal !== null}
|
||||||
handleClose={() => setCreateViewModal(null)}
|
handleClose={() => setCreateViewModal(null)}
|
||||||
|
viewType="project"
|
||||||
preLoadedData={createViewModal}
|
preLoadedData={createViewModal}
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
|
@ -0,0 +1,72 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { MembersSelect } from "components/project";
|
||||||
|
// services
|
||||||
|
import trackEventServices from "services/track-event.service";
|
||||||
|
// types
|
||||||
|
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
projectId: string;
|
||||||
|
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AssigneeColumn: React.FC<Props> = ({
|
||||||
|
issue,
|
||||||
|
projectId,
|
||||||
|
partialUpdateIssue,
|
||||||
|
properties,
|
||||||
|
user,
|
||||||
|
isNotAllowed,
|
||||||
|
}) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
const handleAssigneeChange = (data: any) => {
|
||||||
|
const newData = issue.assignees ?? [];
|
||||||
|
|
||||||
|
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
|
||||||
|
else newData.push(data);
|
||||||
|
|
||||||
|
partialUpdateIssue({ assignees_list: 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_ASSIGNEE",
|
||||||
|
user
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||||
|
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-200">
|
||||||
|
{properties.assignee && (
|
||||||
|
<MembersSelect
|
||||||
|
value={issue.assignees}
|
||||||
|
projectId={projectId}
|
||||||
|
onChange={handleAssigneeChange}
|
||||||
|
membersDetails={issue.assignee_details}
|
||||||
|
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||||
|
hideDropdownArrow
|
||||||
|
disabled={isNotAllowed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./spreadsheet-assignee-column";
|
||||||
|
export * from "./assignee-column";
|
@ -0,0 +1,62 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { AssigneeColumn } 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<IIssue>, issue: IIssue) => void;
|
||||||
|
expandedIssues: string[];
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetAssigneeColumn: React.FC<Props> = ({
|
||||||
|
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 (
|
||||||
|
<div>
|
||||||
|
<AssigneeColumn
|
||||||
|
issue={issue}
|
||||||
|
projectId={projectId}
|
||||||
|
properties={properties}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isExpanded &&
|
||||||
|
!isLoading &&
|
||||||
|
subIssues &&
|
||||||
|
subIssues.length > 0 &&
|
||||||
|
subIssues.map((subIssue: IIssue) => (
|
||||||
|
<SpreadsheetAssigneeColumn
|
||||||
|
key={subIssue.id}
|
||||||
|
issue={subIssue}
|
||||||
|
projectId={subIssue.project_detail.id}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
properties={properties}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -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<IIssue>, issue: IIssue) => void;
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CreatedOnColumn: React.FC<Props> = ({
|
||||||
|
issue,
|
||||||
|
projectId,
|
||||||
|
partialUpdateIssue,
|
||||||
|
properties,
|
||||||
|
user,
|
||||||
|
isNotAllowed,
|
||||||
|
}) => (
|
||||||
|
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||||
|
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-200">
|
||||||
|
{properties.created_on && (
|
||||||
|
<div className="flex items-center text-xs cursor-default text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||||
|
{renderLongDetailDateFormat(issue.created_at)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./spreadsheet-created-on-column";
|
||||||
|
export * from "./created-on-column";
|
@ -0,0 +1,62 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { CreatedOnColumn } 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<IIssue>, issue: IIssue) => void;
|
||||||
|
expandedIssues: string[];
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetCreatedOnColumn: React.FC<Props> = ({
|
||||||
|
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 (
|
||||||
|
<div>
|
||||||
|
<CreatedOnColumn
|
||||||
|
issue={issue}
|
||||||
|
projectId={projectId}
|
||||||
|
properties={properties}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isExpanded &&
|
||||||
|
!isLoading &&
|
||||||
|
subIssues &&
|
||||||
|
subIssues.length > 0 &&
|
||||||
|
subIssues.map((subIssue: IIssue) => (
|
||||||
|
<SpreadsheetCreatedOnColumn
|
||||||
|
key={subIssue.id}
|
||||||
|
issue={subIssue}
|
||||||
|
projectId={subIssue.project_detail.id}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
properties={properties}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,38 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { ViewDueDateSelect } from "components/issues";
|
||||||
|
// types
|
||||||
|
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
projectId: string;
|
||||||
|
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DueDateColumn: React.FC<Props> = ({
|
||||||
|
issue,
|
||||||
|
projectId,
|
||||||
|
partialUpdateIssue,
|
||||||
|
properties,
|
||||||
|
user,
|
||||||
|
isNotAllowed,
|
||||||
|
}) => (
|
||||||
|
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||||
|
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-200">
|
||||||
|
{properties.due_date && (
|
||||||
|
<ViewDueDateSelect
|
||||||
|
issue={issue}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
noBorder
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./spreadsheet-due-date-column";
|
||||||
|
export * from "./due-date-column";
|
@ -0,0 +1,62 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { DueDateColumn } 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<IIssue>, issue: IIssue) => void;
|
||||||
|
expandedIssues: string[];
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetDueDateColumn: React.FC<Props> = ({
|
||||||
|
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 (
|
||||||
|
<div>
|
||||||
|
<DueDateColumn
|
||||||
|
issue={issue}
|
||||||
|
projectId={projectId}
|
||||||
|
properties={properties}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isExpanded &&
|
||||||
|
!isLoading &&
|
||||||
|
subIssues &&
|
||||||
|
subIssues.length > 0 &&
|
||||||
|
subIssues.map((subIssue: IIssue) => (
|
||||||
|
<SpreadsheetDueDateColumn
|
||||||
|
key={subIssue.id}
|
||||||
|
issue={subIssue}
|
||||||
|
projectId={subIssue.project_detail.id}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
properties={properties}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,38 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { ViewEstimateSelect } from "components/issues";
|
||||||
|
// types
|
||||||
|
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
projectId: string;
|
||||||
|
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EstimateColumn: React.FC<Props> = ({
|
||||||
|
issue,
|
||||||
|
projectId,
|
||||||
|
partialUpdateIssue,
|
||||||
|
properties,
|
||||||
|
user,
|
||||||
|
isNotAllowed,
|
||||||
|
}) => (
|
||||||
|
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||||
|
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-200">
|
||||||
|
{properties.estimate && (
|
||||||
|
<ViewEstimateSelect
|
||||||
|
issue={issue}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
position="left"
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./spreadsheet-estimate-column";
|
||||||
|
export * from "./estimate-column";
|
@ -0,0 +1,62 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { EstimateColumn } 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<IIssue>, issue: IIssue) => void;
|
||||||
|
expandedIssues: string[];
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetEstimateColumn: React.FC<Props> = ({
|
||||||
|
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 (
|
||||||
|
<div>
|
||||||
|
<EstimateColumn
|
||||||
|
issue={issue}
|
||||||
|
projectId={projectId}
|
||||||
|
properties={properties}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isExpanded &&
|
||||||
|
!isLoading &&
|
||||||
|
subIssues &&
|
||||||
|
subIssues.length > 0 &&
|
||||||
|
subIssues.map((subIssue: IIssue) => (
|
||||||
|
<SpreadsheetEstimateColumn
|
||||||
|
key={subIssue.id}
|
||||||
|
issue={subIssue}
|
||||||
|
projectId={subIssue.project_detail.id}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
properties={properties}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,14 @@
|
|||||||
|
export * from "./assignee-column";
|
||||||
|
export * from "./created-on-column";
|
||||||
|
export * from "./due-date-column";
|
||||||
|
export * from "./estimate-column";
|
||||||
|
export * from "./issue-column";
|
||||||
|
export * from "./label-column";
|
||||||
|
export * from "./priority-column";
|
||||||
|
export * from "./start-date-column";
|
||||||
|
export * from "./state-column";
|
||||||
|
export * from "./updated-on-column";
|
||||||
export * from "./spreadsheet-view";
|
export * from "./spreadsheet-view";
|
||||||
export * from "./single-issue";
|
export * from "./issue-column/issue-column";
|
||||||
export * from "./spreadsheet-columns";
|
export * from "./spreadsheet-columns";
|
||||||
export * from "./spreadsheet-issues";
|
export * from "./issue-column/spreadsheet-issue-column";
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./spreadsheet-issue-column";
|
||||||
|
export * from "./issue-column";
|
@ -0,0 +1,179 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { Popover2 } from "@blueprintjs/popover2";
|
||||||
|
// icons
|
||||||
|
import { Icon } from "components/ui";
|
||||||
|
import {
|
||||||
|
EllipsisHorizontalIcon,
|
||||||
|
LinkIcon,
|
||||||
|
PencilIcon,
|
||||||
|
TrashIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
// hooks
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
// types
|
||||||
|
import { IIssue, Properties, UserAuth } from "types";
|
||||||
|
// helper
|
||||||
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
projectId: string;
|
||||||
|
expanded: boolean;
|
||||||
|
handleToggleExpand: (issueId: string) => void;
|
||||||
|
properties: Properties;
|
||||||
|
handleEditIssue: (issue: IIssue) => void;
|
||||||
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
|
setCurrentProjectId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
|
disableUserActions: boolean;
|
||||||
|
userAuth: UserAuth;
|
||||||
|
nestingLevel: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IssueColumn: React.FC<Props> = ({
|
||||||
|
issue,
|
||||||
|
projectId,
|
||||||
|
expanded,
|
||||||
|
handleToggleExpand,
|
||||||
|
properties,
|
||||||
|
handleEditIssue,
|
||||||
|
handleDeleteIssue,
|
||||||
|
setCurrentProjectId,
|
||||||
|
disableUserActions,
|
||||||
|
userAuth,
|
||||||
|
nestingLevel,
|
||||||
|
}) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const openPeekOverview = () => {
|
||||||
|
const { query } = router;
|
||||||
|
setCurrentProjectId(issue.project_detail.id);
|
||||||
|
router.push({
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: { ...query, peekIssue: issue.id },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyText = () => {
|
||||||
|
const originURL =
|
||||||
|
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||||
|
copyTextToClipboard(
|
||||||
|
`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`
|
||||||
|
).then(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
title: "Link Copied!",
|
||||||
|
message: "Issue link copied to clipboard.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const paddingLeft = `${nestingLevel * 54}px`;
|
||||||
|
|
||||||
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="group flex items-center w-[28rem] text-sm h-11 sticky top-0 bg-custom-background-100 truncate border-b border-r border-custom-border-200 ">
|
||||||
|
<div
|
||||||
|
className="flex gap-1.5 px-4 pr-0 py-2.5 items-center"
|
||||||
|
style={issue.parent ? { paddingLeft } : {}}
|
||||||
|
>
|
||||||
|
<div className="relative flex items-center cursor-pointer text-xs text-center hover:text-custom-text-100">
|
||||||
|
{properties.key && (
|
||||||
|
<span className="flex items-center justify-center font-medium opacity-100 group-hover:opacity-0">
|
||||||
|
{issue.project_detail?.identifier}-{issue.sequence_id}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{!isNotAllowed && !disableUserActions && (
|
||||||
|
<div className="absolute top-0 left-2.5 opacity-0 group-hover:opacity-100">
|
||||||
|
<Popover2
|
||||||
|
isOpen={isOpen}
|
||||||
|
canEscapeKeyClose
|
||||||
|
onInteraction={(nextOpenState) => setIsOpen(nextOpenState)}
|
||||||
|
content={
|
||||||
|
<div
|
||||||
|
className={`flex flex-col gap-1.5 overflow-y-scroll whitespace-nowrap rounded-md border p-1 text-xs shadow-lg focus:outline-none max-h-44 min-w-full border-custom-border-200 bg-custom-background-90`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
|
||||||
|
onClick={() => {
|
||||||
|
handleEditIssue(issue);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-start gap-2">
|
||||||
|
<PencilIcon className="h-4 w-4" />
|
||||||
|
<span>Edit issue</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
|
||||||
|
onClick={() => {
|
||||||
|
handleDeleteIssue(issue);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-start gap-2">
|
||||||
|
<TrashIcon className="h-4 w-4" />
|
||||||
|
<span>Delete issue</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
|
||||||
|
onClick={() => {
|
||||||
|
handleCopyText();
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-start gap-2">
|
||||||
|
<LinkIcon className="h-4 w-4" />
|
||||||
|
<span>Copy issue link</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
placement="bottom-start"
|
||||||
|
>
|
||||||
|
<EllipsisHorizontalIcon className="h-5 w-5 text-custom-text-200" />
|
||||||
|
</Popover2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{issue.sub_issues_count > 0 && (
|
||||||
|
<div className="h-6 w-6 flex justify-center items-center">
|
||||||
|
<button
|
||||||
|
className="h-5 w-5 hover:bg-custom-background-90 hover:text-custom-text-100 rounded-sm cursor-pointer"
|
||||||
|
onClick={() => handleToggleExpand(issue.id)}
|
||||||
|
>
|
||||||
|
<Icon iconName="chevron_right" className={`${expanded ? "rotate-90" : ""}`} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="flex items-center px-4 py-2.5 h-full truncate flex-grow">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="truncate text-custom-text-100 text-left cursor-pointer w-full text-[0.825rem]"
|
||||||
|
onClick={openPeekOverview}
|
||||||
|
>
|
||||||
|
{issue.name}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,36 +1,34 @@
|
|||||||
import React, { useState } from "react";
|
import React from "react";
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import { SingleSpreadsheetIssue } from "components/core";
|
import { IssueColumn } from "components/core";
|
||||||
// hooks
|
// hooks
|
||||||
import useSubIssue from "hooks/use-sub-issue";
|
import useSubIssue from "hooks/use-sub-issue";
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types";
|
import { IIssue, Properties, UserAuth } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
index: number;
|
projectId: string;
|
||||||
expandedIssues: string[];
|
expandedIssues: string[];
|
||||||
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
|
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
properties: Properties;
|
properties: Properties;
|
||||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
gridTemplateColumns: string;
|
setCurrentProjectId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
disableUserActions: boolean;
|
disableUserActions: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
nestingLevel?: number;
|
nestingLevel?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetIssues: React.FC<Props> = ({
|
export const SpreadsheetIssuesColumn: React.FC<Props> = ({
|
||||||
index,
|
|
||||||
issue,
|
issue,
|
||||||
|
projectId,
|
||||||
expandedIssues,
|
expandedIssues,
|
||||||
setExpandedIssues,
|
setExpandedIssues,
|
||||||
gridTemplateColumns,
|
|
||||||
properties,
|
properties,
|
||||||
handleIssueAction,
|
handleIssueAction,
|
||||||
|
setCurrentProjectId,
|
||||||
disableUserActions,
|
disableUserActions,
|
||||||
user,
|
|
||||||
userAuth,
|
userAuth,
|
||||||
nestingLevel = 0,
|
nestingLevel = 0,
|
||||||
}) => {
|
}) => {
|
||||||
@ -49,22 +47,20 @@ export const SpreadsheetIssues: React.FC<Props> = ({
|
|||||||
|
|
||||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<SingleSpreadsheetIssue
|
<IssueColumn
|
||||||
issue={issue}
|
issue={issue}
|
||||||
projectId={issue.project_detail.id}
|
projectId={projectId}
|
||||||
index={index}
|
|
||||||
expanded={isExpanded}
|
expanded={isExpanded}
|
||||||
handleToggleExpand={handleToggleExpand}
|
handleToggleExpand={handleToggleExpand}
|
||||||
gridTemplateColumns={gridTemplateColumns}
|
|
||||||
properties={properties}
|
properties={properties}
|
||||||
handleEditIssue={() => handleIssueAction(issue, "edit")}
|
handleEditIssue={() => handleIssueAction(issue, "edit")}
|
||||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||||
|
setCurrentProjectId={setCurrentProjectId}
|
||||||
disableUserActions={disableUserActions}
|
disableUserActions={disableUserActions}
|
||||||
user={user}
|
|
||||||
userAuth={userAuth}
|
userAuth={userAuth}
|
||||||
nestingLevel={nestingLevel}
|
nestingLevel={nestingLevel}
|
||||||
/>
|
/>
|
||||||
@ -74,17 +70,16 @@ export const SpreadsheetIssues: React.FC<Props> = ({
|
|||||||
subIssues &&
|
subIssues &&
|
||||||
subIssues.length > 0 &&
|
subIssues.length > 0 &&
|
||||||
subIssues.map((subIssue: IIssue) => (
|
subIssues.map((subIssue: IIssue) => (
|
||||||
<SpreadsheetIssues
|
<SpreadsheetIssuesColumn
|
||||||
key={subIssue.id}
|
key={subIssue.id}
|
||||||
issue={subIssue}
|
issue={subIssue}
|
||||||
index={index}
|
projectId={subIssue.project_detail.id}
|
||||||
expandedIssues={expandedIssues}
|
expandedIssues={expandedIssues}
|
||||||
setExpandedIssues={setExpandedIssues}
|
setExpandedIssues={setExpandedIssues}
|
||||||
gridTemplateColumns={gridTemplateColumns}
|
|
||||||
properties={properties}
|
properties={properties}
|
||||||
handleIssueAction={handleIssueAction}
|
handleIssueAction={handleIssueAction}
|
||||||
|
setCurrentProjectId={setCurrentProjectId}
|
||||||
disableUserActions={disableUserActions}
|
disableUserActions={disableUserActions}
|
||||||
user={user}
|
|
||||||
userAuth={userAuth}
|
userAuth={userAuth}
|
||||||
nestingLevel={nestingLevel + 1}
|
nestingLevel={nestingLevel + 1}
|
||||||
/>
|
/>
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./spreadsheet-label-column";
|
||||||
|
export * from "./label-column";
|
@ -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<IIssue>, issue: IIssue) => void;
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LabelColumn: React.FC<Props> = ({
|
||||||
|
issue,
|
||||||
|
projectId,
|
||||||
|
partialUpdateIssue,
|
||||||
|
properties,
|
||||||
|
user,
|
||||||
|
isNotAllowed,
|
||||||
|
}) => {
|
||||||
|
const handleLabelChange = (data: any) => {
|
||||||
|
partialUpdateIssue({ labels_list: data }, issue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||||
|
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-200">
|
||||||
|
{properties.labels && (
|
||||||
|
<LabelSelect
|
||||||
|
value={issue.labels}
|
||||||
|
projectId={projectId}
|
||||||
|
onChange={handleLabelChange}
|
||||||
|
labelsDetails={issue.label_details}
|
||||||
|
hideDropdownArrow
|
||||||
|
maxRender={1}
|
||||||
|
user={user}
|
||||||
|
disabled={isNotAllowed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -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<IIssue>, issue: IIssue) => void;
|
||||||
|
expandedIssues: string[];
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetLabelColumn: React.FC<Props> = ({
|
||||||
|
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 (
|
||||||
|
<div>
|
||||||
|
<LabelColumn
|
||||||
|
issue={issue}
|
||||||
|
projectId={projectId}
|
||||||
|
properties={properties}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isExpanded &&
|
||||||
|
!isLoading &&
|
||||||
|
subIssues &&
|
||||||
|
subIssues.length > 0 &&
|
||||||
|
subIssues.map((subIssue: IIssue) => (
|
||||||
|
<SpreadsheetLabelColumn
|
||||||
|
key={subIssue.id}
|
||||||
|
issue={subIssue}
|
||||||
|
projectId={subIssue.project_detail.id}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
properties={properties}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./spreadsheet-priority-column";
|
||||||
|
export * from "./priority-column";
|
@ -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<IIssue>, issue: IIssue) => void;
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PriorityColumn: React.FC<Props> = ({
|
||||||
|
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 (
|
||||||
|
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||||
|
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-200">
|
||||||
|
{properties.priority && (
|
||||||
|
<PrioritySelect
|
||||||
|
value={issue.priority}
|
||||||
|
onChange={handlePriorityChange}
|
||||||
|
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||||
|
hideDropdownArrow
|
||||||
|
disabled={isNotAllowed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -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<IIssue>, issue: IIssue) => void;
|
||||||
|
expandedIssues: string[];
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetPriorityColumn: React.FC<Props> = ({
|
||||||
|
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 (
|
||||||
|
<div>
|
||||||
|
<PriorityColumn
|
||||||
|
issue={issue}
|
||||||
|
projectId={projectId}
|
||||||
|
properties={properties}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isExpanded &&
|
||||||
|
!isLoading &&
|
||||||
|
subIssues &&
|
||||||
|
subIssues.length > 0 &&
|
||||||
|
subIssues.map((subIssue: IIssue) => (
|
||||||
|
<SpreadsheetPriorityColumn
|
||||||
|
key={subIssue.id}
|
||||||
|
issue={subIssue}
|
||||||
|
projectId={subIssue.project_detail.id}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
properties={properties}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,23 +1,52 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
|
|
||||||
// next
|
// next
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { KeyedMutator, mutate } from "swr";
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import { SpreadsheetColumns, SpreadsheetIssues, ListInlineCreateIssueForm } from "components/core";
|
import {
|
||||||
import { CustomMenu, Spinner } from "components/ui";
|
SpreadsheetAssigneeColumn,
|
||||||
|
SpreadsheetCreatedOnColumn,
|
||||||
|
SpreadsheetDueDateColumn,
|
||||||
|
SpreadsheetEstimateColumn,
|
||||||
|
SpreadsheetIssuesColumn,
|
||||||
|
SpreadsheetLabelColumn,
|
||||||
|
SpreadsheetPriorityColumn,
|
||||||
|
SpreadsheetStartDateColumn,
|
||||||
|
SpreadsheetStateColumn,
|
||||||
|
SpreadsheetUpdatedOnColumn,
|
||||||
|
} from "components/core";
|
||||||
|
import { Spinner } from "components/ui";
|
||||||
import { IssuePeekOverview } from "components/issues";
|
import { IssuePeekOverview } from "components/issues";
|
||||||
// hooks
|
// hooks
|
||||||
import useIssuesProperties from "hooks/use-issue-properties";
|
import useIssuesProperties from "hooks/use-issue-properties";
|
||||||
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types";
|
import { ICurrentUserResponse, IIssue, ISubIssueResponse, UserAuth } from "types";
|
||||||
// constants
|
import useWorkspaceIssuesFilters from "hooks/use-worskpace-issue-filter";
|
||||||
import { SPREADSHEET_COLUMN } from "constants/spreadsheet";
|
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
|
// icon
|
||||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
spreadsheetIssues: IIssue[];
|
||||||
|
mutateIssues: KeyedMutator<
|
||||||
|
| IIssue[]
|
||||||
|
| {
|
||||||
|
[key: string]: IIssue[];
|
||||||
|
}
|
||||||
|
>;
|
||||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
disableUserActions: boolean;
|
disableUserActions: boolean;
|
||||||
@ -26,6 +55,8 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetView: React.FC<Props> = ({
|
export const SpreadsheetView: React.FC<Props> = ({
|
||||||
|
spreadsheetIssues,
|
||||||
|
mutateIssues,
|
||||||
handleIssueAction,
|
handleIssueAction,
|
||||||
openIssuesListModal,
|
openIssuesListModal,
|
||||||
disableUserActions,
|
disableUserActions,
|
||||||
@ -33,124 +64,218 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
userAuth,
|
userAuth,
|
||||||
}) => {
|
}) => {
|
||||||
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
||||||
const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false);
|
const [currentProjectId, setCurrentProjectId] = useState<string | null>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId, workspaceViewId } = router.query;
|
||||||
|
|
||||||
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
|
||||||
|
|
||||||
const { spreadsheetIssues, mutateIssues } = useSpreadsheetIssuesView();
|
|
||||||
|
|
||||||
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||||
|
|
||||||
const columnData = SPREADSHEET_COLUMN.map((column) => ({
|
const workspaceIssuesPath = [
|
||||||
...column,
|
{
|
||||||
isActive: properties
|
params: {
|
||||||
? column.propertyName === "labels"
|
sub_issue: false,
|
||||||
? properties[column.propertyName as keyof Properties]
|
},
|
||||||
: column.propertyName === "title"
|
path: "workspace-views/all-issues",
|
||||||
? true
|
},
|
||||||
: properties[column.propertyName as keyof Properties]
|
{
|
||||||
: false,
|
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
|
const currentWorkspaceIssuePath = workspaceIssuesPath.find((path) =>
|
||||||
.filter((column) => column.isActive)
|
router.pathname.includes(path.path)
|
||||||
.map((column) => column.colSize)
|
);
|
||||||
.join(" ");
|
|
||||||
|
const { params: workspaceViewParams } = useWorkspaceIssuesFilters(
|
||||||
|
workspaceSlug?.toString(),
|
||||||
|
workspaceViewId?.toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
const { params } = useSpreadsheetIssuesView();
|
||||||
|
|
||||||
|
const partialUpdateIssue = useCallback(
|
||||||
|
(formData: Partial<IIssue>, 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<ISubIssueResponse>(
|
||||||
|
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<IIssue[]>(
|
||||||
|
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<any>) => (
|
||||||
|
<div className="relative flex flex-col h-max w-full bg-custom-background-100 rounded-sm">
|
||||||
|
<div className="flex items-center min-w-[9rem] px-4 py-2.5 text-sm font-medium z-[1] h-11 w-full sticky top-0 bg-custom-background-90 border border-l-0 border-custom-border-200">
|
||||||
|
{header}
|
||||||
|
</div>
|
||||||
|
<div className="h-full min-w-[9rem] w-full">
|
||||||
|
{spreadsheetIssues.map((issue: IIssue, index) => (
|
||||||
|
<Component
|
||||||
|
key={`${issue.id}_${index}`}
|
||||||
|
issue={issue}
|
||||||
|
projectId={issue.project_detail.id}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
properties={properties}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IssuePeekOverview
|
<IssuePeekOverview
|
||||||
handleMutation={() => mutateIssues()}
|
handleMutation={() => mutateIssues()}
|
||||||
projectId={projectId?.toString() ?? ""}
|
projectId={currentProjectId ?? ""}
|
||||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
readOnly={disableUserActions}
|
readOnly={disableUserActions}
|
||||||
/>
|
/>
|
||||||
<div
|
<div className="relative flex h-full w-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-100">
|
||||||
className={`h-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-100 ${
|
|
||||||
isInlineCreateIssueFormOpen ? "mb-24" : "mb-12"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="sticky z-[2] top-0 border-b border-custom-border-200 bg-custom-background-90 w-full min-w-max">
|
|
||||||
<SpreadsheetColumns columnData={columnData} gridTemplateColumns={gridTemplateColumns} />
|
|
||||||
</div>
|
|
||||||
{spreadsheetIssues ? (
|
{spreadsheetIssues ? (
|
||||||
<div className="flex flex-col h-full w-full bg-custom-background-100 rounded-sm">
|
<>
|
||||||
|
<div className="sticky left-0 w-[28rem] z-[2]">
|
||||||
|
<div className="relative flex flex-col h-max w-full bg-custom-background-100 rounded-sm z-[2]">
|
||||||
|
<div className="flex items-center text-sm font-medium z-[2] h-11 w-full sticky top-0 bg-custom-background-90 border border-l-0 border-custom-border-200">
|
||||||
|
<span className="flex items-center px-4 py-2.5 h-full w-20 flex-shrink-0">
|
||||||
|
ID
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center px-4 py-2.5 h-full w-full flex-grow">
|
||||||
|
Issue
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{spreadsheetIssues.map((issue: IIssue, index) => (
|
{spreadsheetIssues.map((issue: IIssue, index) => (
|
||||||
<SpreadsheetIssues
|
<SpreadsheetIssuesColumn
|
||||||
key={`${issue.id}_${index}`}
|
key={`${issue.id}_${index}`}
|
||||||
index={index}
|
|
||||||
issue={issue}
|
issue={issue}
|
||||||
|
projectId={issue.project_detail.id}
|
||||||
expandedIssues={expandedIssues}
|
expandedIssues={expandedIssues}
|
||||||
setExpandedIssues={setExpandedIssues}
|
setExpandedIssues={setExpandedIssues}
|
||||||
gridTemplateColumns={gridTemplateColumns}
|
setCurrentProjectId={setCurrentProjectId}
|
||||||
properties={properties}
|
properties={properties}
|
||||||
handleIssueAction={handleIssueAction}
|
handleIssueAction={handleIssueAction}
|
||||||
disableUserActions={disableUserActions}
|
disableUserActions={disableUserActions}
|
||||||
user={user}
|
|
||||||
userAuth={userAuth}
|
userAuth={userAuth}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<Spinner />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
{renderColumn("State", SpreadsheetStateColumn)}
|
||||||
<div
|
{renderColumn("Priority", SpreadsheetPriorityColumn)}
|
||||||
className={`absolute bottom-0 left-0 z-10 group hover:rounded-sm bg-custom-background-100 hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max ${
|
{renderColumn("Assignees", SpreadsheetAssigneeColumn)}
|
||||||
isInlineCreateIssueFormOpen ? "pb-2" : ""
|
{renderColumn("Label", SpreadsheetLabelColumn)}
|
||||||
}`}
|
{renderColumn("Start Date", SpreadsheetStartDateColumn)}
|
||||||
>
|
{renderColumn("Due Date", SpreadsheetDueDateColumn)}
|
||||||
<ListInlineCreateIssueForm
|
{renderColumn("Estimate", SpreadsheetEstimateColumn)}
|
||||||
isOpen={isInlineCreateIssueFormOpen}
|
{renderColumn("Created On", SpreadsheetCreatedOnColumn)}
|
||||||
handleClose={() => setIsInlineCreateIssueFormOpen(false)}
|
{renderColumn("Updated On", SpreadsheetUpdatedOnColumn)}
|
||||||
prePopulatedData={{
|
</>
|
||||||
...(cycleId && { cycle: cycleId.toString() }),
|
) : (
|
||||||
...(moduleId && { module: moduleId.toString() }),
|
<div className="flex flex-col justify-center items-center h-full w-full">
|
||||||
}}
|
<Spinner />
|
||||||
/>
|
</div>
|
||||||
|
|
||||||
{type === "issue"
|
|
||||||
? !disableUserActions &&
|
|
||||||
!isInlineCreateIssueFormOpen && (
|
|
||||||
<button
|
|
||||||
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 border-custom-border-200 w-full"
|
|
||||||
onClick={() => setIsInlineCreateIssueFormOpen(true)}
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-4 w-4" />
|
|
||||||
Add Issue
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
: !disableUserActions &&
|
|
||||||
!isInlineCreateIssueFormOpen && (
|
|
||||||
<CustomMenu
|
|
||||||
className="sticky left-0 z-[1] !w-full"
|
|
||||||
customButton={
|
|
||||||
<button
|
|
||||||
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 border-custom-border-200 w-full"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-4 w-4" />
|
|
||||||
Add Issue
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
position="left"
|
|
||||||
verticalPosition="top"
|
|
||||||
optionsClassName="left-5 !w-36"
|
|
||||||
noBorder
|
|
||||||
>
|
|
||||||
<CustomMenu.MenuItem onClick={() => setIsInlineCreateIssueFormOpen(true)}>
|
|
||||||
Create new
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
{openIssuesListModal && (
|
|
||||||
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
|
||||||
Add an existing issue
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
)}
|
|
||||||
</CustomMenu>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./spreadsheet-start-date-column";
|
||||||
|
export * from "./start-date-column";
|
@ -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<IIssue>, issue: IIssue) => void;
|
||||||
|
expandedIssues: string[];
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetStartDateColumn: React.FC<Props> = ({
|
||||||
|
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 (
|
||||||
|
<div>
|
||||||
|
<StartDateColumn
|
||||||
|
issue={issue}
|
||||||
|
projectId={projectId}
|
||||||
|
properties={properties}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isExpanded &&
|
||||||
|
!isLoading &&
|
||||||
|
subIssues &&
|
||||||
|
subIssues.length > 0 &&
|
||||||
|
subIssues.map((subIssue: IIssue) => (
|
||||||
|
<SpreadsheetStartDateColumn
|
||||||
|
key={subIssue.id}
|
||||||
|
issue={subIssue}
|
||||||
|
projectId={subIssue.project_detail.id}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
properties={properties}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -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<IIssue>, issue: IIssue) => void;
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StartDateColumn: React.FC<Props> = ({
|
||||||
|
issue,
|
||||||
|
projectId,
|
||||||
|
partialUpdateIssue,
|
||||||
|
properties,
|
||||||
|
user,
|
||||||
|
isNotAllowed,
|
||||||
|
}) => (
|
||||||
|
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||||
|
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-200">
|
||||||
|
{properties.due_date && (
|
||||||
|
<ViewStartDateSelect
|
||||||
|
issue={issue}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
noBorder
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./spreadsheet-state-column";
|
||||||
|
export * from "./state-column";
|
@ -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<IIssue>, issue: IIssue) => void;
|
||||||
|
expandedIssues: string[];
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetStateColumn: React.FC<Props> = ({
|
||||||
|
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 (
|
||||||
|
<div>
|
||||||
|
<StateColumn
|
||||||
|
issue={issue}
|
||||||
|
projectId={projectId}
|
||||||
|
properties={properties}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isExpanded &&
|
||||||
|
!isLoading &&
|
||||||
|
subIssues &&
|
||||||
|
subIssues.length > 0 &&
|
||||||
|
subIssues.map((subIssue: IIssue) => (
|
||||||
|
<SpreadsheetStateColumn
|
||||||
|
key={subIssue.id}
|
||||||
|
issue={subIssue}
|
||||||
|
projectId={subIssue.project_detail.id}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
properties={properties}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -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<IIssue>, issue: IIssue) => void;
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StateColumn: React.FC<Props> = ({
|
||||||
|
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 (
|
||||||
|
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||||
|
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-200">
|
||||||
|
{properties.state && (
|
||||||
|
<StateSelect
|
||||||
|
value={issue.state_detail}
|
||||||
|
projectId={projectId}
|
||||||
|
onChange={handleStateChange}
|
||||||
|
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||||
|
hideDropdownArrow
|
||||||
|
disabled={isNotAllowed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./spreadsheet-updated-on-column";
|
||||||
|
export * from "./updated-on-column";
|
@ -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<IIssue>, issue: IIssue) => void;
|
||||||
|
expandedIssues: string[];
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetUpdatedOnColumn: React.FC<Props> = ({
|
||||||
|
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 (
|
||||||
|
<div>
|
||||||
|
<UpdatedOnColumn
|
||||||
|
issue={issue}
|
||||||
|
projectId={projectId}
|
||||||
|
properties={properties}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isExpanded &&
|
||||||
|
!isLoading &&
|
||||||
|
subIssues &&
|
||||||
|
subIssues.length > 0 &&
|
||||||
|
subIssues.map((subIssue: IIssue) => (
|
||||||
|
<SpreadsheetUpdatedOnColumn
|
||||||
|
key={subIssue.id}
|
||||||
|
issue={subIssue}
|
||||||
|
projectId={subIssue.project_detail.id}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
properties={properties}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -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<IIssue>, issue: IIssue) => void;
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UpdatedOnColumn: React.FC<Props> = ({
|
||||||
|
issue,
|
||||||
|
projectId,
|
||||||
|
partialUpdateIssue,
|
||||||
|
properties,
|
||||||
|
user,
|
||||||
|
isNotAllowed,
|
||||||
|
}) => (
|
||||||
|
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||||
|
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-200">
|
||||||
|
{properties.updated_on && (
|
||||||
|
<div className="flex items-center text-xs cursor-default text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||||
|
{renderLongDetailDateFormat(issue.updated_at)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
@ -4,18 +4,21 @@ import { useRouter } from "next/router";
|
|||||||
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// hook
|
||||||
|
import useProjects from "hooks/use-projects";
|
||||||
|
import useWorkspaceMembers from "hooks/use-workspace-members";
|
||||||
// services
|
// services
|
||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
// components
|
// components
|
||||||
import { DateFilterModal } from "components/core";
|
import { DateFilterModal } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
import { MultiLevelDropdown } from "components/ui";
|
import { Avatar, MultiLevelDropdown } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { PriorityIcon, StateGroupIcon } from "components/icons";
|
import { PriorityIcon, StateGroupIcon } from "components/icons";
|
||||||
// helpers
|
// helpers
|
||||||
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssueFilterOptions, IQuery, TStateGroups } from "types";
|
import { IIssueFilterOptions, TStateGroups } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { WORKSPACE_LABELS } from "constants/fetch-keys";
|
import { WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||||
// constants
|
// constants
|
||||||
@ -23,7 +26,7 @@ import { GROUP_CHOICES, PRIORITIES } from "constants/project";
|
|||||||
import { DATE_FILTER_OPTIONS } from "constants/filters";
|
import { DATE_FILTER_OPTIONS } from "constants/filters";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
filters: Partial<IIssueFilterOptions> | IQuery;
|
filters: Partial<IIssueFilterOptions>;
|
||||||
onSelect: (option: any) => void;
|
onSelect: (option: any) => void;
|
||||||
direction?: "left" | "right";
|
direction?: "left" | "right";
|
||||||
height?: "sm" | "md" | "rg" | "lg";
|
height?: "sm" | "md" | "rg" | "lg";
|
||||||
@ -55,6 +58,11 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { projects: allProjects } = useProjects();
|
||||||
|
const joinedProjects = allProjects?.filter((p) => p.is_member);
|
||||||
|
|
||||||
|
const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isDateFilterModalOpen && (
|
{isDateFilterModalOpen && (
|
||||||
@ -74,25 +82,19 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
|
|||||||
height={height}
|
height={height}
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
id: "priority",
|
id: "project",
|
||||||
label: "Priority",
|
label: "Project",
|
||||||
value: PRIORITIES,
|
value: joinedProjects,
|
||||||
hasChildren: true,
|
hasChildren: true,
|
||||||
children: [
|
children: joinedProjects?.map((project) => ({
|
||||||
...PRIORITIES.map((priority) => ({
|
id: project.id,
|
||||||
id: priority === null ? "null" : priority,
|
label: <div className="flex items-center gap-2">{project.name}</div>,
|
||||||
label: (
|
|
||||||
<div className="flex items-center gap-2 capitalize">
|
|
||||||
<PriorityIcon priority={priority} /> {priority ?? "None"}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
value: {
|
value: {
|
||||||
key: "priority",
|
key: "project",
|
||||||
value: priority === null ? "null" : priority,
|
value: project.id,
|
||||||
},
|
},
|
||||||
selected: filters?.priority?.includes(priority === null ? "null" : priority),
|
selected: filters?.project?.includes(project.id),
|
||||||
})),
|
})),
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "state_group",
|
id: "state_group",
|
||||||
@ -142,6 +144,87 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
|
|||||||
selected: filters?.labels?.includes(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: (
|
||||||
|
<div className="flex items-center gap-2 capitalize">
|
||||||
|
<PriorityIcon priority={priority} /> {priority ?? "None"}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
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: (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Avatar user={member.member} />
|
||||||
|
{member.member.display_name}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
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: (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Avatar user={member.member} />
|
||||||
|
{member.member.display_name}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
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: (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Avatar user={member.member} />
|
||||||
|
{member.member.display_name}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
value: {
|
||||||
|
key: "subscriber",
|
||||||
|
value: member.member.id,
|
||||||
|
},
|
||||||
|
selected: filters?.subscriber?.includes(member.member.id),
|
||||||
|
})),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "start_date",
|
id: "start_date",
|
||||||
label: "Start date",
|
label: "Start date",
|
||||||
|
@ -2,25 +2,20 @@ import React from "react";
|
|||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// headless ui
|
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
|
||||||
// hooks
|
// hooks
|
||||||
import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
|
import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
|
||||||
import useEstimateOption from "hooks/use-estimate-option";
|
|
||||||
// components
|
// components
|
||||||
import { MyIssuesSelectFilters } from "components/issues";
|
import { MyIssuesSelectFilters } from "components/issues";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu, ToggleSwitch, Tooltip } from "components/ui";
|
import { Tooltip } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
import { FormatListBulletedOutlined } from "@mui/icons-material";
|
||||||
import { FormatListBulletedOutlined, GridViewOutlined } from "@mui/icons-material";
|
import { CreditCard } from "lucide-react";
|
||||||
// helpers
|
// helpers
|
||||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||||
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||||
// types
|
// types
|
||||||
import { Properties, TIssueViewOptions } from "types";
|
import { TIssueViewOptions } from "types";
|
||||||
// constants
|
|
||||||
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
|
|
||||||
|
|
||||||
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
|
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
|
||||||
{
|
{
|
||||||
@ -28,19 +23,26 @@ const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
|
|||||||
Icon: FormatListBulletedOutlined,
|
Icon: FormatListBulletedOutlined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "kanban",
|
type: "spreadsheet",
|
||||||
Icon: GridViewOutlined,
|
Icon: CreditCard,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const MyIssuesViewOptions: React.FC = () => {
|
export const MyIssuesViewOptions: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug, workspaceViewId } = router.query;
|
||||||
|
|
||||||
const { displayFilters, setDisplayFilters, properties, setProperty, filters, setFilters } =
|
const { displayFilters, setDisplayFilters, filters, setFilters } = useMyIssuesFilters(
|
||||||
useMyIssuesFilters(workspaceSlug?.toString());
|
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 (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -49,29 +51,35 @@ export const MyIssuesViewOptions: React.FC = () => {
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
key={option.type}
|
key={option.type}
|
||||||
tooltipContent={
|
tooltipContent={
|
||||||
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} Layout</span>
|
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
|
||||||
}
|
}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
|
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-100 duration-300 ${
|
||||||
displayFilters?.layout === option.type
|
displayFilters?.layout === option.type
|
||||||
? "bg-custom-sidebar-background-80"
|
? "bg-custom-sidebar-background-100 shadow-sm"
|
||||||
: "text-custom-sidebar-text-200"
|
: "text-custom-sidebar-text-200"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setDisplayFilters({ layout: option.type })}
|
onClick={() => {
|
||||||
|
setDisplayFilters({ layout: option.type });
|
||||||
|
if (option.type === "spreadsheet")
|
||||||
|
router.push(`/${workspaceSlug}/workspace-views/all-issues`);
|
||||||
|
else router.push(`/${workspaceSlug}/workspace-views`);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<option.Icon
|
<option.Icon
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
}}
|
}}
|
||||||
className={option.type === "gantt_chart" ? "rotate-90" : ""}
|
className={option.type === "spreadsheet" ? "h-4 w-4" : ""}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{showFilters && (
|
||||||
<MyIssuesSelectFilters
|
<MyIssuesSelectFilters
|
||||||
filters={filters}
|
filters={filters}
|
||||||
onSelect={(option) => {
|
onSelect={(option) => {
|
||||||
@ -101,198 +109,7 @@ export const MyIssuesViewOptions: React.FC = () => {
|
|||||||
direction="left"
|
direction="left"
|
||||||
height="rg"
|
height="rg"
|
||||||
/>
|
/>
|
||||||
<Popover className="relative">
|
|
||||||
{({ open }) => (
|
|
||||||
<>
|
|
||||||
<Popover.Button
|
|
||||||
className={`group flex items-center gap-2 rounded-md border border-custom-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
|
|
||||||
open
|
|
||||||
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
|
|
||||||
: "text-custom-sidebar-text-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Display
|
|
||||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
|
||||||
</Popover.Button>
|
|
||||||
|
|
||||||
<Transition
|
|
||||||
as={React.Fragment}
|
|
||||||
enter="transition ease-out duration-200"
|
|
||||||
enterFrom="opacity-0 translate-y-1"
|
|
||||||
enterTo="opacity-100 translate-y-0"
|
|
||||||
leave="transition ease-in duration-150"
|
|
||||||
leaveFrom="opacity-100 translate-y-0"
|
|
||||||
leaveTo="opacity-0 translate-y-1"
|
|
||||||
>
|
|
||||||
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
|
|
||||||
<div className="relative divide-y-2 divide-custom-border-200">
|
|
||||||
<div className="space-y-4 pb-3 text-xs">
|
|
||||||
{displayFilters?.layout !== "calendar" &&
|
|
||||||
displayFilters?.layout !== "spreadsheet" && (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h4 className="text-custom-text-200">Group by</h4>
|
|
||||||
<div className="w-28">
|
|
||||||
<CustomMenu
|
|
||||||
label={
|
|
||||||
displayFilters?.group_by === "project"
|
|
||||||
? "Project"
|
|
||||||
: GROUP_BY_OPTIONS.find(
|
|
||||||
(option) => 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 (
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
key={option.key}
|
|
||||||
onClick={() => setDisplayFilters({ group_by: option.key })}
|
|
||||||
>
|
|
||||||
{option.name}
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h4 className="text-custom-text-200">Order by</h4>
|
|
||||||
<div className="w-28">
|
|
||||||
<CustomMenu
|
|
||||||
label={
|
|
||||||
ORDER_BY_OPTIONS.find(
|
|
||||||
(option) => 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 (
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
key={option.key}
|
|
||||||
onClick={() => {
|
|
||||||
setDisplayFilters({ order_by: option.key });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{option.name}
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h4 className="text-custom-text-200">Issue type</h4>
|
|
||||||
<div className="w-28">
|
|
||||||
<CustomMenu
|
|
||||||
label={
|
|
||||||
FILTER_ISSUE_OPTIONS.find(
|
|
||||||
(option) => option.key === displayFilters?.type
|
|
||||||
)?.name ?? "Select"
|
|
||||||
}
|
|
||||||
className="!w-full"
|
|
||||||
buttonClassName="w-full"
|
|
||||||
>
|
|
||||||
{FILTER_ISSUE_OPTIONS.map((option) => (
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
key={option.key}
|
|
||||||
onClick={() =>
|
|
||||||
setDisplayFilters({
|
|
||||||
type: option.key,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{option.name}
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
))}
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{displayFilters?.layout !== "calendar" &&
|
|
||||||
displayFilters?.layout !== "spreadsheet" && (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h4 className="text-custom-text-200">Show empty groups</h4>
|
|
||||||
<div className="w-28">
|
|
||||||
<ToggleSwitch
|
|
||||||
value={displayFilters?.show_empty_groups ?? true}
|
|
||||||
onChange={() =>
|
|
||||||
setDisplayFilters({
|
|
||||||
show_empty_groups: !displayFilters?.show_empty_groups,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2 py-3">
|
|
||||||
<h4 className="text-sm text-custom-text-200">Display Properties</h4>
|
|
||||||
<div className="flex flex-wrap items-center gap-2 text-custom-text-200">
|
|
||||||
{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 (
|
|
||||||
<button
|
|
||||||
key={key}
|
|
||||||
type="button"
|
|
||||||
className={`rounded border px-2 py-1 text-xs capitalize ${
|
|
||||||
properties[key as keyof Properties]
|
|
||||||
? "border-custom-primary bg-custom-primary text-white"
|
|
||||||
: "border-custom-border-200"
|
|
||||||
}`}
|
|
||||||
onClick={() => setProperty(key as keyof Properties)}
|
|
||||||
>
|
|
||||||
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Popover.Panel>
|
|
||||||
</Transition>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex items-center gap-x- px-1 py-0.5 rounded bg-custom-sidebar-background-90 ">
|
||||||
|
{issueViewOptions.map((option) => (
|
||||||
|
<Tooltip
|
||||||
|
key={option.type}
|
||||||
|
tooltipContent={
|
||||||
|
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
|
||||||
|
}
|
||||||
|
position="bottom"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-100 duration-300 ${
|
||||||
|
displayFilters?.layout === option.type
|
||||||
|
? "bg-custom-sidebar-background-100 shadow-sm"
|
||||||
|
: "text-custom-sidebar-text-200"
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
setDisplayFilters({ layout: option.type });
|
||||||
|
if (option.type === "spreadsheet")
|
||||||
|
router.push(`/${workspaceSlug}/workspace-views/all-issues`);
|
||||||
|
else router.push(`/${workspaceSlug}/workspace-views`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option.Icon
|
||||||
|
sx={{
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
className={option.type === "spreadsheet" ? "h-4 w-4" : ""}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showFilters && (
|
||||||
|
<>
|
||||||
|
<MyIssuesSelectFilters
|
||||||
|
filters={filters}
|
||||||
|
onSelect={(option) => {
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -8,6 +8,7 @@ import { mutate } from "swr";
|
|||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import viewsService from "services/views.service";
|
import viewsService from "services/views.service";
|
||||||
|
import workspaceService from "services/workspace.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// ui
|
// ui
|
||||||
@ -17,16 +18,17 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
|||||||
// types
|
// types
|
||||||
import type { ICurrentUserResponse, IView } from "types";
|
import type { ICurrentUserResponse, IView } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { VIEWS_LIST } from "constants/fetch-keys";
|
import { VIEWS_LIST, WORKSPACE_VIEWS_LIST } from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
viewType: "project" | "workspace";
|
||||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
data: IView | null;
|
data: IView | null;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteViewModal: React.FC<Props> = ({ isOpen, data, setIsOpen, user }) => {
|
export const DeleteViewModal: React.FC<Props> = ({ isOpen, data, setIsOpen, viewType, user }) => {
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -41,14 +43,15 @@ export const DeleteViewModal: React.FC<Props> = ({ isOpen, data, setIsOpen, user
|
|||||||
|
|
||||||
const handleDeletion = async () => {
|
const handleDeletion = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
|
if (viewType === "project") {
|
||||||
if (!workspaceSlug || !data || !projectId) return;
|
if (!workspaceSlug || !data || !projectId) return;
|
||||||
|
|
||||||
await viewsService
|
await viewsService
|
||||||
.deleteView(workspaceSlug as string, projectId as string, data.id, user)
|
.deleteView(workspaceSlug as string, projectId as string, data.id, user)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
mutate<IView[]>(
|
mutate<IView[]>(VIEWS_LIST(projectId as string), (views) =>
|
||||||
VIEWS_LIST(projectId as string),
|
views?.filter((view) => view.id !== data.id)
|
||||||
(views) => views?.filter((view) => view.id !== data.id)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
handleClose();
|
handleClose();
|
||||||
@ -69,6 +72,35 @@ export const DeleteViewModal: React.FC<Props> = ({ isOpen, data, setIsOpen, user
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
if (!workspaceSlug || !data) return;
|
||||||
|
|
||||||
|
await workspaceService
|
||||||
|
.deleteView(workspaceSlug as string, data.id)
|
||||||
|
.then(() => {
|
||||||
|
mutate<IView[]>(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);
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -10,6 +10,8 @@ import { useForm } from "react-hook-form";
|
|||||||
import stateService from "services/state.service";
|
import stateService from "services/state.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useProjectMembers from "hooks/use-project-members";
|
import useProjectMembers from "hooks/use-project-members";
|
||||||
|
import useProjects from "hooks/use-projects";
|
||||||
|
import useWorkspaceMembers from "hooks/use-workspace-members";
|
||||||
// components
|
// components
|
||||||
import { FiltersList } from "components/core";
|
import { FiltersList } from "components/core";
|
||||||
import { SelectFilters } from "components/views";
|
import { SelectFilters } from "components/views";
|
||||||
@ -22,13 +24,14 @@ import { getStatesList } from "helpers/state.helper";
|
|||||||
import { IQuery, IView } from "types";
|
import { IQuery, IView } from "types";
|
||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
// fetch-keys
|
// 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 = {
|
type Props = {
|
||||||
handleFormSubmit: (values: IView) => Promise<void>;
|
handleFormSubmit: (values: IView) => Promise<void>;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
status: boolean;
|
status: boolean;
|
||||||
data?: IView | null;
|
data?: IView | null;
|
||||||
|
viewType?: "workspace" | "project";
|
||||||
preLoadedData?: Partial<IView> | null;
|
preLoadedData?: Partial<IView> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -42,6 +45,7 @@ export const ViewForm: React.FC<Props> = ({
|
|||||||
handleClose,
|
handleClose,
|
||||||
status,
|
status,
|
||||||
data,
|
data,
|
||||||
|
viewType,
|
||||||
preLoadedData,
|
preLoadedData,
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -77,8 +81,26 @@ export const ViewForm: React.FC<Props> = ({
|
|||||||
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
|
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
|
||||||
: null
|
: 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 { 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) => {
|
const handleCreateUpdateView = async (formData: IView) => {
|
||||||
await handleFormSubmit(formData);
|
await handleFormSubmit(formData);
|
||||||
|
|
||||||
@ -91,12 +113,14 @@ export const ViewForm: React.FC<Props> = ({
|
|||||||
setValue("query", {
|
setValue("query", {
|
||||||
assignees: null,
|
assignees: null,
|
||||||
created_by: null,
|
created_by: null,
|
||||||
|
subscriber: null,
|
||||||
labels: null,
|
labels: null,
|
||||||
priority: null,
|
priority: null,
|
||||||
state: null,
|
state: null,
|
||||||
|
state_group: null,
|
||||||
start_date: null,
|
start_date: null,
|
||||||
target_date: null,
|
target_date: null,
|
||||||
type: null,
|
project: null,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -185,9 +209,10 @@ export const ViewForm: React.FC<Props> = ({
|
|||||||
<div>
|
<div>
|
||||||
<FiltersList
|
<FiltersList
|
||||||
filters={filters}
|
filters={filters}
|
||||||
labels={labels}
|
labels={labelOptions}
|
||||||
members={members?.map((m) => m.member)}
|
members={memberOptions}
|
||||||
states={states}
|
states={states}
|
||||||
|
project={joinedProjects}
|
||||||
clearAllFilters={clearAllFilters}
|
clearAllFilters={clearAllFilters}
|
||||||
setFilters={(query: any) => {
|
setFilters={(query: any) => {
|
||||||
setValue("query", {
|
setValue("query", {
|
||||||
|
@ -8,6 +8,7 @@ import { mutate } from "swr";
|
|||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import viewsService from "services/views.service";
|
import viewsService from "services/views.service";
|
||||||
|
import workspaceService from "services/workspace.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
@ -15,10 +16,11 @@ import { ViewForm } from "components/views";
|
|||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IView } from "types";
|
import { ICurrentUserResponse, IView } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { VIEWS_LIST } from "constants/fetch-keys";
|
import { VIEWS_LIST, WORKSPACE_VIEWS_LIST } from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
viewType: "project" | "workspace";
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
data?: IView | null;
|
data?: IView | null;
|
||||||
preLoadedData?: Partial<IView> | null;
|
preLoadedData?: Partial<IView> | null;
|
||||||
@ -27,6 +29,7 @@ type Props = {
|
|||||||
|
|
||||||
export const CreateUpdateViewModal: React.FC<Props> = ({
|
export const CreateUpdateViewModal: React.FC<Props> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
|
viewType,
|
||||||
handleClose,
|
handleClose,
|
||||||
data,
|
data,
|
||||||
preLoadedData,
|
preLoadedData,
|
||||||
@ -46,6 +49,8 @@ export const CreateUpdateViewModal: React.FC<Props> = ({
|
|||||||
...payload,
|
...payload,
|
||||||
query_data: payload.query,
|
query_data: payload.query,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (viewType === "project") {
|
||||||
await viewsService
|
await viewsService
|
||||||
.createView(workspaceSlug as string, projectId as string, payload, user)
|
.createView(workspaceSlug as string, projectId as string, payload, user)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@ -65,6 +70,27 @@ export const CreateUpdateViewModal: React.FC<Props> = ({
|
|||||||
message: "View could not be created. Please try again.",
|
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) => {
|
const updateView = async (payload: IView) => {
|
||||||
@ -72,6 +98,7 @@ export const CreateUpdateViewModal: React.FC<Props> = ({
|
|||||||
...payload,
|
...payload,
|
||||||
query_data: payload.query,
|
query_data: payload.query,
|
||||||
};
|
};
|
||||||
|
if (viewType === "project") {
|
||||||
await viewsService
|
await viewsService
|
||||||
.updateView(workspaceSlug as string, projectId as string, data?.id ?? "", payloadData, user)
|
.updateView(workspaceSlug as string, projectId as string, data?.id ?? "", payloadData, user)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
@ -100,13 +127,50 @@ export const CreateUpdateViewModal: React.FC<Props> = ({
|
|||||||
message: "View could not be updated. Please try again.",
|
message: "View could not be updated. Please try again.",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
await workspaceService
|
||||||
|
.updateView(workspaceSlug as string, data?.id ?? "", payloadData)
|
||||||
|
.then((res) => {
|
||||||
|
mutate<IView[]>(
|
||||||
|
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) => {
|
const handleFormSubmit = async (formData: IView) => {
|
||||||
|
if (viewType === "project") {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
if (!data) await createView(formData);
|
if (!data) await createView(formData);
|
||||||
else await updateView(formData);
|
else await updateView(formData);
|
||||||
|
} else {
|
||||||
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
|
if (!data) await createView(formData);
|
||||||
|
else await updateView(formData);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -141,6 +205,7 @@ export const CreateUpdateViewModal: React.FC<Props> = ({
|
|||||||
handleClose={handleClose}
|
handleClose={handleClose}
|
||||||
status={data ? true : false}
|
status={data ? true : false}
|
||||||
data={data}
|
data={data}
|
||||||
|
viewType={viewType}
|
||||||
preLoadedData={preLoadedData}
|
preLoadedData={preLoadedData}
|
||||||
/>
|
/>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
|
@ -4,6 +4,9 @@ import { useRouter } from "next/router";
|
|||||||
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// hook
|
||||||
|
import useProjects from "hooks/use-projects";
|
||||||
|
import useWorkspaceMembers from "hooks/use-workspace-members";
|
||||||
// services
|
// services
|
||||||
import stateService from "services/state.service";
|
import stateService from "services/state.service";
|
||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
@ -18,15 +21,20 @@ import { PriorityIcon, StateGroupIcon } from "components/icons";
|
|||||||
import { getStatesList } from "helpers/state.helper";
|
import { getStatesList } from "helpers/state.helper";
|
||||||
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssueFilterOptions, IQuery } from "types";
|
import { IIssueFilterOptions, TStateGroups } from "types";
|
||||||
// fetch-keys
|
// 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
|
// constants
|
||||||
import { PRIORITIES } from "constants/project";
|
import { GROUP_CHOICES, PRIORITIES } from "constants/project";
|
||||||
import { DATE_FILTER_OPTIONS } from "constants/filters";
|
import { DATE_FILTER_OPTIONS } from "constants/filters";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
filters: Partial<IIssueFilterOptions> | IQuery;
|
filters: Partial<IIssueFilterOptions>;
|
||||||
onSelect: (option: any) => void;
|
onSelect: (option: any) => void;
|
||||||
direction?: "left" | "right";
|
direction?: "left" | "right";
|
||||||
height?: "sm" | "md" | "rg" | "lg";
|
height?: "sm" | "md" | "rg" | "lg";
|
||||||
@ -48,7 +56,7 @@ export const SelectFilters: React.FC<Props> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId, workspaceViewId } = router.query;
|
||||||
|
|
||||||
const { data: states } = useSWR(
|
const { data: states } = useSWR(
|
||||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||||
@ -58,6 +66,20 @@ export const SelectFilters: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
const statesList = getStatesList(states);
|
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(
|
const { data: members } = useSWR(
|
||||||
projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId
|
||||||
@ -65,6 +87,8 @@ export const SelectFilters: React.FC<Props> = ({
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? "");
|
||||||
|
|
||||||
const { data: issueLabels } = useSWR(
|
const { data: issueLabels } = useSWR(
|
||||||
projectId ? PROJECT_ISSUE_LABELS(projectId.toString()) : null,
|
projectId ? PROJECT_ISSUE_LABELS(projectId.toString()) : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId
|
||||||
@ -72,24 +96,15 @@ export const SelectFilters: React.FC<Props> = ({
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
const { data: workspaceLabels } = useSWR(
|
||||||
<>
|
workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null,
|
||||||
{isDateFilterModalOpen && (
|
workspaceSlug ? () => issuesService.getWorkspaceLabels(workspaceSlug.toString()) : null
|
||||||
<DateFilterModal
|
);
|
||||||
title={dateFilterType.title}
|
|
||||||
field={dateFilterType.type}
|
const { projects: allProjects } = useProjects();
|
||||||
filters={filters as IIssueFilterOptions}
|
const joinedProjects = allProjects?.filter((p) => p.is_member);
|
||||||
handleClose={() => setIsDateFilterModalOpen(false)}
|
|
||||||
isOpen={isDateFilterModalOpen}
|
const projectFilterOption = [
|
||||||
onSelect={onSelect}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<MultiLevelDropdown
|
|
||||||
label="Filters"
|
|
||||||
onSelect={onSelect}
|
|
||||||
direction={direction}
|
|
||||||
height={height}
|
|
||||||
options={[
|
|
||||||
{
|
{
|
||||||
id: "priority",
|
id: "priority",
|
||||||
label: "Priority",
|
label: "Priority",
|
||||||
@ -267,7 +282,245 @@ export const SelectFilters: React.FC<Props> = ({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]}
|
];
|
||||||
|
|
||||||
|
const workspaceFilterOption = [
|
||||||
|
{
|
||||||
|
id: "project",
|
||||||
|
label: "Project",
|
||||||
|
value: joinedProjects,
|
||||||
|
hasChildren: true,
|
||||||
|
children: joinedProjects?.map((project) => ({
|
||||||
|
id: project.id,
|
||||||
|
label: <div className="flex items-center gap-2">{project.name}</div>,
|
||||||
|
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: (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<StateGroupIcon stateGroup={key as TStateGroups} />
|
||||||
|
{GROUP_CHOICES[key as keyof typeof GROUP_CHOICES]}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
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: (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
className="h-2 w-2 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: label.color && label.color !== "" ? label.color : "#000000",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{label.name}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
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: (
|
||||||
|
<div className="flex items-center gap-2 capitalize">
|
||||||
|
<PriorityIcon priority={priority} />
|
||||||
|
{priority ?? "None"}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
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: (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Avatar user={member.member} />
|
||||||
|
{member.member.display_name}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
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: (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Avatar user={member.member} />
|
||||||
|
{member.member.display_name}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
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: (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Avatar user={member.member} />
|
||||||
|
{member.member.display_name}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
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: (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsDateFilterModalOpen(true);
|
||||||
|
setDateFilterType({
|
||||||
|
title: "Start date",
|
||||||
|
type: "start_date",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="w-full rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
|
||||||
|
>
|
||||||
|
Custom
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsDateFilterModalOpen(true);
|
||||||
|
setDateFilterType({
|
||||||
|
title: "Due date",
|
||||||
|
type: "target_date",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="w-full rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
|
||||||
|
>
|
||||||
|
Custom
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const filterOption = isWorkspaceView ? workspaceFilterOption : projectFilterOption;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isDateFilterModalOpen && (
|
||||||
|
<DateFilterModal
|
||||||
|
title={dateFilterType.title}
|
||||||
|
field={dateFilterType.type}
|
||||||
|
filters={filters as IIssueFilterOptions}
|
||||||
|
handleClose={() => setIsDateFilterModalOpen(false)}
|
||||||
|
isOpen={isDateFilterModalOpen}
|
||||||
|
onSelect={onSelect}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<MultiLevelDropdown
|
||||||
|
label="Filters"
|
||||||
|
onSelect={onSelect}
|
||||||
|
direction={direction}
|
||||||
|
height={height}
|
||||||
|
options={filterOption}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -5,9 +5,9 @@ import { useRouter } from "next/router";
|
|||||||
|
|
||||||
// icons
|
// icons
|
||||||
import { TrashIcon, StarIcon, PencilIcon } from "@heroicons/react/24/outline";
|
import { TrashIcon, StarIcon, PencilIcon } from "@heroicons/react/24/outline";
|
||||||
import { StackedLayersIcon } from "components/icons";
|
import { PhotoFilterOutlined } from "@mui/icons-material";
|
||||||
//components
|
//components
|
||||||
import { CustomMenu, Tooltip } from "components/ui";
|
import { CustomMenu } from "components/ui";
|
||||||
// services
|
// services
|
||||||
import viewsService from "services/views.service";
|
import viewsService from "services/views.service";
|
||||||
// types
|
// types
|
||||||
@ -18,15 +18,20 @@ import { VIEWS_LIST } from "constants/fetch-keys";
|
|||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// helpers
|
// helpers
|
||||||
import { truncateText } from "helpers/string.helper";
|
import { truncateText } from "helpers/string.helper";
|
||||||
import { renderShortDateWithYearFormat, render24HourFormatTime } from "helpers/date-time.helper";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
view: IView;
|
view: IView;
|
||||||
|
viewType: "project" | "workspace";
|
||||||
handleEditView: () => void;
|
handleEditView: () => void;
|
||||||
handleDeleteView: () => void;
|
handleDeleteView: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SingleViewItem: React.FC<Props> = ({ view, handleEditView, handleDeleteView }) => {
|
export const SingleViewItem: React.FC<Props> = ({
|
||||||
|
view,
|
||||||
|
viewType,
|
||||||
|
handleEditView,
|
||||||
|
handleDeleteView,
|
||||||
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
@ -82,19 +87,34 @@ export const SingleViewItem: React.FC<Props> = ({ view, handleEditView, handleDe
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const viewRedirectionUrl =
|
||||||
|
viewType === "project"
|
||||||
|
? `/${workspaceSlug}/projects/${projectId}/views/${view.id}`
|
||||||
|
: `/${workspaceSlug}/workspace-views/${view.id}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="first:rounded-t-[10px] last:rounded-b-[10px] hover:bg-custom-background-90">
|
<div className="group hover:bg-custom-background-90 border-b border-custom-border-200">
|
||||||
<Link href={`/${workspaceSlug}/projects/${projectId}/views/${view.id}`}>
|
<Link href={viewRedirectionUrl}>
|
||||||
<a>
|
<a className="flex items-center justify-between relative rounded px-5 py-4 w-full">
|
||||||
<div className="relative rounded p-4">
|
<div className="flex items-center justify-between w-full">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<div
|
||||||
<StackedLayersIcon height={18} width={18} />
|
className={`flex items-center justify-center h-10 w-10 rounded bg-custom-background-90 group-hover:bg-custom-background-100`}
|
||||||
<p className="mr-2 truncate text-sm">{truncateText(view.name, 75)}</p>
|
>
|
||||||
|
<PhotoFilterOutlined className="!text-base !leading-6" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<p className="truncate text-sm leading-4 font-medium">
|
||||||
|
{truncateText(view.name, 75)}
|
||||||
|
</p>
|
||||||
|
{view?.description && (
|
||||||
|
<p className="text-xs text-custom-text-200">{view.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-2 flex flex-shrink-0">
|
<div className="ml-2 flex flex-shrink-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-4">
|
||||||
<p className="rounded-full bg-custom-background-80 py-0.5 px-2 text-xs text-custom-text-200">
|
<p className="rounded bg-custom-background-80 py-1 px-2 text-xs text-custom-text-200 opacity-0 group-hover:opacity-100">
|
||||||
{Object.keys(view.query_data)
|
{Object.keys(view.query_data)
|
||||||
.map((key: string) =>
|
.map((key: string) =>
|
||||||
view.query_data[key as keyof typeof view.query_data] !== null
|
view.query_data[key as keyof typeof view.query_data] !== null
|
||||||
@ -104,16 +124,9 @@ export const SingleViewItem: React.FC<Props> = ({ view, handleEditView, handleDe
|
|||||||
.reduce((curr, prev) => curr + prev, 0)}{" "}
|
.reduce((curr, prev) => curr + prev, 0)}{" "}
|
||||||
filters
|
filters
|
||||||
</p>
|
</p>
|
||||||
<Tooltip
|
|
||||||
tooltipContent={`Last updated at ${render24HourFormatTime(
|
{viewType === "project" ? (
|
||||||
view.updated_at
|
view.is_favorite ? (
|
||||||
)} ${renderShortDateWithYearFormat(view.updated_at)}`}
|
|
||||||
>
|
|
||||||
<p className="text-sm text-custom-text-200">
|
|
||||||
{render24HourFormatTime(view.updated_at)}
|
|
||||||
</p>
|
|
||||||
</Tooltip>
|
|
||||||
{view.is_favorite ? (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@ -135,8 +148,9 @@ export const SingleViewItem: React.FC<Props> = ({ view, handleEditView, handleDe
|
|||||||
>
|
>
|
||||||
<StarIcon className="h-4 w-4 " color="rgb(var(--color-text-200))" />
|
<StarIcon className="h-4 w-4 " color="rgb(var(--color-text-200))" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)
|
||||||
<CustomMenu width="auto" verticalEllipsis>
|
) : null}
|
||||||
|
<CustomMenu width="auto" ellipsis>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={(e: any) => {
|
onClick={(e: any) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -165,12 +179,6 @@ export const SingleViewItem: React.FC<Props> = ({ view, handleEditView, handleDe
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{view?.description && (
|
|
||||||
<p className="px-[27px] text-sm font-normal leading-5 text-custom-text-200">
|
|
||||||
{view.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -34,8 +34,8 @@ const workspaceLinks = (workspaceSlug: string) => [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Icon: TaskAltOutlined,
|
Icon: TaskAltOutlined,
|
||||||
name: "My Issues",
|
name: "Issues",
|
||||||
href: `/${workspaceSlug}/me/my-issues`,
|
href: `/${workspaceSlug}/workspace-views/all-issues`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
92
web/components/workspace/views/workpace-view-navigation.tsx
Normal file
92
web/components/workspace/views/workpace-view-navigation.tsx
Normal file
@ -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<Props> = ({ 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 (
|
||||||
|
<div className="group flex items-center overflow-x-scroll">
|
||||||
|
{tabsList.map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.key}
|
||||||
|
type="button"
|
||||||
|
onClick={tab.onClick}
|
||||||
|
className={`border-b-2 min-w-[96px] p-4 text-sm font-medium outline-none whitespace-nowrap ${
|
||||||
|
tab.selected
|
||||||
|
? "border-custom-primary-100 text-custom-primary-100"
|
||||||
|
: "border-transparent hover:border-custom-primary-100 hover:text-custom-primary-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{workspaceViews &&
|
||||||
|
workspaceViews.length > 0 &&
|
||||||
|
workspaceViews?.map((view) => (
|
||||||
|
<button
|
||||||
|
className={`border-b-2 min-w-[96px] p-4 text-sm font-medium outline-none whitespace-nowrap ${
|
||||||
|
view.id === workspaceViewId
|
||||||
|
? "border-custom-primary-100 text-custom-primary-100"
|
||||||
|
: "border-transparent hover:border-custom-primary-100 hover:text-custom-primary-100"
|
||||||
|
}`}
|
||||||
|
onClick={() => router.push(`/${workspaceSlug}/workspace-views/${view.id}`)}
|
||||||
|
>
|
||||||
|
{view.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button type="button" className="min-w-[96px] " onClick={handleAddView}>
|
||||||
|
<PlusIcon className="h-4 w-4 text-custom-primary-200 hover:text-current" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -4,6 +4,7 @@ import { IAnalyticsParams, IJiraMetadata, INotificationParams } from "types";
|
|||||||
const paramsToKey = (params: any) => {
|
const paramsToKey = (params: any) => {
|
||||||
const {
|
const {
|
||||||
state,
|
state,
|
||||||
|
state_group,
|
||||||
priority,
|
priority,
|
||||||
assignees,
|
assignees,
|
||||||
created_by,
|
created_by,
|
||||||
@ -12,9 +13,12 @@ const paramsToKey = (params: any) => {
|
|||||||
target_date,
|
target_date,
|
||||||
sub_issue,
|
sub_issue,
|
||||||
start_target_date,
|
start_target_date,
|
||||||
|
project,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
|
let projectKey = project ? project.split(",") : [];
|
||||||
let stateKey = state ? state.split(",") : [];
|
let stateKey = state ? state.split(",") : [];
|
||||||
|
let stateGroupKey = state_group ? state_group.split(",") : [];
|
||||||
let priorityKey = priority ? priority.split(",") : [];
|
let priorityKey = priority ? priority.split(",") : [];
|
||||||
let assigneesKey = assignees ? assignees.split(",") : [];
|
let assigneesKey = assignees ? assignees.split(",") : [];
|
||||||
let createdByKey = created_by ? created_by.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";
|
const orderBy = params.order_by ? params.order_by.toUpperCase() : "NULL";
|
||||||
|
|
||||||
// sorting each keys in ascending order
|
// sorting each keys in ascending order
|
||||||
|
projectKey = projectKey.sort().join("_");
|
||||||
stateKey = stateKey.sort().join("_");
|
stateKey = stateKey.sort().join("_");
|
||||||
|
stateGroupKey = stateGroupKey.sort().join("_");
|
||||||
priorityKey = priorityKey.sort().join("_");
|
priorityKey = priorityKey.sort().join("_");
|
||||||
assigneesKey = assigneesKey.sort().join("_");
|
assigneesKey = assigneesKey.sort().join("_");
|
||||||
createdByKey = createdByKey.sort().join("_");
|
createdByKey = createdByKey.sort().join("_");
|
||||||
labelsKey = labelsKey.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) => {
|
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}`;
|
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) =>
|
export const PROJECT_ISSUES_DETAILS = (issueId: string) =>
|
||||||
`PROJECT_ISSUES_DETAILS_${issueId.toUpperCase()}`;
|
`PROJECT_ISSUES_DETAILS_${issueId.toUpperCase()}`;
|
||||||
export const PROJECT_ISSUES_PROPERTIES = (projectId: string) =>
|
export const PROJECT_ISSUES_PROPERTIES = (projectId: string) =>
|
||||||
|
@ -21,6 +21,8 @@ export const GROUP_CHOICES = {
|
|||||||
cancelled: "Cancelled",
|
cancelled: "Cancelled",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const STATE_GROUP = ["Backlog", "Unstarted", "Started", "Completed", "Cancelled"];
|
||||||
|
|
||||||
export const PRIORITIES: TIssuePriorities[] = ["urgent", "high", "medium", "low", "none"];
|
export const PRIORITIES: TIssuePriorities[] = ["urgent", "high", "medium", "low", "none"];
|
||||||
|
|
||||||
export const MONTHS = [
|
export const MONTHS = [
|
||||||
|
@ -11,9 +11,9 @@ import { ISubIssueResponse } from "types";
|
|||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { SUB_ISSUES } from "constants/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 router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const shouldFetch = workspaceSlug && projectId && issueId && isExpanded;
|
const shouldFetch = workspaceSlug && projectId && issueId && isExpanded;
|
||||||
|
|
||||||
|
113
web/hooks/use-worskpace-issue-filter.tsx
Normal file
113
web/hooks/use-worskpace-issue-filter.tsx
Normal file
@ -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<IIssueFilterOptions>) => {
|
||||||
|
if (!workspaceSlug || !workspaceViewId || !workspaceViewDetails) return;
|
||||||
|
|
||||||
|
const oldData = { ...workspaceViewDetails };
|
||||||
|
|
||||||
|
mutate<IView>(
|
||||||
|
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<IIssueFilterOptions>) => {
|
||||||
|
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;
|
@ -88,12 +88,14 @@ const ProjectViews: NextPage = () => {
|
|||||||
>
|
>
|
||||||
<CreateUpdateViewModal
|
<CreateUpdateViewModal
|
||||||
isOpen={createUpdateViewModal}
|
isOpen={createUpdateViewModal}
|
||||||
|
viewType="project"
|
||||||
handleClose={() => setCreateUpdateViewModal(false)}
|
handleClose={() => setCreateUpdateViewModal(false)}
|
||||||
data={selectedViewToUpdate}
|
data={selectedViewToUpdate}
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
<DeleteViewModal
|
<DeleteViewModal
|
||||||
isOpen={deleteViewModal}
|
isOpen={deleteViewModal}
|
||||||
|
viewType="project"
|
||||||
data={selectedViewToDelete}
|
data={selectedViewToDelete}
|
||||||
setIsOpen={setDeleteViewModal}
|
setIsOpen={setDeleteViewModal}
|
||||||
user={user}
|
user={user}
|
||||||
@ -107,6 +109,7 @@ const ProjectViews: NextPage = () => {
|
|||||||
<SingleViewItem
|
<SingleViewItem
|
||||||
key={view.id}
|
key={view.id}
|
||||||
view={view}
|
view={view}
|
||||||
|
viewType="project"
|
||||||
handleEditView={() => handleEditView(view)}
|
handleEditView={() => handleEditView(view)}
|
||||||
handleDeleteView={() => handleDeleteView(view)}
|
handleDeleteView={() => handleDeleteView(view)}
|
||||||
/>
|
/>
|
||||||
|
322
web/pages/[workspaceSlug]/workspace-views/[workspaceViewId].tsx
Normal file
322
web/pages/[workspaceSlug]/workspace-views/[workspaceViewId].tsx
Normal file
@ -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<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 { 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<IView[]>(
|
||||||
|
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 (
|
||||||
|
<WorkspaceAuthorizationLayout
|
||||||
|
breadcrumbs={
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" />
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
right={
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<WorkspaceIssuesViewOptions />
|
||||||
|
<PrimaryButton
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() => {
|
||||||
|
const e = new KeyboardEvent("keydown", { key: "c" });
|
||||||
|
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`),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="h-full w-full flex flex-col">
|
||||||
|
{areFiltersApplied && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0">
|
||||||
|
<FiltersList
|
||||||
|
filters={filters}
|
||||||
|
setFilters={(updatedFilter) => 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<PrimaryButton
|
||||||
|
onClick={() => {
|
||||||
|
if (workspaceViewId) {
|
||||||
|
updateView(filters);
|
||||||
|
} else
|
||||||
|
setCreateViewModal({
|
||||||
|
query: filters,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2 text-sm"
|
||||||
|
>
|
||||||
|
{!workspaceViewId && <PlusIcon className="h-4 w-4" />}
|
||||||
|
{workspaceViewId ? "Update" : "Save"} view
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
{<div className="mt-3 border-t border-custom-border-200" />}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<SpreadsheetView
|
||||||
|
spreadsheetIssues={viewIssues}
|
||||||
|
mutateIssues={mutateIssues}
|
||||||
|
handleIssueAction={handleIssueAction}
|
||||||
|
disableUserActions={isNotAllowed ?? false}
|
||||||
|
user={user}
|
||||||
|
userAuth={memberRole}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</WorkspaceAuthorizationLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WorkspaceView;
|
283
web/pages/[workspaceSlug]/workspace-views/all-issues.tsx
Normal file
283
web/pages/[workspaceSlug]/workspace-views/all-issues.tsx
Normal file
@ -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<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 { 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 (
|
||||||
|
<WorkspaceAuthorizationLayout
|
||||||
|
breadcrumbs={
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" />
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
right={
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<MyIssuesViewOptions />
|
||||||
|
<PrimaryButton
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() => {
|
||||||
|
const e = new KeyboardEvent("keydown", { key: "c" });
|
||||||
|
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`),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="h-full w-full flex flex-col">
|
||||||
|
{areFiltersApplied && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0">
|
||||||
|
<FiltersList
|
||||||
|
filters={filters}
|
||||||
|
setFilters={(updatedFilter) => 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<PrimaryButton
|
||||||
|
onClick={() => {
|
||||||
|
setCreateViewModal({
|
||||||
|
query: filters,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2 text-sm"
|
||||||
|
>
|
||||||
|
{!workspaceViewId && <PlusIcon className="h-4 w-4" />}
|
||||||
|
{workspaceViewId ? "Update" : "Save"} view
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
{<div className="mt-3 border-t border-custom-border-200" />}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<SpreadsheetView
|
||||||
|
spreadsheetIssues={viewIssues}
|
||||||
|
mutateIssues={mutateIssues}
|
||||||
|
handleIssueAction={handleIssueAction}
|
||||||
|
disableUserActions={false}
|
||||||
|
user={user}
|
||||||
|
userAuth={memberRole}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</WorkspaceAuthorizationLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WorkspaceViewAllIssue;
|
205
web/pages/[workspaceSlug]/workspace-views/assigned.tsx
Normal file
205
web/pages/[workspaceSlug]/workspace-views/assigned.tsx
Normal file
@ -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<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 { 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 (
|
||||||
|
<WorkspaceAuthorizationLayout
|
||||||
|
breadcrumbs={
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" />
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
right={
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<WorkspaceIssuesViewOptions />
|
||||||
|
<PrimaryButton
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() => {
|
||||||
|
const e = new KeyboardEvent("keydown", { key: "c" });
|
||||||
|
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`),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="h-full w-full flex flex-col">
|
||||||
|
<SpreadsheetView
|
||||||
|
spreadsheetIssues={viewIssues}
|
||||||
|
mutateIssues={mutateIssues}
|
||||||
|
handleIssueAction={handleIssueAction}
|
||||||
|
disableUserActions={false}
|
||||||
|
user={user}
|
||||||
|
userAuth={memberRole}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</WorkspaceAuthorizationLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WorkspaceViewAssignedIssue;
|
205
web/pages/[workspaceSlug]/workspace-views/created.tsx
Normal file
205
web/pages/[workspaceSlug]/workspace-views/created.tsx
Normal file
@ -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<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 { 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 (
|
||||||
|
<WorkspaceAuthorizationLayout
|
||||||
|
breadcrumbs={
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" />
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
right={
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<WorkspaceIssuesViewOptions />
|
||||||
|
<PrimaryButton
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() => {
|
||||||
|
const e = new KeyboardEvent("keydown", { key: "c" });
|
||||||
|
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`),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="h-full w-full flex flex-col">
|
||||||
|
<SpreadsheetView
|
||||||
|
spreadsheetIssues={viewIssues}
|
||||||
|
mutateIssues={mutateIssues}
|
||||||
|
handleIssueAction={handleIssueAction}
|
||||||
|
disableUserActions={false}
|
||||||
|
user={user}
|
||||||
|
userAuth={memberRole}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</WorkspaceAuthorizationLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WorkspaceViewCreatedIssue;
|
213
web/pages/[workspaceSlug]/workspace-views/index.tsx
Normal file
213
web/pages/[workspaceSlug]/workspace-views/index.tsx
Normal file
@ -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<IView | null>(null);
|
||||||
|
|
||||||
|
const [deleteViewModal, setDeleteViewModal] = useState(false);
|
||||||
|
const [selectedViewToDelete, setSelectedViewToDelete] = useState<IView | null>(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 (
|
||||||
|
<WorkspaceAuthorizationLayout
|
||||||
|
breadcrumbs={
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<span className="text-sm font-medium">Workspace Views</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
right={
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<WorkspaceIssuesViewOptions />
|
||||||
|
|
||||||
|
<PrimaryButton
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() => setCreateUpdateViewModal(true)}
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-4 w-4" />
|
||||||
|
New View
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CreateUpdateViewModal
|
||||||
|
isOpen={createUpdateViewModal}
|
||||||
|
handleClose={() => {
|
||||||
|
setCreateUpdateViewModal(false);
|
||||||
|
setSelectedViewToUpdate(null);
|
||||||
|
}}
|
||||||
|
data={selectedViewToUpdate}
|
||||||
|
viewType="workspace"
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
<DeleteViewModal
|
||||||
|
isOpen={deleteViewModal}
|
||||||
|
data={selectedViewToDelete}
|
||||||
|
setIsOpen={setDeleteViewModal}
|
||||||
|
viewType="workspace"
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||||
|
<div className="flex items-center gap-2.5 w-full px-5 py-3 border-b border-custom-border-200">
|
||||||
|
<MagnifyingGlassIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
|
<Input
|
||||||
|
className="w-full bg-transparent text-xs leading-5 text-custom-text-200 placeholder:text-custom-text-400 !p-0 focus:outline-none"
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
placeholder="Search"
|
||||||
|
mode="trueTransparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{filteredDefaultOptions &&
|
||||||
|
filteredDefaultOptions.length > 0 &&
|
||||||
|
filteredDefaultOptions.map((option) => (
|
||||||
|
<div className="group hover:bg-custom-background-90 border-b border-custom-border-200">
|
||||||
|
<Link href={option.href}>
|
||||||
|
<a className="flex items-center justify-between relative rounded px-5 py-4 w-full">
|
||||||
|
<div className="flex items-center justify-between w-full">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div
|
||||||
|
className={`flex items-center justify-center h-10 w-10 rounded bg-custom-background-90 group-hover:bg-custom-background-100`}
|
||||||
|
>
|
||||||
|
<PhotoFilterOutlined className="!text-base !leading-6" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<p className="truncate text-sm leading-4 font-medium">
|
||||||
|
{truncateText(option.label, 75)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{filteredOptions ? (
|
||||||
|
filteredOptions.length > 0 ? (
|
||||||
|
<div>
|
||||||
|
{filteredOptions.map((view) => (
|
||||||
|
<SingleViewItem
|
||||||
|
key={view.id}
|
||||||
|
view={view}
|
||||||
|
viewType="workspace"
|
||||||
|
handleEditView={() => handleEditView(view)}
|
||||||
|
handleDeleteView={() => handleDeleteView(view)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<EmptyState
|
||||||
|
title="Get focused with views"
|
||||||
|
description="Views aid in saving your issues by applying various filters and grouping options."
|
||||||
|
image={emptyView}
|
||||||
|
primaryButton={{
|
||||||
|
icon: <PlusIcon className="h-4 w-4" />,
|
||||||
|
text: "New View",
|
||||||
|
onClick: () => setCreateUpdateViewModal(true),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Loader className="space-y-1.5">
|
||||||
|
<Loader.Item height="72px" />
|
||||||
|
<Loader.Item height="72px" />
|
||||||
|
<Loader.Item height="72px" />
|
||||||
|
<Loader.Item height="72px" />
|
||||||
|
<Loader.Item height="72px" />
|
||||||
|
</Loader>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</WorkspaceAuthorizationLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WorkspaceViews;
|
205
web/pages/[workspaceSlug]/workspace-views/subscribed.tsx
Normal file
205
web/pages/[workspaceSlug]/workspace-views/subscribed.tsx
Normal file
@ -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<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 { 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 (
|
||||||
|
<WorkspaceAuthorizationLayout
|
||||||
|
breadcrumbs={
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" />
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
right={
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<WorkspaceIssuesViewOptions />
|
||||||
|
<PrimaryButton
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() => {
|
||||||
|
const e = new KeyboardEvent("keydown", { key: "c" });
|
||||||
|
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`),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="h-full w-full flex flex-col">
|
||||||
|
<SpreadsheetView
|
||||||
|
spreadsheetIssues={viewIssues}
|
||||||
|
mutateIssues={mutateIssues}
|
||||||
|
handleIssueAction={handleIssueAction}
|
||||||
|
disableUserActions={false}
|
||||||
|
user={user}
|
||||||
|
userAuth={memberRole}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</WorkspaceAuthorizationLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WorkspaceViewSubscribedIssue;
|
@ -14,6 +14,8 @@ import {
|
|||||||
ICurrentUserResponse,
|
ICurrentUserResponse,
|
||||||
IWorkspaceBulkInviteFormData,
|
IWorkspaceBulkInviteFormData,
|
||||||
IWorkspaceViewProps,
|
IWorkspaceViewProps,
|
||||||
|
IView,
|
||||||
|
IIssueFilterOptions,
|
||||||
} from "types";
|
} from "types";
|
||||||
|
|
||||||
class WorkspaceService extends APIService {
|
class WorkspaceService extends APIService {
|
||||||
@ -261,6 +263,56 @@ class WorkspaceService extends APIService {
|
|||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createView(workspaceSlug: string, data: IView): Promise<any> {
|
||||||
|
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<IView>): Promise<any> {
|
||||||
|
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<any> {
|
||||||
|
return this.delete(`/api/workspaces/${workspaceSlug}/views/${viewId}/`)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllViews(workspaceSlug: string): Promise<IView[]> {
|
||||||
|
return this.get(`/api/workspaces/${workspaceSlug}/views/`)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getViewDetails(workspaceSlug: string, viewId: string): Promise<IView> {
|
||||||
|
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<any> {
|
||||||
|
return this.get(`/api/workspaces/${workspaceSlug}/issues/`, {
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new WorkspaceService();
|
export default new WorkspaceService();
|
||||||
|
3
web/types/view-props.d.ts
vendored
3
web/types/view-props.d.ts
vendored
@ -35,9 +35,10 @@ export interface IIssueFilterOptions {
|
|||||||
priority?: string[] | null;
|
priority?: string[] | null;
|
||||||
start_date?: string[] | null;
|
start_date?: string[] | null;
|
||||||
state?: string[] | null;
|
state?: string[] | null;
|
||||||
state_group?: TStateGroups[] | null;
|
state_group?: string[] | null;
|
||||||
subscriber?: string[] | null;
|
subscriber?: string[] | null;
|
||||||
target_date?: string[] | null;
|
target_date?: string[] | null;
|
||||||
|
project?: string[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IIssueDisplayFilterOptions {
|
export interface IIssueDisplayFilterOptions {
|
||||||
|
12
web/types/views.d.ts
vendored
12
web/types/views.d.ts
vendored
@ -1,3 +1,5 @@
|
|||||||
|
import { IIssueFilterOptions } from "./view-props";
|
||||||
|
|
||||||
export interface IView {
|
export interface IView {
|
||||||
id: string;
|
id: string;
|
||||||
access: string;
|
access: string;
|
||||||
@ -8,10 +10,15 @@ export interface IView {
|
|||||||
updated_by: string;
|
updated_by: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
query: IQuery;
|
query: IIssueFilterOptions;
|
||||||
query_data: IQuery;
|
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 {
|
||||||
@ -23,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;
|
||||||
|
project: string[] | null;
|
||||||
}
|
}
|
||||||
|
11
web/types/workspace.d.ts
vendored
11
web/types/workspace.d.ts
vendored
@ -1,13 +1,4 @@
|
|||||||
import type {
|
import type { IProjectMember, IUser, IUserMemberLite, IWorkspaceViewProps } from "types";
|
||||||
IIssueFilterOptions,
|
|
||||||
IProjectMember,
|
|
||||||
IUser,
|
|
||||||
IUserMemberLite,
|
|
||||||
IWorkspaceViewProps,
|
|
||||||
TIssueGroupByOptions,
|
|
||||||
TIssueOrderByOptions,
|
|
||||||
TIssueViewOptions,
|
|
||||||
} from "types";
|
|
||||||
|
|
||||||
export interface IWorkspace {
|
export interface IWorkspace {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
|
Loading…
Reference in New Issue
Block a user