forked from github/plane
chore: workspace view display filters and properties , code refactor (#2295)
* chore: spreadsheet view context * chore: spreadsheet context provider * chore: spreadsheet view context * chore: display filters and properties added in workspace view and code refactor * fix: build error fix * chore: set sub-issue display option to false for global views --------- Co-authored-by: gurusainath <gurusainath007@gmail.com>
This commit is contained in:
parent
4503810aeb
commit
a048e513b7
@ -161,7 +161,6 @@ 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,14 +10,7 @@ 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 {
|
import { IIssueFilterOptions, IIssueLabels, IState, IUserLite, TStateGroups } from "types";
|
||||||
IIssueFilterOptions,
|
|
||||||
IIssueLabels,
|
|
||||||
IProject,
|
|
||||||
IState,
|
|
||||||
IUserLite,
|
|
||||||
TStateGroups,
|
|
||||||
} from "types";
|
|
||||||
// constants
|
// constants
|
||||||
import { STATE_GROUP_COLORS } from "constants/state";
|
import { STATE_GROUP_COLORS } from "constants/state";
|
||||||
|
|
||||||
@ -27,9 +20,7 @@ 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> = ({
|
||||||
@ -39,7 +30,6 @@ export const FiltersList: React.FC<Props> = ({
|
|||||||
labels,
|
labels,
|
||||||
members,
|
members,
|
||||||
states,
|
states,
|
||||||
project,
|
|
||||||
}) => {
|
}) => {
|
||||||
if (!filters) return <></>;
|
if (!filters) return <></>;
|
||||||
|
|
||||||
@ -165,29 +155,6 @@ 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
|
||||||
@ -333,30 +300,6 @@ 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"
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export * from "./date-filter-modal";
|
export * from "./date-filter-modal";
|
||||||
export * from "./date-filter-select";
|
export * from "./date-filter-select";
|
||||||
export * from "./filters-list";
|
export * from "./filters-list";
|
||||||
|
export * from "./workspace-filters-list";
|
||||||
export * from "./issues-view-filter";
|
export * from "./issues-view-filter";
|
||||||
|
366
web/components/core/filters/workspace-filters-list.tsx
Normal file
366
web/components/core/filters/workspace-filters-list.tsx
Normal file
@ -0,0 +1,366 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// icons
|
||||||
|
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { PriorityIcon, StateGroupIcon } from "components/icons";
|
||||||
|
// ui
|
||||||
|
import { Avatar } from "components/ui";
|
||||||
|
// helpers
|
||||||
|
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||||
|
// helpers
|
||||||
|
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
|
// types
|
||||||
|
import {
|
||||||
|
IIssueLabels,
|
||||||
|
IProject,
|
||||||
|
IUserLite,
|
||||||
|
IWorkspaceIssueFilterOptions,
|
||||||
|
TStateGroups,
|
||||||
|
} from "types";
|
||||||
|
// constants
|
||||||
|
import { STATE_GROUP_COLORS } from "constants/state";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
filters: Partial<IWorkspaceIssueFilterOptions>;
|
||||||
|
setFilters: (updatedFilter: Partial<IWorkspaceIssueFilterOptions>) => void;
|
||||||
|
clearAllFilters: (...args: any) => void;
|
||||||
|
labels: IIssueLabels[] | undefined;
|
||||||
|
members: IUserLite[] | undefined;
|
||||||
|
stateGroup: string[] | undefined;
|
||||||
|
project?: IProject[] | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorkspaceFiltersList: React.FC<Props> = ({
|
||||||
|
filters,
|
||||||
|
setFilters,
|
||||||
|
clearAllFilters,
|
||||||
|
labels,
|
||||||
|
members,
|
||||||
|
stateGroup,
|
||||||
|
project,
|
||||||
|
}) => {
|
||||||
|
if (!filters) return <></>;
|
||||||
|
|
||||||
|
const nullFilters = Object.keys(filters).filter(
|
||||||
|
(key) => filters[key as keyof IWorkspaceIssueFilterOptions] === null
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 flex-wrap items-center gap-2 text-xs">
|
||||||
|
{Object.keys(filters).map((filterKey) => {
|
||||||
|
const key = filterKey as keyof typeof filters;
|
||||||
|
|
||||||
|
if (filters[key] === null || (filters[key]?.length ?? 0) <= 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
className="flex items-center gap-x-2 rounded-full border border-custom-border-200 bg-custom-background-80 px-2 py-1"
|
||||||
|
>
|
||||||
|
<span className="capitalize text-custom-text-200">
|
||||||
|
{key === "target_date" ? "Due Date" : replaceUnderscoreIfSnakeCase(key)}:
|
||||||
|
</span>
|
||||||
|
{filters[key] === null || (filters[key]?.length ?? 0) <= 0 ? (
|
||||||
|
<span className="inline-flex items-center px-2 py-0.5 font-medium">None</span>
|
||||||
|
) : Array.isArray(filters[key]) ? (
|
||||||
|
<div className="space-x-2">
|
||||||
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
|
{key === "state_group"
|
||||||
|
? filters.state_group?.map((stateGroup) => {
|
||||||
|
const group = stateGroup as TStateGroups;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
key={group}
|
||||||
|
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize"
|
||||||
|
style={{
|
||||||
|
color: STATE_GROUP_COLORS[group],
|
||||||
|
backgroundColor: `${STATE_GROUP_COLORS[group]}20`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<StateGroupIcon stateGroup={group} color={undefined} />
|
||||||
|
</span>
|
||||||
|
<span>{group}</span>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
state_group: filters.state_group?.filter((g) => g !== group),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: key === "priority"
|
||||||
|
? filters.priority?.map((priority: any) => (
|
||||||
|
<p
|
||||||
|
key={priority}
|
||||||
|
className={`inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize ${
|
||||||
|
priority === "urgent"
|
||||||
|
? "bg-red-500/20 text-red-500"
|
||||||
|
: priority === "high"
|
||||||
|
? "bg-orange-500/20 text-orange-500"
|
||||||
|
: priority === "medium"
|
||||||
|
? "bg-yellow-500/20 text-yellow-500"
|
||||||
|
: priority === "low"
|
||||||
|
? "bg-green-500/20 text-green-500"
|
||||||
|
: "bg-custom-background-90 text-custom-text-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<PriorityIcon priority={priority} />
|
||||||
|
</span>
|
||||||
|
<span>{priority === "null" ? "None" : priority}</span>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
priority: filters.priority?.filter((p: any) => p !== priority),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
))
|
||||||
|
: key === "assignees"
|
||||||
|
? filters.assignees?.map((memberId: string) => {
|
||||||
|
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 (
|
||||||
|
<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 === "created_by"
|
||||||
|
? filters.created_by?.map((memberId: string) => {
|
||||||
|
const member = members?.find((m) => m.id === memberId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${memberId}-${key}`}
|
||||||
|
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1 capitalize"
|
||||||
|
>
|
||||||
|
<Avatar user={member} />
|
||||||
|
<span>{member?.display_name}</span>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
created_by: filters.created_by?.filter(
|
||||||
|
(p: any) => p !== memberId
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: key === "labels"
|
||||||
|
? filters.labels?.map((labelId: string) => {
|
||||||
|
const label = labels?.find((l) => l.id === labelId);
|
||||||
|
|
||||||
|
if (!label) return null;
|
||||||
|
const color = label.color !== "" ? label.color : "#0f172a";
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5"
|
||||||
|
style={{
|
||||||
|
color: color,
|
||||||
|
backgroundColor: `${color}20`, // add 20% opacity
|
||||||
|
}}
|
||||||
|
key={labelId}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="h-1.5 w-1.5 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span>{label.name}</span>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
labels: filters.labels?.filter((l: any) => l !== labelId),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon
|
||||||
|
className="h-3 w-3"
|
||||||
|
style={{
|
||||||
|
color: color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: key === "start_date"
|
||||||
|
? filters.start_date?.map((date: string) => {
|
||||||
|
if (filters.start_date && filters.start_date.length <= 0) return null;
|
||||||
|
|
||||||
|
const splitDate = date.split(";");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={date}
|
||||||
|
className="inline-flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-100 px-1 py-0.5"
|
||||||
|
>
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full" />
|
||||||
|
<span className="capitalize">
|
||||||
|
{splitDate[1]} {renderShortDateWithYearFormat(splitDate[0])}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
start_date: filters.start_date?.filter((d: any) => d !== date),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: key === "target_date"
|
||||||
|
? filters.target_date?.map((date: string) => {
|
||||||
|
if (filters.target_date && filters.target_date.length <= 0) return null;
|
||||||
|
|
||||||
|
const splitDate = date.split(";");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={date}
|
||||||
|
className="inline-flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-100 px-1 py-0.5"
|
||||||
|
>
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full" />
|
||||||
|
<span className="capitalize">
|
||||||
|
{splitDate[1]} {renderShortDateWithYearFormat(splitDate[0])}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
target_date: filters.target_date?.filter((d: any) => d !== date),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
</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(", ")}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
[key]: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-x-1 capitalize">
|
||||||
|
{filters[key as keyof typeof filters]}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
[key]: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{Object.keys(filters).length > 0 && nullFilters.length !== Object.keys(filters).length && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={clearAllFilters}
|
||||||
|
className="flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-80 px-3 py-1.5 text-xs"
|
||||||
|
>
|
||||||
|
<span>Clear all filters</span>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -483,7 +483,6 @@ 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}
|
||||||
/>
|
/>
|
||||||
|
@ -10,5 +10,4 @@ export * from "./state-column";
|
|||||||
export * from "./updated-on-column";
|
export * from "./updated-on-column";
|
||||||
export * from "./spreadsheet-view";
|
export * from "./spreadsheet-view";
|
||||||
export * from "./issue-column/issue-column";
|
export * from "./issue-column/issue-column";
|
||||||
export * from "./spreadsheet-columns";
|
|
||||||
export * from "./issue-column/spreadsheet-issue-column";
|
export * from "./issue-column/spreadsheet-issue-column";
|
||||||
|
@ -82,10 +82,10 @@ export const IssueColumn: React.FC<Props> = ({
|
|||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||||
|
|
||||||
return (
|
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-100">
|
<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
|
<div
|
||||||
className="flex gap-1.5 px-4 pr-0 py-2.5 items-center"
|
className="flex gap-1.5 px-4 pr-0 py-2.5 items-center w-24"
|
||||||
style={issue.parent ? { paddingLeft } : {}}
|
style={issue.parent && nestingLevel !== 0 ? { paddingLeft } : {}}
|
||||||
>
|
>
|
||||||
<div className="relative flex items-center cursor-pointer text-xs text-center hover:text-custom-text-100">
|
<div className="relative flex items-center cursor-pointer text-xs text-center hover:text-custom-text-100">
|
||||||
{properties.key && (
|
{properties.key && (
|
||||||
|
@ -1,274 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
// hooks
|
|
||||||
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
|
||||||
import useLocalStorage from "hooks/use-local-storage";
|
|
||||||
// component
|
|
||||||
import { CustomMenu, Icon } from "components/ui";
|
|
||||||
// icon
|
|
||||||
import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
|
|
||||||
// types
|
|
||||||
import { TIssueOrderByOptions } from "types";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
columnData: any;
|
|
||||||
gridTemplateColumns: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SpreadsheetColumns: React.FC<Props> = ({ columnData, gridTemplateColumns }) => {
|
|
||||||
const { storedValue: selectedMenuItem, setValue: setSelectedMenuItem } = useLocalStorage(
|
|
||||||
"spreadsheetViewSorting",
|
|
||||||
""
|
|
||||||
);
|
|
||||||
const { storedValue: activeSortingProperty, setValue: setActiveSortingProperty } =
|
|
||||||
useLocalStorage("spreadsheetViewActiveSortingProperty", "");
|
|
||||||
|
|
||||||
const { displayFilters, setDisplayFilters } = useSpreadsheetIssuesView();
|
|
||||||
|
|
||||||
const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => {
|
|
||||||
setDisplayFilters({ order_by: order });
|
|
||||||
setSelectedMenuItem(`${order}_${itemKey}`);
|
|
||||||
setActiveSortingProperty(order === "-created_at" ? "" : itemKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`grid auto-rows-[minmax(36px,1fr)] w-full min-w-max`}
|
|
||||||
style={{ gridTemplateColumns }}
|
|
||||||
>
|
|
||||||
{columnData.map((col: any) => {
|
|
||||||
if (col.isActive) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`bg-custom-background-90 w-full ${
|
|
||||||
col.propertyName === "title"
|
|
||||||
? "sticky left-0 z-20 bg-custom-background-90 pl-24"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{col.propertyName === "title" ? (
|
|
||||||
<div
|
|
||||||
className={`flex items-center justify-start gap-1.5 cursor-default text-sm text-custom-text-200 text-current w-full py-2.5 px-2`}
|
|
||||||
>
|
|
||||||
{col.colName}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<CustomMenu
|
|
||||||
className="!w-full"
|
|
||||||
customButton={
|
|
||||||
<div
|
|
||||||
className={`relative group flex items-center justify-start gap-1.5 cursor-pointer text-sm text-custom-text-200 hover:text-custom-text-100 w-full py-3 px-2 ${
|
|
||||||
activeSortingProperty === col.propertyName ? "bg-custom-background-80" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{activeSortingProperty === col.propertyName && (
|
|
||||||
<div className="absolute top-1 right-1.5">
|
|
||||||
<Icon
|
|
||||||
iconName="filter_list"
|
|
||||||
className="flex items-center justify-center h-3.5 w-3.5 rounded-full bg-custom-primary text-xs text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{col.icon ? (
|
|
||||||
<col.icon
|
|
||||||
className={`text-custom-text-200 group-hover:text-custom-text-100 ${
|
|
||||||
col.propertyName === "estimate" ? "-rotate-90" : ""
|
|
||||||
}`}
|
|
||||||
aria-hidden="true"
|
|
||||||
height="14"
|
|
||||||
width="14"
|
|
||||||
/>
|
|
||||||
) : col.propertyName === "priority" ? (
|
|
||||||
<span className="text-sm material-symbols-rounded text-custom-text-200">
|
|
||||||
signal_cellular_alt
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
|
|
||||||
{col.colName}
|
|
||||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
width="xl"
|
|
||||||
>
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
onClick={() => {
|
|
||||||
handleOrderBy(col.ascendingOrder, col.propertyName);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`group flex gap-1.5 px-1 items-center justify-between ${
|
|
||||||
selectedMenuItem === `${col.ascendingOrder}_${col.propertyName}`
|
|
||||||
? "text-custom-text-100"
|
|
||||||
: "text-custom-text-200 hover:text-custom-text-100"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
{col.propertyName === "assignee" || col.propertyName === "labels" ? (
|
|
||||||
<>
|
|
||||||
<span className="relative flex items-center h-6 w-6">
|
|
||||||
<Icon
|
|
||||||
iconName="east"
|
|
||||||
className="absolute left-0 rotate-90 text-xs leading-3"
|
|
||||||
/>
|
|
||||||
<Icon iconName="sort" className="absolute right-0 text-sm" />
|
|
||||||
</span>
|
|
||||||
<span>A</span>
|
|
||||||
<Icon iconName="east" className="text-sm" />
|
|
||||||
<span>Z</span>
|
|
||||||
</>
|
|
||||||
) : col.propertyName === "due_date" ||
|
|
||||||
col.propertyName === "created_on" ||
|
|
||||||
col.propertyName === "updated_on" ? (
|
|
||||||
<>
|
|
||||||
<span className="relative flex items-center h-6 w-6">
|
|
||||||
<Icon
|
|
||||||
iconName="east"
|
|
||||||
className="absolute left-0 rotate-90 text-xs leading-3"
|
|
||||||
/>
|
|
||||||
<Icon iconName="sort" className="absolute right-0 text-sm" />
|
|
||||||
</span>
|
|
||||||
<span>New</span>
|
|
||||||
<Icon iconName="east" className="text-sm" />
|
|
||||||
<span>Old</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span className="relative flex items-center h-6 w-6">
|
|
||||||
<Icon
|
|
||||||
iconName="east"
|
|
||||||
className="absolute left-0 rotate-90 text-xs leading-3"
|
|
||||||
/>
|
|
||||||
<Icon iconName="sort" className="absolute right-0 text-sm" />
|
|
||||||
</span>
|
|
||||||
<span>First</span>
|
|
||||||
<Icon iconName="east" className="text-sm" />
|
|
||||||
<span>Last</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CheckIcon
|
|
||||||
className={`h-3.5 w-3.5 opacity-0 group-hover:opacity-100 ${
|
|
||||||
selectedMenuItem === `${col.ascendingOrder}_${col.propertyName}`
|
|
||||||
? "opacity-100"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
className={`mt-0.5 ${
|
|
||||||
selectedMenuItem === `${col.descendingOrder}_${col.propertyName}`
|
|
||||||
? "bg-custom-background-80"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
key={col.property}
|
|
||||||
onClick={() => {
|
|
||||||
handleOrderBy(col.descendingOrder, col.propertyName);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`group flex gap-1.5 px-1 items-center justify-between ${
|
|
||||||
selectedMenuItem === `${col.descendingOrder}_${col.propertyName}`
|
|
||||||
? "text-custom-text-100"
|
|
||||||
: "text-custom-text-200 hover:text-custom-text-100"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
{col.propertyName === "assignee" || col.propertyName === "labels" ? (
|
|
||||||
<>
|
|
||||||
<span className="relative flex items-center h-6 w-6">
|
|
||||||
<Icon
|
|
||||||
iconName="east"
|
|
||||||
className="absolute left-0 -rotate-90 text-xs leading-3"
|
|
||||||
/>
|
|
||||||
<Icon
|
|
||||||
iconName="sort"
|
|
||||||
className="absolute rotate-180 transform scale-x-[-1] right-0 text-sm"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span>Z</span>
|
|
||||||
<Icon iconName="east" className="text-sm" />
|
|
||||||
<span>A</span>
|
|
||||||
</>
|
|
||||||
) : col.propertyName === "due_date" ? (
|
|
||||||
<>
|
|
||||||
<span className="relative flex items-center h-6 w-6">
|
|
||||||
<Icon
|
|
||||||
iconName="east"
|
|
||||||
className="absolute left-0 -rotate-90 text-xs leading-3"
|
|
||||||
/>
|
|
||||||
<Icon
|
|
||||||
iconName="sort"
|
|
||||||
className="absolute rotate-180 transform scale-x-[-1] right-0 text-sm"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span>Old</span>
|
|
||||||
<Icon iconName="east" className="text-sm" />
|
|
||||||
<span>New</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span className="relative flex items-center h-6 w-6">
|
|
||||||
<Icon
|
|
||||||
iconName="east"
|
|
||||||
className="absolute left-0 -rotate-90 text-xs leading-3"
|
|
||||||
/>
|
|
||||||
<Icon
|
|
||||||
iconName="sort"
|
|
||||||
className="absolute rotate-180 transform scale-x-[-1] right-0 text-sm"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span>Last</span>
|
|
||||||
<Icon iconName="east" className="text-sm" />
|
|
||||||
<span>First</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CheckIcon
|
|
||||||
className={`h-3.5 w-3.5 opacity-0 group-hover:opacity-100 ${
|
|
||||||
selectedMenuItem === `${col.descendingOrder}_${col.propertyName}`
|
|
||||||
? "opacity-100"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
{selectedMenuItem &&
|
|
||||||
selectedMenuItem !== "" &&
|
|
||||||
displayFilters?.order_by !== "-created_at" &&
|
|
||||||
selectedMenuItem.includes(col.propertyName) && (
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
className={`mt-0.5${
|
|
||||||
selectedMenuItem === `-created_at_${col.propertyName}`
|
|
||||||
? "bg-custom-background-80"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
key={col.property}
|
|
||||||
onClick={() => {
|
|
||||||
handleOrderBy("-created_at", col.propertyName);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={`group flex gap-1.5 px-1 items-center justify-between `}>
|
|
||||||
<div className="flex gap-1.5 items-center">
|
|
||||||
<span className="relative flex items-center justify-center h-6 w-6">
|
|
||||||
<Icon iconName="ink_eraser" className="text-sm" />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span>Clear sorting</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
)}
|
|
||||||
</CustomMenu>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
// next
|
// next
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@ -19,13 +19,20 @@ import {
|
|||||||
SpreadsheetStateColumn,
|
SpreadsheetStateColumn,
|
||||||
SpreadsheetUpdatedOnColumn,
|
SpreadsheetUpdatedOnColumn,
|
||||||
} from "components/core";
|
} from "components/core";
|
||||||
import { CustomMenu, Spinner } from "components/ui";
|
import { CustomMenu, Icon, 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 useLocalStorage from "hooks/use-local-storage";
|
||||||
|
import { useWorkspaceView } from "hooks/use-workspace-view";
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssue, ISubIssueResponse, UserAuth } from "types";
|
import {
|
||||||
import useWorkspaceIssuesFilters from "hooks/use-worskpace-issue-filter";
|
ICurrentUserResponse,
|
||||||
|
IIssue,
|
||||||
|
ISubIssueResponse,
|
||||||
|
TIssueOrderByOptions,
|
||||||
|
UserAuth,
|
||||||
|
} from "types";
|
||||||
import {
|
import {
|
||||||
CYCLE_DETAILS,
|
CYCLE_DETAILS,
|
||||||
CYCLE_ISSUES_WITH_PARAMS,
|
CYCLE_ISSUES_WITH_PARAMS,
|
||||||
@ -39,7 +46,7 @@ import {
|
|||||||
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||||
import projectIssuesServices from "services/issues.service";
|
import projectIssuesServices from "services/issues.service";
|
||||||
// icon
|
// icon
|
||||||
import { PlusIcon } from "lucide-react";
|
import { CheckIcon, ChevronDownIcon, PlusIcon } from "lucide-react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
spreadsheetIssues: IIssue[];
|
spreadsheetIssues: IIssue[];
|
||||||
@ -70,6 +77,10 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
|
|
||||||
const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false);
|
const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false);
|
||||||
|
|
||||||
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
|
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId, workspaceViewId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId, workspaceViewId } = router.query;
|
||||||
|
|
||||||
@ -77,6 +88,13 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
|
|
||||||
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||||
|
|
||||||
|
const { storedValue: selectedMenuItem, setValue: setSelectedMenuItem } = useLocalStorage(
|
||||||
|
"spreadsheetViewSorting",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
const { storedValue: activeSortingProperty, setValue: setActiveSortingProperty } =
|
||||||
|
useLocalStorage("spreadsheetViewActiveSortingProperty", "");
|
||||||
|
|
||||||
const workspaceIssuesPath = [
|
const workspaceIssuesPath = [
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
@ -111,12 +129,9 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
router.pathname.includes(path.path)
|
router.pathname.includes(path.path)
|
||||||
);
|
);
|
||||||
|
|
||||||
const { params: workspaceViewParams } = useWorkspaceIssuesFilters(
|
const { params: workspaceViewParams, handleFilters } = useWorkspaceView();
|
||||||
workspaceSlug?.toString(),
|
|
||||||
workspaceViewId?.toString()
|
|
||||||
);
|
|
||||||
|
|
||||||
const { params } = useSpreadsheetIssuesView();
|
const { params, displayFilters, setDisplayFilters } = useSpreadsheetIssuesView();
|
||||||
|
|
||||||
const partialUpdateIssue = useCallback(
|
const partialUpdateIssue = useCallback(
|
||||||
(formData: Partial<IIssue>, issue: IIssue) => {
|
(formData: Partial<IIssue>, issue: IIssue) => {
|
||||||
@ -199,8 +214,8 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
moduleId,
|
moduleId,
|
||||||
viewId,
|
viewId,
|
||||||
workspaceViewId,
|
workspaceViewId,
|
||||||
currentWorkspaceIssuePath,
|
|
||||||
workspaceViewParams,
|
workspaceViewParams,
|
||||||
|
currentWorkspaceIssuePath,
|
||||||
params,
|
params,
|
||||||
user,
|
user,
|
||||||
]
|
]
|
||||||
@ -208,10 +223,216 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||||
|
|
||||||
const renderColumn = (header: string, Component: React.ComponentType<any>) => (
|
const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => {
|
||||||
<div className="relative flex flex-col h-max w-full bg-custom-background-100 rounded-sm">
|
if (!workspaceViewId || !currentWorkspaceIssuePath)
|
||||||
|
handleFilters("display_filters", { order_by: order });
|
||||||
|
else setDisplayFilters({ order_by: order });
|
||||||
|
setSelectedMenuItem(`${order}_${itemKey}`);
|
||||||
|
setActiveSortingProperty(order === "-created_at" ? "" : itemKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderColumn = (
|
||||||
|
header: string,
|
||||||
|
propertyName: string,
|
||||||
|
Component: React.ComponentType<any>,
|
||||||
|
ascendingOrder: TIssueOrderByOptions,
|
||||||
|
descendingOrder: TIssueOrderByOptions
|
||||||
|
) => (
|
||||||
|
<div className="relative flex flex-col h-max w-full bg-custom-background-100">
|
||||||
<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">
|
<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}
|
<CustomMenu
|
||||||
|
customButtonClassName="!w-full"
|
||||||
|
className="!w-full"
|
||||||
|
position="left"
|
||||||
|
customButton={
|
||||||
|
<div
|
||||||
|
className={`relative group flex items-center justify-between gap-1.5 cursor-pointer text-sm text-custom-text-200 hover:text-custom-text-100 w-full py-3 px-2 ${
|
||||||
|
activeSortingProperty === propertyName ? "bg-custom-background-80" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{activeSortingProperty === propertyName && (
|
||||||
|
<div className="absolute top-1 right-1.5">
|
||||||
|
<Icon
|
||||||
|
iconName="filter_list"
|
||||||
|
className="flex items-center justify-center h-3.5 w-3.5 rounded-full bg-custom-primary text-xs text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{header}
|
||||||
|
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
width="xl"
|
||||||
|
>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
handleOrderBy(ascendingOrder, propertyName);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`group flex gap-1.5 px-1 items-center justify-between ${
|
||||||
|
selectedMenuItem === `${ascendingOrder}_${propertyName}`
|
||||||
|
? "text-custom-text-100"
|
||||||
|
: "text-custom-text-200 hover:text-custom-text-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
{propertyName === "assignee" || propertyName === "labels" ? (
|
||||||
|
<>
|
||||||
|
<span className="relative flex items-center h-6 w-6">
|
||||||
|
<Icon
|
||||||
|
iconName="east"
|
||||||
|
className="absolute left-0 rotate-90 text-xs leading-3"
|
||||||
|
/>
|
||||||
|
<Icon iconName="sort" className="absolute right-0 text-sm" />
|
||||||
|
</span>
|
||||||
|
<span>A</span>
|
||||||
|
<Icon iconName="east" className="text-sm" />
|
||||||
|
<span>Z</span>
|
||||||
|
</>
|
||||||
|
) : propertyName === "due_date" ||
|
||||||
|
propertyName === "created_on" ||
|
||||||
|
propertyName === "updated_on" ? (
|
||||||
|
<>
|
||||||
|
<span className="relative flex items-center h-6 w-6">
|
||||||
|
<Icon
|
||||||
|
iconName="east"
|
||||||
|
className="absolute left-0 rotate-90 text-xs leading-3"
|
||||||
|
/>
|
||||||
|
<Icon iconName="sort" className="absolute right-0 text-sm" />
|
||||||
|
</span>
|
||||||
|
<span>New</span>
|
||||||
|
<Icon iconName="east" className="text-sm" />
|
||||||
|
<span>Old</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="relative flex items-center h-6 w-6">
|
||||||
|
<Icon
|
||||||
|
iconName="east"
|
||||||
|
className="absolute left-0 rotate-90 text-xs leading-3"
|
||||||
|
/>
|
||||||
|
<Icon iconName="sort" className="absolute right-0 text-sm" />
|
||||||
|
</span>
|
||||||
|
<span>First</span>
|
||||||
|
<Icon iconName="east" className="text-sm" />
|
||||||
|
<span>Last</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CheckIcon
|
||||||
|
className={`h-3.5 w-3.5 opacity-0 group-hover:opacity-100 ${
|
||||||
|
selectedMenuItem === `${ascendingOrder}_${propertyName}` ? "opacity-100" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
className={`mt-0.5 ${
|
||||||
|
selectedMenuItem === `${descendingOrder}_${propertyName}`
|
||||||
|
? "bg-custom-background-80"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
key={propertyName}
|
||||||
|
onClick={() => {
|
||||||
|
handleOrderBy(descendingOrder, propertyName);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`group flex gap-1.5 px-1 items-center justify-between ${
|
||||||
|
selectedMenuItem === `${descendingOrder}_${propertyName}`
|
||||||
|
? "text-custom-text-100"
|
||||||
|
: "text-custom-text-200 hover:text-custom-text-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
{propertyName === "assignee" || propertyName === "labels" ? (
|
||||||
|
<>
|
||||||
|
<span className="relative flex items-center h-6 w-6">
|
||||||
|
<Icon
|
||||||
|
iconName="east"
|
||||||
|
className="absolute left-0 -rotate-90 text-xs leading-3"
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
iconName="sort"
|
||||||
|
className="absolute rotate-180 transform scale-x-[-1] right-0 text-sm"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>Z</span>
|
||||||
|
<Icon iconName="east" className="text-sm" />
|
||||||
|
<span>A</span>
|
||||||
|
</>
|
||||||
|
) : propertyName === "due_date" ? (
|
||||||
|
<>
|
||||||
|
<span className="relative flex items-center h-6 w-6">
|
||||||
|
<Icon
|
||||||
|
iconName="east"
|
||||||
|
className="absolute left-0 -rotate-90 text-xs leading-3"
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
iconName="sort"
|
||||||
|
className="absolute rotate-180 transform scale-x-[-1] right-0 text-sm"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>Old</span>
|
||||||
|
<Icon iconName="east" className="text-sm" />
|
||||||
|
<span>New</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="relative flex items-center h-6 w-6">
|
||||||
|
<Icon
|
||||||
|
iconName="east"
|
||||||
|
className="absolute left-0 -rotate-90 text-xs leading-3"
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
iconName="sort"
|
||||||
|
className="absolute rotate-180 transform scale-x-[-1] right-0 text-sm"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>Last</span>
|
||||||
|
<Icon iconName="east" className="text-sm" />
|
||||||
|
<span>First</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CheckIcon
|
||||||
|
className={`h-3.5 w-3.5 opacity-0 group-hover:opacity-100 ${
|
||||||
|
selectedMenuItem === `${descendingOrder}_${propertyName}` ? "opacity-100" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
{selectedMenuItem &&
|
||||||
|
selectedMenuItem !== "" &&
|
||||||
|
displayFilters?.order_by !== "-created_at" &&
|
||||||
|
selectedMenuItem.includes(propertyName) && (
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
className={`mt-0.5${
|
||||||
|
selectedMenuItem === `-created_at_${propertyName}`
|
||||||
|
? "bg-custom-background-80"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
key={propertyName}
|
||||||
|
onClick={() => {
|
||||||
|
handleOrderBy("-created_at", propertyName);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={`group flex gap-1.5 px-1 items-center justify-between `}>
|
||||||
|
<div className="flex gap-1.5 items-center">
|
||||||
|
<span className="relative flex items-center justify-center h-6 w-6">
|
||||||
|
<Icon iconName="ink_eraser" className="text-sm" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>Clear sorting</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
)}
|
||||||
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full min-w-[9rem] w-full">
|
<div className="h-full min-w-[9rem] w-full">
|
||||||
{spreadsheetIssues.map((issue: IIssue, index) => (
|
{spreadsheetIssues.map((issue: IIssue, index) => (
|
||||||
@ -230,6 +451,27 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (containerRef.current) {
|
||||||
|
const scrollLeft = containerRef.current.scrollLeft;
|
||||||
|
setIsScrolled(scrollLeft > 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentContainerRef = containerRef.current;
|
||||||
|
|
||||||
|
if (currentContainerRef) {
|
||||||
|
currentContainerRef.addEventListener("scroll", handleScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (currentContainerRef) {
|
||||||
|
currentContainerRef.removeEventListener("scroll", handleScroll);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IssuePeekOverview
|
<IssuePeekOverview
|
||||||
@ -238,15 +480,19 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
readOnly={disableUserActions}
|
readOnly={disableUserActions}
|
||||||
/>
|
/>
|
||||||
<div className="relative flex h-full w-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-100">
|
<div className="relative flex h-full w-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-200">
|
||||||
<div className="h-full w-full flex flex-col">
|
<div className="h-full w-full flex flex-col">
|
||||||
<div className="flex max-h-full overflow-y-auto">
|
<div ref={containerRef} className="flex max-h-full h-full overflow-y-auto">
|
||||||
{spreadsheetIssues ? (
|
{spreadsheetIssues ? (
|
||||||
<>
|
<>
|
||||||
<div className="sticky left-0 w-[28rem] z-[2]">
|
<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={`relative flex flex-col h-max w-full bg-custom-background-100 z-[2] ${
|
||||||
|
isScrolled ? "shadow-r shadow-custom-shadow-xs" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<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">
|
<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">
|
<span className="flex items-center px-4 py-2.5 h-full w-24 flex-shrink-0">
|
||||||
ID
|
ID
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center px-4 py-2.5 h-full w-full flex-grow">
|
<span className="flex items-center px-4 py-2.5 h-full w-full flex-grow">
|
||||||
@ -270,15 +516,69 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{renderColumn("State", SpreadsheetStateColumn)}
|
{renderColumn(
|
||||||
{renderColumn("Priority", SpreadsheetPriorityColumn)}
|
"State",
|
||||||
{renderColumn("Assignees", SpreadsheetAssigneeColumn)}
|
"state",
|
||||||
{renderColumn("Label", SpreadsheetLabelColumn)}
|
SpreadsheetStateColumn,
|
||||||
{renderColumn("Start Date", SpreadsheetStartDateColumn)}
|
"state__name",
|
||||||
{renderColumn("Due Date", SpreadsheetDueDateColumn)}
|
"-state__name"
|
||||||
{renderColumn("Estimate", SpreadsheetEstimateColumn)}
|
)}
|
||||||
{renderColumn("Created On", SpreadsheetCreatedOnColumn)}
|
{renderColumn(
|
||||||
{renderColumn("Updated On", SpreadsheetUpdatedOnColumn)}
|
"Priority",
|
||||||
|
"priority",
|
||||||
|
SpreadsheetPriorityColumn,
|
||||||
|
"priority",
|
||||||
|
"-priority"
|
||||||
|
)}
|
||||||
|
{renderColumn(
|
||||||
|
"Assignees",
|
||||||
|
"assignee",
|
||||||
|
SpreadsheetAssigneeColumn,
|
||||||
|
"assignees__first_name",
|
||||||
|
"-assignees__first_name"
|
||||||
|
)}
|
||||||
|
{renderColumn(
|
||||||
|
"Label",
|
||||||
|
"labels",
|
||||||
|
SpreadsheetLabelColumn,
|
||||||
|
"labels__name",
|
||||||
|
"-labels__name"
|
||||||
|
)}
|
||||||
|
{renderColumn(
|
||||||
|
"Start Date",
|
||||||
|
"start_date",
|
||||||
|
SpreadsheetStartDateColumn,
|
||||||
|
"-start_date",
|
||||||
|
"start_date"
|
||||||
|
)}
|
||||||
|
{renderColumn(
|
||||||
|
"Due Date",
|
||||||
|
"due_date",
|
||||||
|
SpreadsheetDueDateColumn,
|
||||||
|
"-target_date",
|
||||||
|
"target_date"
|
||||||
|
)}
|
||||||
|
{renderColumn(
|
||||||
|
"Estimate",
|
||||||
|
"estimate",
|
||||||
|
SpreadsheetEstimateColumn,
|
||||||
|
"estimate_point",
|
||||||
|
"-estimate_point"
|
||||||
|
)}
|
||||||
|
{renderColumn(
|
||||||
|
"Created On",
|
||||||
|
"created_on",
|
||||||
|
SpreadsheetCreatedOnColumn,
|
||||||
|
"-created_at",
|
||||||
|
"created_at"
|
||||||
|
)}
|
||||||
|
{renderColumn(
|
||||||
|
"Updated On",
|
||||||
|
"updated_on",
|
||||||
|
SpreadsheetUpdatedOnColumn,
|
||||||
|
"-updated_at",
|
||||||
|
"updated_at"
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col justify-center items-center h-full w-full">
|
<div className="flex flex-col justify-center items-center h-full w-full">
|
||||||
|
@ -76,7 +76,7 @@ export const StateColumn: React.FC<Props> = ({
|
|||||||
value={issue.state_detail}
|
value={issue.state_detail}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
onChange={handleStateChange}
|
onChange={handleStateChange}
|
||||||
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
buttonClassName="!shadow-none !border-0"
|
||||||
hideDropdownArrow
|
hideDropdownArrow
|
||||||
disabled={isNotAllowed}
|
disabled={isNotAllowed}
|
||||||
/>
|
/>
|
||||||
|
@ -4,21 +4,18 @@ 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 { Avatar, MultiLevelDropdown } from "components/ui";
|
import { 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, TStateGroups } from "types";
|
import { IIssueFilterOptions, IQuery, TStateGroups } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { WORKSPACE_LABELS } from "constants/fetch-keys";
|
import { WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||||
// constants
|
// constants
|
||||||
@ -26,7 +23,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>;
|
filters: Partial<IIssueFilterOptions> | IQuery;
|
||||||
onSelect: (option: any) => void;
|
onSelect: (option: any) => void;
|
||||||
direction?: "left" | "right";
|
direction?: "left" | "right";
|
||||||
height?: "sm" | "md" | "rg" | "lg";
|
height?: "sm" | "md" | "rg" | "lg";
|
||||||
@ -58,11 +55,6 @@ 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 && (
|
||||||
@ -82,19 +74,25 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
|
|||||||
height={height}
|
height={height}
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
id: "project",
|
id: "priority",
|
||||||
label: "Project",
|
label: "Priority",
|
||||||
value: joinedProjects,
|
value: PRIORITIES,
|
||||||
hasChildren: true,
|
hasChildren: true,
|
||||||
children: joinedProjects?.map((project) => ({
|
children: [
|
||||||
id: project.id,
|
...PRIORITIES.map((priority) => ({
|
||||||
label: <div className="flex items-center gap-2">{project.name}</div>,
|
id: priority === null ? "null" : priority,
|
||||||
value: {
|
label: (
|
||||||
key: "project",
|
<div className="flex items-center gap-2 capitalize">
|
||||||
value: project.id,
|
<PriorityIcon priority={priority} /> {priority ?? "None"}
|
||||||
},
|
</div>
|
||||||
selected: filters?.project?.includes(project.id),
|
),
|
||||||
})),
|
value: {
|
||||||
|
key: "priority",
|
||||||
|
value: priority === null ? "null" : priority,
|
||||||
|
},
|
||||||
|
selected: filters?.priority?.includes(priority === null ? "null" : priority),
|
||||||
|
})),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "state_group",
|
id: "state_group",
|
||||||
@ -144,87 +142,6 @@ 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",
|
||||||
|
221
web/components/issues/workspace-views/workpace-view-issues.tsx
Normal file
221
web/components/issues/workspace-views/workpace-view-issues.tsx
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
import React, { useCallback, useState } from "react";
|
||||||
|
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// context
|
||||||
|
import { useProjectMyMembership } from "contexts/project-member.context";
|
||||||
|
// service
|
||||||
|
import projectIssuesServices from "services/issues.service";
|
||||||
|
// hooks
|
||||||
|
import useProjects from "hooks/use-projects";
|
||||||
|
import useUser from "hooks/use-user";
|
||||||
|
import { useWorkspaceView } from "hooks/use-workspace-view";
|
||||||
|
import useWorkspaceMembers from "hooks/use-workspace-members";
|
||||||
|
// components
|
||||||
|
import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation";
|
||||||
|
import { EmptyState, PrimaryButton } from "components/ui";
|
||||||
|
import { SpreadsheetView, WorkspaceFiltersList } from "components/core";
|
||||||
|
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||||
|
import { CreateUpdateWorkspaceViewModal } from "components/workspace/views/modal";
|
||||||
|
// icon
|
||||||
|
import { PlusIcon } from "components/icons";
|
||||||
|
// image
|
||||||
|
import emptyView from "public/empty-state/view.svg";
|
||||||
|
// constants
|
||||||
|
import { WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||||
|
import { STATE_GROUP } from "constants/project";
|
||||||
|
// types
|
||||||
|
import { IIssue, IWorkspaceIssueFilterOptions } from "types";
|
||||||
|
|
||||||
|
export const WorkspaceViewIssues = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, workspaceViewId } = router.query;
|
||||||
|
|
||||||
|
const { memberRole } = useProjectMyMembership();
|
||||||
|
const { user } = useUser();
|
||||||
|
const { isGuest, isViewer } = useWorkspaceMembers(
|
||||||
|
workspaceSlug?.toString(),
|
||||||
|
Boolean(workspaceSlug)
|
||||||
|
);
|
||||||
|
const { filters, viewIssues, mutateViewIssues, handleFilters } = useWorkspaceView();
|
||||||
|
|
||||||
|
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 { 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 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.filters &&
|
||||||
|
Object.keys(filters.filters).filter(
|
||||||
|
(key) => filters.filters[key as keyof IWorkspaceIssueFilterOptions] === null
|
||||||
|
);
|
||||||
|
|
||||||
|
const areFiltersApplied =
|
||||||
|
filters.filters &&
|
||||||
|
Object.keys(filters.filters).length > 0 &&
|
||||||
|
nullFilters.length !== Object.keys(filters.filters).length;
|
||||||
|
|
||||||
|
const isNotAllowed = isGuest || isViewer;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CreateUpdateIssueModal
|
||||||
|
isOpen={createIssueModal && preloadedData?.actionType === "createIssue"}
|
||||||
|
handleClose={() => setCreateIssueModal(false)}
|
||||||
|
prePopulateData={{
|
||||||
|
...preloadedData,
|
||||||
|
}}
|
||||||
|
onSubmit={async () => mutateViewIssues()}
|
||||||
|
/>
|
||||||
|
<CreateUpdateIssueModal
|
||||||
|
isOpen={editIssueModal && issueToEdit?.actionType !== "delete"}
|
||||||
|
handleClose={() => setEditIssueModal(false)}
|
||||||
|
data={issueToEdit}
|
||||||
|
onSubmit={async () => mutateViewIssues()}
|
||||||
|
/>
|
||||||
|
<DeleteIssueModal
|
||||||
|
handleClose={() => setDeleteIssueModal(false)}
|
||||||
|
isOpen={deleteIssueModal}
|
||||||
|
data={issueToDelete}
|
||||||
|
user={user}
|
||||||
|
onSubmit={async () => mutateViewIssues()}
|
||||||
|
/>
|
||||||
|
<CreateUpdateWorkspaceViewModal
|
||||||
|
isOpen={createViewModal !== null}
|
||||||
|
handleClose={() => setCreateViewModal(null)}
|
||||||
|
preLoadedData={createViewModal}
|
||||||
|
/>
|
||||||
|
<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)} />
|
||||||
|
{false ? (
|
||||||
|
<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">
|
||||||
|
<WorkspaceFiltersList
|
||||||
|
filters={filters.filters}
|
||||||
|
setFilters={(updatedFilter) => handleFilters("filters", updatedFilter)}
|
||||||
|
labels={workspaceLabels}
|
||||||
|
members={workspaceMembers?.map((m) => m.member)}
|
||||||
|
stateGroup={STATE_GROUP}
|
||||||
|
project={joinedProjects}
|
||||||
|
clearAllFilters={() =>
|
||||||
|
handleFilters("filters", {
|
||||||
|
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) handleFilters("filters", filters.filters, true);
|
||||||
|
else
|
||||||
|
setCreateViewModal({
|
||||||
|
query: filters.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={mutateViewIssues}
|
||||||
|
handleIssueAction={handleIssueAction}
|
||||||
|
disableUserActions={isNotAllowed ?? false}
|
||||||
|
user={user}
|
||||||
|
userAuth={memberRole}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
234
web/components/issues/workspace-views/workspace-all-issue.tsx
Normal file
234
web/components/issues/workspace-views/workspace-all-issue.tsx
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
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 { useWorkspaceView } from "hooks/use-workspace-view";
|
||||||
|
// context
|
||||||
|
import { useProjectMyMembership } from "contexts/project-member.context";
|
||||||
|
// services
|
||||||
|
import workspaceService from "services/workspace.service";
|
||||||
|
import projectIssuesServices from "services/issues.service";
|
||||||
|
// components
|
||||||
|
import { SpreadsheetView, WorkspaceFiltersList } from "components/core";
|
||||||
|
import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation";
|
||||||
|
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||||
|
import { CreateUpdateWorkspaceViewModal } from "components/workspace/views/modal";
|
||||||
|
// ui
|
||||||
|
import { PrimaryButton } from "components/ui";
|
||||||
|
// icons
|
||||||
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
|
// fetch-keys
|
||||||
|
import { WORKSPACE_LABELS, WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys";
|
||||||
|
// constants
|
||||||
|
import { STATE_GROUP } from "constants/project";
|
||||||
|
// types
|
||||||
|
import { IIssue, IWorkspaceIssueFilterOptions } from "types";
|
||||||
|
|
||||||
|
export const WorkspaceAllIssue = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, workspaceViewId } = router.query;
|
||||||
|
|
||||||
|
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 { user } = useUser();
|
||||||
|
const { memberRole } = useProjectMyMembership();
|
||||||
|
|
||||||
|
const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? "");
|
||||||
|
|
||||||
|
const { data: workspaceLabels } = useSWR(
|
||||||
|
workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null,
|
||||||
|
workspaceSlug ? () => projectIssuesServices.getWorkspaceLabels(workspaceSlug.toString()) : null
|
||||||
|
);
|
||||||
|
|
||||||
|
const { filters, handleFilters } = useWorkspaceView();
|
||||||
|
|
||||||
|
const params: any = {
|
||||||
|
assignees: filters?.filters?.assignees ? filters?.filters?.assignees.join(",") : undefined,
|
||||||
|
subscriber: filters?.filters?.subscriber ? filters?.filters?.subscriber.join(",") : undefined,
|
||||||
|
state_group: filters?.filters?.state_group
|
||||||
|
? filters?.filters?.state_group.join(",")
|
||||||
|
: undefined,
|
||||||
|
priority: filters?.filters?.priority ? filters?.filters?.priority.join(",") : undefined,
|
||||||
|
labels: filters?.filters?.labels ? filters?.filters?.labels.join(",") : undefined,
|
||||||
|
created_by: filters?.filters?.created_by ? filters?.filters?.created_by.join(",") : undefined,
|
||||||
|
start_date: filters?.filters?.start_date ? filters?.filters?.start_date.join(",") : undefined,
|
||||||
|
target_date: filters?.filters?.target_date
|
||||||
|
? filters?.filters?.target_date.join(",")
|
||||||
|
: undefined,
|
||||||
|
project: filters?.filters?.project ? filters?.filters?.project.join(",") : undefined,
|
||||||
|
sub_issue: false,
|
||||||
|
type: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: viewIssues, mutate: mutateViewIssues } = 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.filters &&
|
||||||
|
Object.keys(filters.filters).filter(
|
||||||
|
(key) => filters.filters[key as keyof IWorkspaceIssueFilterOptions] === null
|
||||||
|
);
|
||||||
|
|
||||||
|
const areFiltersApplied =
|
||||||
|
filters.filters &&
|
||||||
|
Object.keys(filters.filters).length > 0 &&
|
||||||
|
nullFilters.length !== Object.keys(filters.filters).length;
|
||||||
|
|
||||||
|
const { projects: allProjects } = useProjects();
|
||||||
|
const joinedProjects = allProjects?.filter((p) => p.is_member);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CreateUpdateIssueModal
|
||||||
|
isOpen={createIssueModal && preloadedData?.actionType === "createIssue"}
|
||||||
|
handleClose={() => setCreateIssueModal(false)}
|
||||||
|
prePopulateData={{
|
||||||
|
...preloadedData,
|
||||||
|
}}
|
||||||
|
onSubmit={async () => {
|
||||||
|
mutateViewIssues();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CreateUpdateIssueModal
|
||||||
|
isOpen={editIssueModal && issueToEdit?.actionType !== "delete"}
|
||||||
|
handleClose={() => setEditIssueModal(false)}
|
||||||
|
data={issueToEdit}
|
||||||
|
onSubmit={async () => {
|
||||||
|
mutateViewIssues();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<DeleteIssueModal
|
||||||
|
handleClose={() => setDeleteIssueModal(false)}
|
||||||
|
isOpen={deleteIssueModal}
|
||||||
|
data={issueToDelete}
|
||||||
|
user={user}
|
||||||
|
onSubmit={async () => {
|
||||||
|
mutateViewIssues();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CreateUpdateWorkspaceViewModal
|
||||||
|
isOpen={createViewModal !== null}
|
||||||
|
handleClose={() => setCreateViewModal(null)}
|
||||||
|
preLoadedData={createViewModal}
|
||||||
|
/>
|
||||||
|
<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)} />
|
||||||
|
<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">
|
||||||
|
<WorkspaceFiltersList
|
||||||
|
filters={filters.filters}
|
||||||
|
setFilters={(updatedFilter) => handleFilters("filters", updatedFilter)}
|
||||||
|
labels={workspaceLabels}
|
||||||
|
members={workspaceMembers?.map((m) => m.member)}
|
||||||
|
stateGroup={STATE_GROUP}
|
||||||
|
project={joinedProjects}
|
||||||
|
clearAllFilters={() =>
|
||||||
|
handleFilters("filters", {
|
||||||
|
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) handleFilters("filters", filters.filters, true);
|
||||||
|
else
|
||||||
|
setCreateViewModal({
|
||||||
|
query: filters.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={mutateViewIssues}
|
||||||
|
handleIssueAction={handleIssueAction}
|
||||||
|
disableUserActions={false}
|
||||||
|
user={user}
|
||||||
|
userAuth={memberRole}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,155 @@
|
|||||||
|
import React, { 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";
|
||||||
|
// components
|
||||||
|
import { SpreadsheetView } from "components/core";
|
||||||
|
import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation";
|
||||||
|
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||||
|
import { CreateUpdateWorkspaceViewModal } from "components/workspace/views/modal";
|
||||||
|
// fetch-keys
|
||||||
|
import { WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
export const WorkspaceAssignedIssue = () => {
|
||||||
|
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 } = 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 (
|
||||||
|
<>
|
||||||
|
<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();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CreateUpdateWorkspaceViewModal
|
||||||
|
isOpen={createViewModal !== null}
|
||||||
|
handleClose={() => setCreateViewModal(null)}
|
||||||
|
preLoadedData={createViewModal}
|
||||||
|
/>
|
||||||
|
<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)} />
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,147 @@
|
|||||||
|
import React, { 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";
|
||||||
|
// components
|
||||||
|
import { SpreadsheetView } from "components/core";
|
||||||
|
import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation";
|
||||||
|
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||||
|
import { CreateUpdateWorkspaceViewModal } from "components/workspace/views/modal";
|
||||||
|
// fetch-keys
|
||||||
|
import { WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
export const WorkspaceCreatedIssues = () => {
|
||||||
|
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 } = router.query;
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
|
const { memberRole } = useProjectMyMembership();
|
||||||
|
|
||||||
|
const params: any = {
|
||||||
|
created_by: user?.id ?? undefined,
|
||||||
|
sub_issue: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<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();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CreateUpdateWorkspaceViewModal
|
||||||
|
isOpen={createViewModal !== null}
|
||||||
|
handleClose={() => setCreateViewModal(null)}
|
||||||
|
preLoadedData={createViewModal}
|
||||||
|
/>
|
||||||
|
<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)} />
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -3,10 +3,9 @@ import React from "react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
|
import { useWorkspaceView } from "hooks/use-workspace-view";
|
||||||
import useWorkspaceIssuesFilters from "hooks/use-worskpace-issue-filter";
|
|
||||||
// components
|
// components
|
||||||
import { MyIssuesSelectFilters } from "components/issues";
|
import { GlobalSelectFilters } from "components/workspace/views/global-select-filters";
|
||||||
// ui
|
// ui
|
||||||
import { Tooltip } from "components/ui";
|
import { Tooltip } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -33,12 +32,7 @@ export const WorkspaceIssuesViewOptions: React.FC = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, workspaceViewId } = router.query;
|
const { workspaceSlug, workspaceViewId } = router.query;
|
||||||
|
|
||||||
const { displayFilters, setDisplayFilters } = useMyIssuesFilters(workspaceSlug?.toString());
|
const { filters, handleFilters } = useWorkspaceView();
|
||||||
|
|
||||||
const { filters, setFilters } = useWorkspaceIssuesFilters(
|
|
||||||
workspaceSlug?.toString(),
|
|
||||||
workspaceViewId?.toString()
|
|
||||||
);
|
|
||||||
|
|
||||||
const isWorkspaceViewPath = router.pathname.includes("workspace-views/all-issues");
|
const isWorkspaceViewPath = router.pathname.includes("workspace-views/all-issues");
|
||||||
|
|
||||||
@ -58,12 +52,12 @@ export const WorkspaceIssuesViewOptions: React.FC = () => {
|
|||||||
<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-100 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
|
filters.display_filters?.layout === option.type
|
||||||
? "bg-custom-sidebar-background-100 shadow-sm"
|
? "bg-custom-sidebar-background-100 shadow-sm"
|
||||||
: "text-custom-sidebar-text-200"
|
: "text-custom-sidebar-text-200"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDisplayFilters({ layout: option.type });
|
handleFilters("display_filters", { layout: option.type }, true);
|
||||||
if (option.type === "spreadsheet")
|
if (option.type === "spreadsheet")
|
||||||
router.push(`/${workspaceSlug}/workspace-views/all-issues`);
|
router.push(`/${workspaceSlug}/workspace-views/all-issues`);
|
||||||
else router.push(`/${workspaceSlug}/workspace-views`);
|
else router.push(`/${workspaceSlug}/workspace-views`);
|
||||||
@ -82,37 +76,38 @@ export const WorkspaceIssuesViewOptions: React.FC = () => {
|
|||||||
|
|
||||||
{showFilters && (
|
{showFilters && (
|
||||||
<>
|
<>
|
||||||
<MyIssuesSelectFilters
|
<GlobalSelectFilters
|
||||||
filters={filters}
|
filters={filters.filters}
|
||||||
onSelect={(option) => {
|
onSelect={(option) => {
|
||||||
const key = option.key as keyof typeof filters;
|
const key = option.key as keyof typeof filters.filters;
|
||||||
|
|
||||||
if (key === "start_date" || key === "target_date") {
|
if (key === "start_date" || key === "target_date") {
|
||||||
const valueExists = checkIfArraysHaveSameElements(
|
const valueExists = checkIfArraysHaveSameElements(
|
||||||
filters?.[key] ?? [],
|
filters.filters?.[key] ?? [],
|
||||||
option.value
|
option.value
|
||||||
);
|
);
|
||||||
|
|
||||||
setFilters({
|
handleFilters("filters", {
|
||||||
|
...filters,
|
||||||
[key]: valueExists ? null : option.value,
|
[key]: valueExists ? null : option.value,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const valueExists = filters[key]?.includes(option.value);
|
if (!filters?.filters?.[key]?.includes(option.value))
|
||||||
|
handleFilters("filters", {
|
||||||
if (valueExists)
|
...filters,
|
||||||
setFilters({
|
[key]: [...((filters?.filters?.[key] as any[]) ?? []), option.value],
|
||||||
[option.key]: ((filters[key] ?? []) as any[])?.filter(
|
});
|
||||||
(val) => val !== option.value
|
else {
|
||||||
|
handleFilters("filters", {
|
||||||
|
...filters,
|
||||||
|
[key]: (filters?.filters?.[key] as any[])?.filter(
|
||||||
|
(item) => item !== option.value
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
else
|
}
|
||||||
setFilters({
|
|
||||||
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
direction="left"
|
direction="left"
|
||||||
height="rg"
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -0,0 +1,148 @@
|
|||||||
|
import React, { 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";
|
||||||
|
// components
|
||||||
|
import { SpreadsheetView } from "components/core";
|
||||||
|
import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation";
|
||||||
|
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||||
|
import { CreateUpdateWorkspaceViewModal } from "components/workspace/views/modal";
|
||||||
|
// fetch-keys
|
||||||
|
import { WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
export const WorkspaceSubscribedIssues = () => {
|
||||||
|
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 } = router.query;
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
|
const { memberRole } = useProjectMyMembership();
|
||||||
|
|
||||||
|
const params: any = {
|
||||||
|
subscriber: user?.id ?? undefined,
|
||||||
|
sub_issue: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<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();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CreateUpdateWorkspaceViewModal
|
||||||
|
isOpen={createViewModal !== null}
|
||||||
|
handleClose={() => setCreateViewModal(null)}
|
||||||
|
preLoadedData={createViewModal}
|
||||||
|
/>
|
||||||
|
<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)} />
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -10,7 +10,7 @@ import useEstimateOption from "hooks/use-estimate-option";
|
|||||||
// components
|
// components
|
||||||
import { MyIssuesSelectFilters } from "components/issues";
|
import { MyIssuesSelectFilters } from "components/issues";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu, CustomSearchSelect, ToggleSwitch, Tooltip } from "components/ui";
|
import { CustomMenu, ToggleSwitch, Tooltip } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||||
import { FormatListBulletedOutlined, GridViewOutlined } from "@mui/icons-material";
|
import { FormatListBulletedOutlined, GridViewOutlined } from "@mui/icons-material";
|
||||||
@ -21,7 +21,6 @@ import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
|||||||
import { Properties, TIssueViewOptions } from "types";
|
import { Properties, TIssueViewOptions } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
|
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
|
||||||
import useProjects from "hooks/use-projects";
|
|
||||||
|
|
||||||
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
|
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
|
||||||
{
|
{
|
||||||
@ -37,9 +36,6 @@ const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
|
|||||||
export const ProfileIssuesViewOptions: React.FC = () => {
|
export const ProfileIssuesViewOptions: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, userId } = router.query;
|
const { workspaceSlug, userId } = router.query;
|
||||||
|
|
||||||
const { projects } = useProjects();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
displayFilters,
|
displayFilters,
|
||||||
setDisplayFilters,
|
setDisplayFilters,
|
||||||
@ -51,28 +47,12 @@ export const ProfileIssuesViewOptions: React.FC = () => {
|
|||||||
|
|
||||||
const { isEstimateActive } = useEstimateOption();
|
const { isEstimateActive } = useEstimateOption();
|
||||||
|
|
||||||
const options = projects?.map((project) => ({
|
|
||||||
value: project.id,
|
|
||||||
query: project.name + " " + project.identifier,
|
|
||||||
content: project.name,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!router.pathname.includes("assigned") &&
|
!router.pathname.includes("assigned") &&
|
||||||
!router.pathname.includes("created") &&
|
!router.pathname.includes("created") &&
|
||||||
!router.pathname.includes("subscribed")
|
!router.pathname.includes("subscribed")
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
// return (
|
|
||||||
// <CustomSearchSelect
|
|
||||||
// value={projects ?? null}
|
|
||||||
// onChange={(val: string[] | null) => console.log(val)}
|
|
||||||
// label="Filters"
|
|
||||||
// options={options}
|
|
||||||
// position="right"
|
|
||||||
// multiple
|
|
||||||
// />
|
|
||||||
// );
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -287,38 +267,39 @@ export const ProfileIssuesViewOptions: React.FC = () => {
|
|||||||
<div className="space-y-2 py-3">
|
<div className="space-y-2 py-3">
|
||||||
<h4 className="text-sm text-custom-text-200">Display Properties</h4>
|
<h4 className="text-sm text-custom-text-200">Display Properties</h4>
|
||||||
<div className="flex flex-wrap items-center gap-2 text-custom-text-200">
|
<div className="flex flex-wrap items-center gap-2 text-custom-text-200">
|
||||||
{Object.keys(displayProperties).map((key) => {
|
{displayProperties &&
|
||||||
if (key === "estimate" && !isEstimateActive) return null;
|
Object.keys(displayProperties).map((key) => {
|
||||||
|
if (key === "estimate" && !isEstimateActive) return null;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
displayFilters?.layout === "spreadsheet" &&
|
displayFilters?.layout === "spreadsheet" &&
|
||||||
(key === "attachment_count" ||
|
(key === "attachment_count" ||
|
||||||
key === "link" ||
|
key === "link" ||
|
||||||
key === "sub_issue_count")
|
key === "sub_issue_count")
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
displayFilters?.layout !== "spreadsheet" &&
|
displayFilters?.layout !== "spreadsheet" &&
|
||||||
(key === "created_on" || key === "updated_on")
|
(key === "created_on" || key === "updated_on")
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={key}
|
key={key}
|
||||||
type="button"
|
type="button"
|
||||||
className={`rounded border px-2 py-1 text-xs capitalize ${
|
className={`rounded border px-2 py-1 text-xs capitalize ${
|
||||||
displayProperties[key as keyof Properties]
|
displayProperties[key as keyof Properties]
|
||||||
? "border-custom-primary bg-custom-primary text-white"
|
? "border-custom-primary bg-custom-primary text-white"
|
||||||
: "border-custom-border-200"
|
: "border-custom-border-200"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setProperties(key as keyof Properties)}
|
onClick={() => setProperties(key as keyof Properties)}
|
||||||
>
|
>
|
||||||
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
|
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,6 +19,7 @@ export type CustomMenuProps = DropdownProps & {
|
|||||||
|
|
||||||
const CustomMenu = ({
|
const CustomMenu = ({
|
||||||
buttonClassName = "",
|
buttonClassName = "",
|
||||||
|
customButtonClassName = "",
|
||||||
children,
|
children,
|
||||||
className = "",
|
className = "",
|
||||||
customButton,
|
customButton,
|
||||||
@ -40,7 +41,12 @@ const CustomMenu = ({
|
|||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
{customButton ? (
|
{customButton ? (
|
||||||
<Menu.Button as="button" type="button" onClick={menuButtonOnClick}>
|
<Menu.Button
|
||||||
|
as="button"
|
||||||
|
type="button"
|
||||||
|
onClick={menuButtonOnClick}
|
||||||
|
className={customButtonClassName}
|
||||||
|
>
|
||||||
{customButton}
|
{customButton}
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
) : (
|
) : (
|
||||||
|
1
web/components/ui/dropdowns/types.d.ts
vendored
1
web/components/ui/dropdowns/types.d.ts
vendored
@ -1,5 +1,6 @@
|
|||||||
export type DropdownProps = {
|
export type DropdownProps = {
|
||||||
buttonClassName?: string;
|
buttonClassName?: string;
|
||||||
|
customButtonClassName?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
customButton?: JSX.Element;
|
customButton?: JSX.Element;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
@ -8,7 +8,6 @@ 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
|
||||||
@ -18,17 +17,16 @@ 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, WORKSPACE_VIEWS_LIST } from "constants/fetch-keys";
|
import { 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, viewType, user }) => {
|
export const DeleteViewModal: React.FC<Props> = ({ isOpen, data, setIsOpen, user }) => {
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -43,64 +41,33 @@ export const DeleteViewModal: React.FC<Props> = ({ isOpen, data, setIsOpen, view
|
|||||||
|
|
||||||
const handleDeletion = async () => {
|
const handleDeletion = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
if (!workspaceSlug || !data || !projectId) return;
|
||||||
|
|
||||||
if (viewType === "project") {
|
await viewsService
|
||||||
if (!workspaceSlug || !data || !projectId) return;
|
.deleteView(workspaceSlug as string, projectId as string, data.id, user)
|
||||||
|
.then(() => {
|
||||||
|
mutate<IView[]>(VIEWS_LIST(projectId as string), (views) =>
|
||||||
|
views?.filter((view) => view.id !== data.id)
|
||||||
|
);
|
||||||
|
|
||||||
await viewsService
|
handleClose();
|
||||||
.deleteView(workspaceSlug as string, projectId as string, data.id, user)
|
|
||||||
.then(() => {
|
|
||||||
mutate<IView[]>(VIEWS_LIST(projectId as string), (views) =>
|
|
||||||
views?.filter((view) => view.id !== data.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
handleClose();
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
setToastAlert({
|
title: "Success!",
|
||||||
type: "success",
|
message: "View deleted successfully.",
|
||||||
title: "Success!",
|
|
||||||
message: "View deleted successfully.",
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "View could not be deleted. Please try again.",
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsDeleteLoading(false);
|
|
||||||
});
|
});
|
||||||
} else {
|
})
|
||||||
if (!workspaceSlug || !data) return;
|
.catch(() => {
|
||||||
|
setToastAlert({
|
||||||
await workspaceService
|
type: "error",
|
||||||
.deleteView(workspaceSlug as string, data.id)
|
title: "Error!",
|
||||||
.then(() => {
|
message: "View could not be deleted. Please try again.",
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
}
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -10,8 +10,6 @@ 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";
|
||||||
@ -24,14 +22,13 @@ 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, WORKSPACE_LABELS } from "constants/fetch-keys";
|
import { PROJECT_ISSUE_LABELS, STATES_LIST } 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -45,7 +42,6 @@ export const ViewForm: React.FC<Props> = ({
|
|||||||
handleClose,
|
handleClose,
|
||||||
status,
|
status,
|
||||||
data,
|
data,
|
||||||
viewType,
|
|
||||||
preLoadedData,
|
preLoadedData,
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -81,26 +77,8 @@ 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);
|
||||||
|
|
||||||
@ -113,14 +91,12 @@ 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,
|
||||||
project: null,
|
type: null,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -209,10 +185,9 @@ export const ViewForm: React.FC<Props> = ({
|
|||||||
<div>
|
<div>
|
||||||
<FiltersList
|
<FiltersList
|
||||||
filters={filters}
|
filters={filters}
|
||||||
labels={labelOptions}
|
labels={labels}
|
||||||
members={memberOptions}
|
members={members?.map((m) => m.member)}
|
||||||
states={states}
|
states={states}
|
||||||
project={joinedProjects}
|
|
||||||
clearAllFilters={clearAllFilters}
|
clearAllFilters={clearAllFilters}
|
||||||
setFilters={(query: any) => {
|
setFilters={(query: any) => {
|
||||||
setValue("query", {
|
setValue("query", {
|
||||||
|
@ -8,7 +8,6 @@ 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
|
||||||
@ -16,11 +15,10 @@ import { ViewForm } from "components/views";
|
|||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IView } from "types";
|
import { ICurrentUserResponse, IView } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { VIEWS_LIST, WORKSPACE_VIEWS_LIST } from "constants/fetch-keys";
|
import { 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;
|
||||||
@ -29,7 +27,6 @@ type Props = {
|
|||||||
|
|
||||||
export const CreateUpdateViewModal: React.FC<Props> = ({
|
export const CreateUpdateViewModal: React.FC<Props> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
viewType,
|
|
||||||
handleClose,
|
handleClose,
|
||||||
data,
|
data,
|
||||||
preLoadedData,
|
preLoadedData,
|
||||||
@ -49,48 +46,25 @@ export const CreateUpdateViewModal: React.FC<Props> = ({
|
|||||||
...payload,
|
...payload,
|
||||||
query_data: payload.query,
|
query_data: payload.query,
|
||||||
};
|
};
|
||||||
|
await viewsService
|
||||||
|
.createView(workspaceSlug as string, projectId as string, payload, user)
|
||||||
|
.then(() => {
|
||||||
|
mutate(VIEWS_LIST(projectId as string));
|
||||||
|
handleClose();
|
||||||
|
|
||||||
if (viewType === "project") {
|
setToastAlert({
|
||||||
await viewsService
|
type: "success",
|
||||||
.createView(workspaceSlug as string, projectId as string, payload, user)
|
title: "Success!",
|
||||||
.then(() => {
|
message: "View created successfully.",
|
||||||
mutate(VIEWS_LIST(projectId as string));
|
|
||||||
handleClose();
|
|
||||||
|
|
||||||
setToastAlert({
|
|
||||||
type: "success",
|
|
||||||
title: "Success!",
|
|
||||||
message: "View created successfully.",
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "View could not be created. Please try again.",
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
} else {
|
})
|
||||||
await workspaceService
|
.catch(() => {
|
||||||
.createView(workspaceSlug as string, payload)
|
setToastAlert({
|
||||||
.then(() => {
|
type: "error",
|
||||||
mutate(WORKSPACE_VIEWS_LIST(workspaceSlug as string));
|
title: "Error!",
|
||||||
handleClose();
|
message: "View could not be created. Please try again.",
|
||||||
|
|
||||||
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) => {
|
||||||
@ -98,79 +72,41 @@ 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) => {
|
mutate<IView[]>(
|
||||||
mutate<IView[]>(
|
VIEWS_LIST(projectId as string),
|
||||||
VIEWS_LIST(projectId as string),
|
(prevData) =>
|
||||||
(prevData) =>
|
prevData?.map((p) => {
|
||||||
prevData?.map((p) => {
|
if (p.id === res.id) return { ...p, ...payloadData };
|
||||||
if (p.id === res.id) return { ...p, ...payloadData };
|
|
||||||
|
|
||||||
return p;
|
return p;
|
||||||
}),
|
}),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
onClose();
|
onClose();
|
||||||
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
message: "View updated successfully.",
|
message: "View updated successfully.",
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "View could not be updated. Please try again.",
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
} else {
|
})
|
||||||
await workspaceService
|
.catch(() => {
|
||||||
.updateView(workspaceSlug as string, data?.id ?? "", payloadData)
|
setToastAlert({
|
||||||
.then((res) => {
|
type: "error",
|
||||||
mutate<IView[]>(
|
title: "Error!",
|
||||||
WORKSPACE_VIEWS_LIST(workspaceSlug as string),
|
message: "View could not be updated. Please try again.",
|
||||||
(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 (
|
||||||
@ -205,7 +141,6 @@ 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,9 +4,6 @@ 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";
|
||||||
@ -21,16 +18,11 @@ 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, TStateGroups } from "types";
|
import { IIssueFilterOptions } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import {
|
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
|
||||||
PROJECT_ISSUE_LABELS,
|
|
||||||
PROJECT_MEMBERS,
|
|
||||||
STATES_LIST,
|
|
||||||
WORKSPACE_LABELS,
|
|
||||||
} from "constants/fetch-keys";
|
|
||||||
// constants
|
// constants
|
||||||
import { GROUP_CHOICES, PRIORITIES } from "constants/project";
|
import { PRIORITIES } from "constants/project";
|
||||||
import { DATE_FILTER_OPTIONS } from "constants/filters";
|
import { DATE_FILTER_OPTIONS } from "constants/filters";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -56,7 +48,7 @@ export const SelectFilters: React.FC<Props> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, workspaceViewId } = router.query;
|
const { workspaceSlug, projectId } = 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,
|
||||||
@ -66,20 +58,6 @@ 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
|
||||||
@ -87,8 +65,6 @@ 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
|
||||||
@ -96,14 +72,6 @@ export const SelectFilters: React.FC<Props> = ({
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: workspaceLabels } = useSWR(
|
|
||||||
workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null,
|
|
||||||
workspaceSlug ? () => issuesService.getWorkspaceLabels(workspaceSlug.toString()) : null
|
|
||||||
);
|
|
||||||
|
|
||||||
const { projects: allProjects } = useProjects();
|
|
||||||
const joinedProjects = allProjects?.filter((p) => p.is_member);
|
|
||||||
|
|
||||||
const projectFilterOption = [
|
const projectFilterOption = [
|
||||||
{
|
{
|
||||||
id: "priority",
|
id: "priority",
|
||||||
@ -283,226 +251,6 @@ 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{isDateFilterModalOpen && (
|
{isDateFilterModalOpen && (
|
||||||
@ -520,7 +268,7 @@ export const SelectFilters: React.FC<Props> = ({
|
|||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
direction={direction}
|
direction={direction}
|
||||||
height={height}
|
height={height}
|
||||||
options={filterOption}
|
options={projectFilterOption}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -12,6 +12,7 @@ import { CustomMenu } from "components/ui";
|
|||||||
import viewsService from "services/views.service";
|
import viewsService from "services/views.service";
|
||||||
// types
|
// types
|
||||||
import { IView } from "types";
|
import { IView } from "types";
|
||||||
|
import { IWorkspaceView } from "types/workspace-views";
|
||||||
// fetch keys
|
// fetch keys
|
||||||
import { VIEWS_LIST } from "constants/fetch-keys";
|
import { VIEWS_LIST } from "constants/fetch-keys";
|
||||||
// hooks
|
// hooks
|
||||||
@ -20,7 +21,7 @@ import useToast from "hooks/use-toast";
|
|||||||
import { truncateText } from "helpers/string.helper";
|
import { truncateText } from "helpers/string.helper";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
view: IView;
|
view: IView | IWorkspaceView;
|
||||||
viewType: "project" | "workspace";
|
viewType: "project" | "workspace";
|
||||||
handleEditView: () => void;
|
handleEditView: () => void;
|
||||||
handleDeleteView: () => void;
|
handleDeleteView: () => void;
|
||||||
|
141
web/components/workspace/views/delete-workspace-view-modal.tsx
Normal file
141
web/components/workspace/views/delete-workspace-view-modal.tsx
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { mutate } from "swr";
|
||||||
|
|
||||||
|
// headless ui
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
// services
|
||||||
|
import workspaceService from "services/workspace.service";
|
||||||
|
// hooks
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
// ui
|
||||||
|
import { DangerButton, SecondaryButton } from "components/ui";
|
||||||
|
// icons
|
||||||
|
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||||
|
// types
|
||||||
|
import { IWorkspaceView } from "types/workspace-views";
|
||||||
|
// fetch-keys
|
||||||
|
import { WORKSPACE_VIEWS_LIST } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
data: IWorkspaceView | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DeleteWorkspaceViewModal: React.FC<Props> = ({ isOpen, data, setIsOpen }) => {
|
||||||
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeletion = async () => {
|
||||||
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
|
if (!workspaceSlug || !data) return;
|
||||||
|
|
||||||
|
await workspaceService
|
||||||
|
.deleteView(workspaceSlug as string, data.id)
|
||||||
|
.then(() => {
|
||||||
|
mutate<IWorkspaceView[]>(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 (
|
||||||
|
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||||
|
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-[40rem]">
|
||||||
|
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
|
<div className="sm:flex sm:items-start">
|
||||||
|
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-500/20 sm:mx-0 sm:h-10 sm:w-10">
|
||||||
|
<ExclamationTriangleIcon
|
||||||
|
className="h-6 w-6 text-red-600"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
|
<Dialog.Title
|
||||||
|
as="h3"
|
||||||
|
className="text-lg font-medium leading-6 text-custom-text-100"
|
||||||
|
>
|
||||||
|
Delete View
|
||||||
|
</Dialog.Title>
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-sm text-custom-text-200">
|
||||||
|
Are you sure you want to delete view-{" "}
|
||||||
|
<span className="break-words font-medium text-custom-text-100">
|
||||||
|
{data?.name}
|
||||||
|
</span>
|
||||||
|
? All of the data related to the view will be permanently removed. This
|
||||||
|
action cannot be undone.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-2 p-4 sm:px-6">
|
||||||
|
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||||
|
<DangerButton onClick={handleDeletion} loading={isDeleteLoading}>
|
||||||
|
{isDeleteLoading ? "Deleting..." : "Delete"}
|
||||||
|
</DangerButton>
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
);
|
||||||
|
};
|
213
web/components/workspace/views/form.tsx
Normal file
213
web/components/workspace/views/form.tsx
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// react-hook-form
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
// services
|
||||||
|
import issuesService from "services/issues.service";
|
||||||
|
|
||||||
|
// hooks
|
||||||
|
import useProjects from "hooks/use-projects";
|
||||||
|
import useWorkspaceMembers from "hooks/use-workspace-members";
|
||||||
|
// components
|
||||||
|
import { WorkspaceFiltersList } from "components/core";
|
||||||
|
import { GlobalSelectFilters } from "components/workspace/views/global-select-filters";
|
||||||
|
|
||||||
|
// ui
|
||||||
|
import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
|
||||||
|
// helpers
|
||||||
|
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||||
|
// types
|
||||||
|
import { IQuery } from "types";
|
||||||
|
import { IWorkspaceView } from "types/workspace-views";
|
||||||
|
// fetch-keys
|
||||||
|
import { WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||||
|
import { STATE_GROUP } from "constants/project";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handleFormSubmit: (values: IWorkspaceView) => Promise<void>;
|
||||||
|
handleClose: () => void;
|
||||||
|
status: boolean;
|
||||||
|
data?: IWorkspaceView | null;
|
||||||
|
preLoadedData?: Partial<IWorkspaceView> | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultValues: Partial<IWorkspaceView> = {
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorkspaceViewForm: React.FC<Props> = ({
|
||||||
|
handleFormSubmit,
|
||||||
|
handleClose,
|
||||||
|
status,
|
||||||
|
data,
|
||||||
|
preLoadedData,
|
||||||
|
}) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
|
} = useForm<any>({
|
||||||
|
defaultValues,
|
||||||
|
});
|
||||||
|
const filters = watch("query");
|
||||||
|
|
||||||
|
const { data: labelOptions } = useSWR(
|
||||||
|
workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null,
|
||||||
|
workspaceSlug ? () => issuesService.getWorkspaceLabels(workspaceSlug.toString()) : null
|
||||||
|
);
|
||||||
|
|
||||||
|
const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? "");
|
||||||
|
|
||||||
|
const memberOptions = workspaceMembers?.map((m) => m.member);
|
||||||
|
|
||||||
|
const { projects: allProjects } = useProjects();
|
||||||
|
const joinedProjects = allProjects?.filter((p) => p.is_member);
|
||||||
|
|
||||||
|
const handleCreateUpdateView = async (formData: IWorkspaceView) => {
|
||||||
|
await handleFormSubmit(formData);
|
||||||
|
|
||||||
|
reset({
|
||||||
|
...defaultValues,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearAllFilters = () => {
|
||||||
|
setValue("query", {
|
||||||
|
assignees: null,
|
||||||
|
created_by: null,
|
||||||
|
subscriber: null,
|
||||||
|
labels: null,
|
||||||
|
priority: null,
|
||||||
|
state_group: null,
|
||||||
|
start_date: null,
|
||||||
|
target_date: null,
|
||||||
|
project: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset({
|
||||||
|
...defaultValues,
|
||||||
|
...preLoadedData,
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
}, [data, preLoadedData, reset]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (status && data) {
|
||||||
|
setValue("query", data.query_data);
|
||||||
|
}
|
||||||
|
}, [data, status, setValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(handleCreateUpdateView)}>
|
||||||
|
<div className="space-y-5">
|
||||||
|
<h3 className="text-lg font-medium leading-6 text-custom-text-100">
|
||||||
|
{status ? "Update" : "Create"} View
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
type="name"
|
||||||
|
placeholder="Title"
|
||||||
|
autoComplete="off"
|
||||||
|
className="resize-none text-xl"
|
||||||
|
error={errors.name}
|
||||||
|
register={register}
|
||||||
|
validations={{
|
||||||
|
required: "Title is required",
|
||||||
|
maxLength: {
|
||||||
|
value: 255,
|
||||||
|
message: "Title should be less than 255 characters",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TextArea
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
placeholder="Description"
|
||||||
|
className="h-32 resize-none text-sm"
|
||||||
|
error={errors.description}
|
||||||
|
register={register}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<GlobalSelectFilters
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
setValue("query", {
|
||||||
|
...filters,
|
||||||
|
[key]: valueExists ? null : option.value,
|
||||||
|
} as IQuery);
|
||||||
|
} else {
|
||||||
|
if (!filters?.[key]?.includes(option.value))
|
||||||
|
setValue("query", {
|
||||||
|
...filters,
|
||||||
|
[key]: [...((filters?.[key] as any[]) ?? []), option.value],
|
||||||
|
});
|
||||||
|
else {
|
||||||
|
setValue("query", {
|
||||||
|
...filters,
|
||||||
|
[key]: (filters?.[key] as any[])?.filter((item) => item !== option.value),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<WorkspaceFiltersList
|
||||||
|
filters={filters}
|
||||||
|
labels={labelOptions}
|
||||||
|
members={memberOptions}
|
||||||
|
project={joinedProjects}
|
||||||
|
stateGroup={STATE_GROUP}
|
||||||
|
clearAllFilters={clearAllFilters}
|
||||||
|
setFilters={(query: any) => {
|
||||||
|
setValue("query", {
|
||||||
|
...filters,
|
||||||
|
...query,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5 flex justify-end gap-2">
|
||||||
|
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||||
|
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||||
|
{status
|
||||||
|
? isSubmitting
|
||||||
|
? "Updating View..."
|
||||||
|
: "Update View"
|
||||||
|
: isSubmitting
|
||||||
|
? "Creating View..."
|
||||||
|
: "Create View"}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
301
web/components/workspace/views/global-select-filters.tsx
Normal file
301
web/components/workspace/views/global-select-filters.tsx
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// hook
|
||||||
|
import useProjects from "hooks/use-projects";
|
||||||
|
import useWorkspaceMembers from "hooks/use-workspace-members";
|
||||||
|
// services
|
||||||
|
import issuesService from "services/issues.service";
|
||||||
|
// components
|
||||||
|
import { DateFilterModal } from "components/core";
|
||||||
|
// ui
|
||||||
|
import { Avatar, MultiLevelDropdown } from "components/ui";
|
||||||
|
// icons
|
||||||
|
import { PriorityIcon, StateGroupIcon } from "components/icons";
|
||||||
|
// helpers
|
||||||
|
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||||
|
// types
|
||||||
|
import { IWorkspaceIssueFilterOptions, TStateGroups } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||||
|
// constants
|
||||||
|
import { GROUP_CHOICES, PRIORITIES } from "constants/project";
|
||||||
|
import { DATE_FILTER_OPTIONS } from "constants/filters";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
filters: Partial<IWorkspaceIssueFilterOptions>;
|
||||||
|
onSelect: (option: any) => void;
|
||||||
|
direction?: "left" | "right";
|
||||||
|
height?: "sm" | "md" | "rg" | "lg";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GlobalSelectFilters: React.FC<Props> = ({
|
||||||
|
filters,
|
||||||
|
onSelect,
|
||||||
|
direction = "right",
|
||||||
|
height = "md",
|
||||||
|
}) => {
|
||||||
|
const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false);
|
||||||
|
const [dateFilterType, setDateFilterType] = useState<{
|
||||||
|
title: string;
|
||||||
|
type: "start_date" | "target_date";
|
||||||
|
}>({
|
||||||
|
title: "",
|
||||||
|
type: "start_date",
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? "");
|
||||||
|
|
||||||
|
const { data: workspaceLabels } = useSWR(
|
||||||
|
workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null,
|
||||||
|
workspaceSlug ? () => issuesService.getWorkspaceLabels(workspaceSlug.toString()) : null
|
||||||
|
);
|
||||||
|
|
||||||
|
const { projects: allProjects } = useProjects();
|
||||||
|
const joinedProjects = allProjects?.filter((p) => p.is_member);
|
||||||
|
|
||||||
|
const 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_group?.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>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isDateFilterModalOpen && (
|
||||||
|
<DateFilterModal
|
||||||
|
title={dateFilterType.title}
|
||||||
|
field={dateFilterType.type}
|
||||||
|
filters={filters as IWorkspaceIssueFilterOptions}
|
||||||
|
handleClose={() => setIsDateFilterModalOpen(false)}
|
||||||
|
isOpen={isDateFilterModalOpen}
|
||||||
|
onSelect={onSelect}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<MultiLevelDropdown
|
||||||
|
label="Filters"
|
||||||
|
onSelect={onSelect}
|
||||||
|
direction={direction}
|
||||||
|
height={height}
|
||||||
|
options={workspaceFilterOption}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
155
web/components/workspace/views/modal.tsx
Normal file
155
web/components/workspace/views/modal.tsx
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { mutate } from "swr";
|
||||||
|
|
||||||
|
// headless ui
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
// services
|
||||||
|
import workspaceService from "services/workspace.service";
|
||||||
|
// hooks
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
// components
|
||||||
|
import { WorkspaceViewForm } from "components/workspace/views/form";
|
||||||
|
// types
|
||||||
|
import { IWorkspaceView } from "types/workspace-views";
|
||||||
|
// fetch-keys
|
||||||
|
import { WORKSPACE_VIEWS_LIST } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
handleClose: () => void;
|
||||||
|
data?: IWorkspaceView | null;
|
||||||
|
preLoadedData?: Partial<IWorkspaceView> | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CreateUpdateWorkspaceViewModal: React.FC<Props> = ({
|
||||||
|
isOpen,
|
||||||
|
handleClose,
|
||||||
|
data,
|
||||||
|
preLoadedData,
|
||||||
|
}) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const createView = async (payload: any) => {
|
||||||
|
const payloadData = {
|
||||||
|
...payload,
|
||||||
|
query_data: {
|
||||||
|
filters: payload.query,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await workspaceService
|
||||||
|
.createView(workspaceSlug as string, payloadData)
|
||||||
|
.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: any) => {
|
||||||
|
const payloadData = {
|
||||||
|
...payload,
|
||||||
|
query_data: {
|
||||||
|
filters: payload.query,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await workspaceService
|
||||||
|
.updateView(workspaceSlug as string, data?.id ?? "", payloadData)
|
||||||
|
.then((res) => {
|
||||||
|
mutate<IWorkspaceView[]>(
|
||||||
|
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: any) => {
|
||||||
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
|
if (!data) await createView(formData);
|
||||||
|
else await updateView(formData);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||||
|
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||||
|
<WorkspaceViewForm
|
||||||
|
handleFormSubmit={handleFormSubmit}
|
||||||
|
handleClose={handleClose}
|
||||||
|
status={data ? true : false}
|
||||||
|
data={data}
|
||||||
|
preLoadedData={preLoadedData}
|
||||||
|
/>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
);
|
||||||
|
};
|
@ -14,6 +14,7 @@ const paramsToKey = (params: any) => {
|
|||||||
sub_issue,
|
sub_issue,
|
||||||
start_target_date,
|
start_target_date,
|
||||||
project,
|
project,
|
||||||
|
layout,
|
||||||
subscriber,
|
subscriber,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
@ -31,6 +32,7 @@ const paramsToKey = (params: any) => {
|
|||||||
const type = params.type ? params.type.toUpperCase() : "NULL";
|
const type = params.type ? params.type.toUpperCase() : "NULL";
|
||||||
const groupBy = params.group_by ? params.group_by.toUpperCase() : "NULL";
|
const groupBy = params.group_by ? params.group_by.toUpperCase() : "NULL";
|
||||||
const orderBy = params.order_by ? params.order_by.toUpperCase() : "NULL";
|
const orderBy = params.order_by ? params.order_by.toUpperCase() : "NULL";
|
||||||
|
const layoutKey = layout ? layout.toUpperCase() : "";
|
||||||
|
|
||||||
// sorting each keys in ascending order
|
// sorting each keys in ascending order
|
||||||
projectKey = projectKey.sort().join("_");
|
projectKey = projectKey.sort().join("_");
|
||||||
@ -42,7 +44,7 @@ const paramsToKey = (params: any) => {
|
|||||||
labelsKey = labelsKey.sort().join("_");
|
labelsKey = labelsKey.sort().join("_");
|
||||||
subscriberKey = subscriberKey.sort().join("_");
|
subscriberKey = subscriberKey.sort().join("_");
|
||||||
|
|
||||||
return `${projectKey}_${stateGroupKey}_${stateKey}_${priorityKey}_${assigneesKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${startDateKey}_${targetDateKey}_${sub_issue}_${startTargetDate}_${subscriberKey}`;
|
return `${layoutKey}_${projectKey}_${stateGroupKey}_${stateKey}_${priorityKey}_${assigneesKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${startDateKey}_${targetDateKey}_${sub_issue}_${startTargetDate}_${subscriberKey}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const inboxParamsToKey = (params: any) => {
|
const inboxParamsToKey = (params: any) => {
|
||||||
@ -162,12 +164,11 @@ export const WORKSPACE_VIEWS_LIST = (workspaceSlug: string) =>
|
|||||||
`WORKSPACE_VIEWS_LIST_${workspaceSlug.toUpperCase()}`;
|
`WORKSPACE_VIEWS_LIST_${workspaceSlug.toUpperCase()}`;
|
||||||
export const WORKSPACE_VIEW_DETAILS = (workspaceViewId: string) =>
|
export const WORKSPACE_VIEW_DETAILS = (workspaceViewId: string) =>
|
||||||
`WORKSPACE_VIEW_DETAILS_${workspaceViewId.toUpperCase()}`;
|
`WORKSPACE_VIEW_DETAILS_${workspaceViewId.toUpperCase()}`;
|
||||||
export const WORKSPACE_VIEW_ISSUES = (workspaceViewId: string, params?: any) => {
|
export const WORKSPACE_VIEW_ISSUES = (workspaceViewId: string, params: any) => {
|
||||||
if (!params) return `WORKSPACE_VIEW_ISSUES_${workspaceViewId.toUpperCase()}`;
|
if (!params) return `WORKSPACE_VIEW_ISSUES_${workspaceViewId.toUpperCase()}`;
|
||||||
|
return `WORKSPACE_VIEW_ISSUES_${workspaceViewId.toUpperCase()}_${paramsToKey(
|
||||||
const paramsKey = paramsToKey(params);
|
params
|
||||||
|
).toUpperCase()}`;
|
||||||
return `WORKSPACE_VIEW_ISSUES_${workspaceViewId.toUpperCase()}_${paramsKey.toUpperCase()}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PROJECT_ISSUES_DETAILS = (issueId: string) =>
|
export const PROJECT_ISSUES_DETAILS = (issueId: string) =>
|
||||||
|
@ -177,6 +177,7 @@ export const ProfileIssuesContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
type: "SET_PROPERTIES",
|
type: "SET_PROPERTIES",
|
||||||
payload: {
|
payload: {
|
||||||
display_properties: {
|
display_properties: {
|
||||||
|
...state.display_properties,
|
||||||
[key]: !state.display_properties[key],
|
[key]: !state.display_properties[key],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
235
web/contexts/workspace-view-context.tsx
Normal file
235
web/contexts/workspace-view-context.tsx
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
import { createContext, useEffect, useState } from "react";
|
||||||
|
// next imports
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
// swr
|
||||||
|
import useSWR, { KeyedMutator } from "swr";
|
||||||
|
// services
|
||||||
|
import workspaceService from "services/workspace.service";
|
||||||
|
// types
|
||||||
|
import { IIssue, IWorkspaceGlobalViewProps } from "types";
|
||||||
|
import { IWorkspaceView } from "types/workspace-views";
|
||||||
|
// fetch-keys
|
||||||
|
import { WORKSPACE_VIEW_DETAILS, WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
export interface IWorkspaceViewContext {
|
||||||
|
params: any;
|
||||||
|
view: IWorkspaceView | undefined;
|
||||||
|
viewLoading: boolean;
|
||||||
|
viewIssues: IIssue[];
|
||||||
|
mutateViewIssues: KeyedMutator<any>;
|
||||||
|
viewIssueLoading: boolean;
|
||||||
|
filters: IWorkspaceGlobalViewProps;
|
||||||
|
handleFilters: (
|
||||||
|
filterType: "filters" | "display_filters" | "display_properties",
|
||||||
|
payload: { [key: string]: any },
|
||||||
|
saveFiltersToServer?: boolean
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WorkspaceIssueViewContext = createContext<IWorkspaceViewContext | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
export const initialState: IWorkspaceGlobalViewProps = {
|
||||||
|
filters: {
|
||||||
|
assignees: null,
|
||||||
|
created_by: null,
|
||||||
|
labels: null,
|
||||||
|
priority: null,
|
||||||
|
state_group: null,
|
||||||
|
subscriber: null,
|
||||||
|
start_date: null,
|
||||||
|
target_date: null,
|
||||||
|
project: null,
|
||||||
|
},
|
||||||
|
display_filters: {
|
||||||
|
order_by: "-created_at",
|
||||||
|
sub_issue: false,
|
||||||
|
type: null,
|
||||||
|
layout: "spreadsheet",
|
||||||
|
},
|
||||||
|
display_properties: {
|
||||||
|
assignee: true,
|
||||||
|
start_date: true,
|
||||||
|
due_date: true,
|
||||||
|
key: true,
|
||||||
|
labels: true,
|
||||||
|
priority: true,
|
||||||
|
state: true,
|
||||||
|
sub_issue_count: true,
|
||||||
|
attachment_count: true,
|
||||||
|
link: true,
|
||||||
|
estimate: true,
|
||||||
|
created_on: true,
|
||||||
|
updated_on: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveViewFilters = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
workspaceViewId: string,
|
||||||
|
state: IWorkspaceGlobalViewProps
|
||||||
|
) => {
|
||||||
|
await workspaceService.updateView(workspaceSlug, workspaceViewId, {
|
||||||
|
query_data: state,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorkspaceViewProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, workspaceViewId } = router.query as {
|
||||||
|
workspaceSlug: string;
|
||||||
|
workspaceViewId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [filters, setFilters] = useState<IWorkspaceGlobalViewProps>(initialState);
|
||||||
|
|
||||||
|
const handleFilters = (
|
||||||
|
filterType: "filters" | "display_filters" | "display_properties",
|
||||||
|
payload: { [key: string]: any },
|
||||||
|
saveFiltersToServer?: boolean
|
||||||
|
) => {
|
||||||
|
const updatedFilterPayload = {
|
||||||
|
...filters,
|
||||||
|
[filterType]: {
|
||||||
|
...filters[filterType],
|
||||||
|
...payload,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setFilters(() => updatedFilterPayload);
|
||||||
|
|
||||||
|
if (saveFiltersToServer) saveViewFilters(workspaceSlug, workspaceViewId, updatedFilterPayload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const computedFilter = (filters: any) => {
|
||||||
|
const computedFilters: any = {};
|
||||||
|
Object.keys(filters).map((key) => {
|
||||||
|
if (filters[key] != undefined)
|
||||||
|
computedFilters[key] =
|
||||||
|
typeof filters[key] === "string" || typeof filters[key] === "boolean"
|
||||||
|
? filters[key]
|
||||||
|
: filters[key].join(",");
|
||||||
|
});
|
||||||
|
return computedFilters;
|
||||||
|
};
|
||||||
|
|
||||||
|
const computedParams = (filters: any) => {
|
||||||
|
const params: any = {
|
||||||
|
assignees: (filters && filters?.filters?.assignees) || undefined,
|
||||||
|
created_by: (filters && filters?.filters?.created_by) || undefined,
|
||||||
|
labels: (filters && filters?.filters?.labels) || undefined,
|
||||||
|
priority: (filters && filters?.filters?.priority) || undefined,
|
||||||
|
state_group: (filters && filters?.filters?.state_group) || undefined,
|
||||||
|
subscriber: (filters && filters?.filters?.subscriber) || undefined,
|
||||||
|
start_date: (filters && filters?.filters?.start_date) || undefined,
|
||||||
|
target_date: (filters && filters?.filters?.target_date) || undefined,
|
||||||
|
project: (filters && filters?.filters?.project) || undefined,
|
||||||
|
order_by: (filters && filters?.display_filters?.order_by) || "-created_at",
|
||||||
|
sub_issue: (filters && filters?.display_filters?.sub_issue) || false,
|
||||||
|
type: filters && filters?.display_filters?.type,
|
||||||
|
};
|
||||||
|
return params;
|
||||||
|
};
|
||||||
|
|
||||||
|
const params: any = {
|
||||||
|
assignees: filters?.filters.assignees ? filters?.filters?.assignees.join(",") : undefined,
|
||||||
|
created_by: filters?.filters.created_by ? filters?.filters?.created_by.join(",") : undefined,
|
||||||
|
labels: filters?.filters.labels ? filters?.filters?.labels.join(",") : undefined,
|
||||||
|
priority: filters?.filters.priority ? filters?.filters?.priority.join(",") : undefined,
|
||||||
|
state_group: filters?.filters.state_group ? filters?.filters?.state_group.join(",") : undefined,
|
||||||
|
subscriber: filters?.filters.subscriber ? filters?.filters?.subscriber.join(",") : undefined,
|
||||||
|
start_date: filters?.filters.start_date ? filters?.filters?.start_date.join(",") : undefined,
|
||||||
|
target_date: filters?.filters.target_date ? filters?.filters?.target_date.join(",") : undefined,
|
||||||
|
project: filters?.filters.project ? filters?.filters?.project.join(",") : undefined,
|
||||||
|
|
||||||
|
order_by: filters?.display_filters?.order_by
|
||||||
|
? filters?.display_filters?.order_by
|
||||||
|
: "-created_at",
|
||||||
|
sub_issue: filters?.display_filters?.sub_issue ? filters?.display_filters?.sub_issue : false,
|
||||||
|
type: filters?.display_filters?.type ? filters?.display_filters?.type : undefined,
|
||||||
|
layout: filters?.display_filters?.layout ? filters?.display_filters?.layout : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: view, isLoading: viewLoading } = useSWR(
|
||||||
|
workspaceSlug && workspaceViewId ? WORKSPACE_VIEW_DETAILS(workspaceViewId.toString()) : null,
|
||||||
|
workspaceSlug && workspaceViewId
|
||||||
|
? () => workspaceService.getViewDetails(workspaceSlug.toString(), workspaceViewId.toString())
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: viewIssues,
|
||||||
|
mutate: mutateViewIssues,
|
||||||
|
isLoading: viewIssueLoading,
|
||||||
|
} = useSWR(
|
||||||
|
workspaceSlug && view && workspaceViewId && filters
|
||||||
|
? WORKSPACE_VIEW_ISSUES(workspaceViewId.toString(), params)
|
||||||
|
: null,
|
||||||
|
workspaceSlug && view && workspaceViewId
|
||||||
|
? () =>
|
||||||
|
workspaceService.getViewIssues(
|
||||||
|
workspaceSlug.toString(),
|
||||||
|
computedFilter(computedParams(filters))
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (view && view?.query_data) {
|
||||||
|
const payload = {
|
||||||
|
filters: {
|
||||||
|
...view?.query_data?.filters,
|
||||||
|
assignees: view?.query_data?.filters?.assignees || null,
|
||||||
|
created_by: view?.query_data?.filters?.created_by || null,
|
||||||
|
labels: view?.query_data?.filters?.labels || null,
|
||||||
|
priority: view?.query_data?.filters?.priority || null,
|
||||||
|
state_group: view?.query_data?.filters?.state_group || null,
|
||||||
|
subscriber: view?.query_data?.filters?.subscriber || null,
|
||||||
|
start_date: view?.query_data?.filters?.start_date || null,
|
||||||
|
target_date: view?.query_data?.filters?.target_date || null,
|
||||||
|
project: view?.query_data?.filters?.project || null,
|
||||||
|
},
|
||||||
|
display_filters: {
|
||||||
|
...view?.query_data?.display_filters,
|
||||||
|
order_by: view?.query_data?.display_filters?.order_by || "-created_at",
|
||||||
|
sub_issue: view?.query_data?.display_filters?.sub_issue || true,
|
||||||
|
type: view?.query_data?.display_filters?.type || null,
|
||||||
|
},
|
||||||
|
display_properties: {
|
||||||
|
...view?.query_data?.display_properties,
|
||||||
|
assignee: view?.query_data?.display_properties?.assignee || true,
|
||||||
|
start_date: view?.query_data?.display_properties?.start_date || true,
|
||||||
|
due_date: view?.query_data?.display_properties?.due_date || true,
|
||||||
|
key: view?.query_data?.display_properties?.key || true,
|
||||||
|
labels: view?.query_data?.display_properties?.labels || true,
|
||||||
|
priority: view?.query_data?.display_properties?.priority || true,
|
||||||
|
state: view?.query_data?.display_properties?.state || true,
|
||||||
|
sub_issue_count: view?.query_data?.display_properties?.sub_issue_count || true,
|
||||||
|
attachment_count: view?.query_data?.display_properties?.attachment_count || true,
|
||||||
|
link: view?.query_data?.display_properties?.link || true,
|
||||||
|
estimate: view?.query_data?.display_properties?.estimate || true,
|
||||||
|
created_on: view?.query_data?.display_properties?.created_on || true,
|
||||||
|
updated_on: view?.query_data?.display_properties?.updated_on || true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setFilters(payload);
|
||||||
|
}
|
||||||
|
}, [view, setFilters]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WorkspaceIssueViewContext.Provider
|
||||||
|
value={{
|
||||||
|
params,
|
||||||
|
view,
|
||||||
|
viewLoading,
|
||||||
|
viewIssues,
|
||||||
|
mutateViewIssues,
|
||||||
|
viewIssueLoading,
|
||||||
|
filters,
|
||||||
|
handleFilters,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</WorkspaceIssueViewContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
@ -118,7 +118,7 @@ const useMyIssuesFilters = (workspaceSlug: string | undefined) => {
|
|||||||
|
|
||||||
const setProperty = useCallback(
|
const setProperty = useCallback(
|
||||||
(key: keyof Properties) => {
|
(key: keyof Properties) => {
|
||||||
if (!myWorkspace) return;
|
if (!myWorkspace?.view_props.display_properties) return;
|
||||||
|
|
||||||
saveData({
|
saveData({
|
||||||
display_properties: {
|
display_properties: {
|
||||||
|
11
web/hooks/use-workspace-view.tsx
Normal file
11
web/hooks/use-workspace-view.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
// types
|
||||||
|
import { IWorkspaceViewContext, WorkspaceIssueViewContext } from "contexts/workspace-view-context";
|
||||||
|
|
||||||
|
export const useWorkspaceView = (): IWorkspaceViewContext => {
|
||||||
|
const context = useContext(WorkspaceIssueViewContext);
|
||||||
|
|
||||||
|
if (!context) throw new Error("useWorkspaceView must be used within a WorkspaceIssueViewContext");
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
@ -1,113 +0,0 @@
|
|||||||
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;
|
|
@ -5,6 +5,7 @@ import { useRouter } from "next/router";
|
|||||||
|
|
||||||
// contexts
|
// contexts
|
||||||
import { useProjectMyMembership, ProjectMemberProvider } from "contexts/project-member.context";
|
import { useProjectMyMembership, ProjectMemberProvider } from "contexts/project-member.context";
|
||||||
|
import { WorkspaceViewProvider } from "contexts/workspace-view-context";
|
||||||
// layouts
|
// layouts
|
||||||
import AppHeader from "layouts/app-layout/app-header";
|
import AppHeader from "layouts/app-layout/app-header";
|
||||||
import AppSidebar from "layouts/app-layout/app-sidebar";
|
import AppSidebar from "layouts/app-layout/app-sidebar";
|
||||||
@ -29,7 +30,9 @@ type Props = {
|
|||||||
|
|
||||||
export const ProjectAuthorizationWrapper: React.FC<Props> = (props) => (
|
export const ProjectAuthorizationWrapper: React.FC<Props> = (props) => (
|
||||||
<ProjectMemberProvider>
|
<ProjectMemberProvider>
|
||||||
<ProjectAuthorizationWrapped {...props} />
|
<WorkspaceViewProvider>
|
||||||
|
<ProjectAuthorizationWrapped {...props} />
|
||||||
|
</WorkspaceViewProvider>
|
||||||
</ProjectMemberProvider>
|
</ProjectMemberProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import useSWR from "swr";
|
|||||||
import workspaceServices from "services/workspace.service";
|
import workspaceServices from "services/workspace.service";
|
||||||
// contexts
|
// contexts
|
||||||
import { WorkspaceMemberProvider } from "contexts/workspace-member.context";
|
import { WorkspaceMemberProvider } from "contexts/workspace-member.context";
|
||||||
|
import { WorkspaceViewProvider } from "contexts/workspace-view-context";
|
||||||
// layouts
|
// layouts
|
||||||
import AppSidebar from "layouts/app-layout/app-sidebar";
|
import AppSidebar from "layouts/app-layout/app-sidebar";
|
||||||
import AppHeader from "layouts/app-layout/app-header";
|
import AppHeader from "layouts/app-layout/app-header";
|
||||||
@ -81,47 +82,49 @@ export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<UserAuthorizationLayout>
|
<UserAuthorizationLayout>
|
||||||
<WorkspaceMemberProvider>
|
<WorkspaceMemberProvider>
|
||||||
<CommandPalette />
|
<WorkspaceViewProvider>
|
||||||
<div className="relative flex h-screen w-full overflow-hidden">
|
<CommandPalette />
|
||||||
<AppSidebar toggleSidebar={toggleSidebar} setToggleSidebar={setToggleSidebar} />
|
<div className="relative flex h-screen w-full overflow-hidden">
|
||||||
{settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? (
|
<AppSidebar toggleSidebar={toggleSidebar} setToggleSidebar={setToggleSidebar} />
|
||||||
<NotAuthorizedView
|
{settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? (
|
||||||
actionButton={
|
<NotAuthorizedView
|
||||||
<Link href={`/${workspaceSlug}`}>
|
actionButton={
|
||||||
<a>
|
<Link href={`/${workspaceSlug}`}>
|
||||||
<PrimaryButton className="flex items-center gap-1">
|
<a>
|
||||||
<LayerDiagonalIcon height={16} width={16} color="white" /> Go to workspace
|
<PrimaryButton className="flex items-center gap-1">
|
||||||
</PrimaryButton>
|
<LayerDiagonalIcon height={16} width={16} color="white" /> Go to workspace
|
||||||
</a>
|
</PrimaryButton>
|
||||||
</Link>
|
</a>
|
||||||
}
|
</Link>
|
||||||
type="workspace"
|
}
|
||||||
/>
|
type="workspace"
|
||||||
) : (
|
|
||||||
<main
|
|
||||||
className={`relative flex h-full w-full flex-col overflow-hidden ${
|
|
||||||
bg === "primary"
|
|
||||||
? "bg-custom-background-100"
|
|
||||||
: bg === "secondary"
|
|
||||||
? "bg-custom-background-90"
|
|
||||||
: "bg-custom-background-80"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<AppHeader
|
|
||||||
breadcrumbs={breadcrumbs}
|
|
||||||
left={left}
|
|
||||||
right={right}
|
|
||||||
setToggleSidebar={setToggleSidebar}
|
|
||||||
noHeader={noHeader}
|
|
||||||
/>
|
/>
|
||||||
<div className="h-full w-full overflow-hidden">
|
) : (
|
||||||
<div className="relative h-full w-full overflow-x-hidden overflow-y-scroll">
|
<main
|
||||||
{children}
|
className={`relative flex h-full w-full flex-col overflow-hidden ${
|
||||||
|
bg === "primary"
|
||||||
|
? "bg-custom-background-100"
|
||||||
|
: bg === "secondary"
|
||||||
|
? "bg-custom-background-90"
|
||||||
|
: "bg-custom-background-80"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<AppHeader
|
||||||
|
breadcrumbs={breadcrumbs}
|
||||||
|
left={left}
|
||||||
|
right={right}
|
||||||
|
setToggleSidebar={setToggleSidebar}
|
||||||
|
noHeader={noHeader}
|
||||||
|
/>
|
||||||
|
<div className="h-full w-full overflow-hidden">
|
||||||
|
<div className="relative h-full w-full overflow-x-hidden overflow-y-scroll">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
</main>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</WorkspaceViewProvider>
|
||||||
</WorkspaceMemberProvider>
|
</WorkspaceMemberProvider>
|
||||||
</UserAuthorizationLayout>
|
</UserAuthorizationLayout>
|
||||||
);
|
);
|
||||||
|
@ -88,14 +88,12 @@ 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}
|
||||||
|
@ -1,322 +1,40 @@
|
|||||||
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
|
// layouts
|
||||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||||
// components
|
// 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 { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option";
|
||||||
import { CreateUpdateViewModal } from "components/views";
|
import { WorkspaceViewIssues } from "components/issues/workspace-views/workpace-view-issues";
|
||||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
|
||||||
// ui
|
// ui
|
||||||
import { EmptyState, PrimaryButton } from "components/ui";
|
import { PrimaryButton } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
import { CheckCircle } from "lucide-react";
|
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 WorkspaceView = () => (
|
||||||
const [createViewModal, setCreateViewModal] = useState<any>(null);
|
<WorkspaceAuthorizationLayout
|
||||||
|
breadcrumbs={
|
||||||
// create issue modal
|
<div className="flex gap-2 items-center">
|
||||||
const [createIssueModal, setCreateIssueModal] = useState(false);
|
<CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" />
|
||||||
const [preloadedData, setPreloadedData] = useState<
|
<span className="text-sm font-medium">Workspace issues</span>
|
||||||
(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>
|
</div>
|
||||||
</WorkspaceAuthorizationLayout>
|
}
|
||||||
);
|
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>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<WorkspaceViewIssues />
|
||||||
|
</WorkspaceAuthorizationLayout>
|
||||||
|
);
|
||||||
|
|
||||||
export default WorkspaceView;
|
export default WorkspaceView;
|
||||||
|
@ -1,283 +1,41 @@
|
|||||||
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
|
// layouts
|
||||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||||
// components
|
import { PrimaryButton } from "components/ui";
|
||||||
import { FiltersList, SpreadsheetView } from "components/core";
|
// component
|
||||||
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 { WorkspaceAllIssue } from "components/issues/workspace-views/workspace-all-issue";
|
||||||
import { CreateUpdateIssueModal, DeleteIssueModal, MyIssuesViewOptions } from "components/issues";
|
|
||||||
// ui
|
|
||||||
import { EmptyState, PrimaryButton } from "components/ui";
|
|
||||||
// icons
|
// icons
|
||||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
import { CheckCircle } from "lucide-react";
|
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 WorkspaceViewAllIssue = () => (
|
||||||
const [createViewModal, setCreateViewModal] = useState<any>(null);
|
<WorkspaceAuthorizationLayout
|
||||||
|
breadcrumbs={
|
||||||
// create issue modal
|
<div className="flex gap-2 items-center">
|
||||||
const [createIssueModal, setCreateIssueModal] = useState(false);
|
<CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" />
|
||||||
const [preloadedData, setPreloadedData] = useState<
|
<span className="text-sm font-medium">Workspace issues</span>
|
||||||
(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>
|
</div>
|
||||||
</WorkspaceAuthorizationLayout>
|
}
|
||||||
);
|
right={
|
||||||
};
|
<div className="flex items-center gap-2">
|
||||||
|
{/* <MyIssuesViewOptions /> */}
|
||||||
|
<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>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<WorkspaceAllIssue />
|
||||||
|
</WorkspaceAuthorizationLayout>
|
||||||
|
);
|
||||||
|
|
||||||
export default WorkspaceViewAllIssue;
|
export default WorkspaceViewAllIssue;
|
||||||
|
@ -1,205 +1,40 @@
|
|||||||
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
|
// layouts
|
||||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||||
// components
|
// 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 { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option";
|
||||||
import { CreateUpdateViewModal } from "components/views";
|
import { WorkspaceAssignedIssue } from "components/issues/workspace-views/workspace-assigned-issue";
|
||||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
|
||||||
// ui
|
// ui
|
||||||
import { EmptyState, PrimaryButton } from "components/ui";
|
import { PrimaryButton } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
import { CheckCircle } from "lucide-react";
|
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 WorkspaceViewAssignedIssue: React.FC = () => (
|
||||||
const [createViewModal, setCreateViewModal] = useState<any>(null);
|
<WorkspaceAuthorizationLayout
|
||||||
|
breadcrumbs={
|
||||||
// create issue modal
|
<div className="flex gap-2 items-center">
|
||||||
const [createIssueModal, setCreateIssueModal] = useState(false);
|
<CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" />
|
||||||
const [preloadedData, setPreloadedData] = useState<
|
<span className="text-sm font-medium">Workspace Issues</span>
|
||||||
(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>
|
</div>
|
||||||
</WorkspaceAuthorizationLayout>
|
}
|
||||||
);
|
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>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<WorkspaceAssignedIssue />
|
||||||
|
</WorkspaceAuthorizationLayout>
|
||||||
|
);
|
||||||
|
|
||||||
export default WorkspaceViewAssignedIssue;
|
export default WorkspaceViewAssignedIssue;
|
||||||
|
@ -1,205 +1,40 @@
|
|||||||
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
|
// layouts
|
||||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||||
// components
|
// 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 { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option";
|
||||||
import { CreateUpdateViewModal } from "components/views";
|
import { WorkspaceCreatedIssues } from "components/issues/workspace-views/workspace-created-issues";
|
||||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
|
||||||
// ui
|
// ui
|
||||||
import { EmptyState, PrimaryButton } from "components/ui";
|
import { PrimaryButton } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
import { CheckCircle } from "lucide-react";
|
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 WorkspaceViewCreatedIssue: React.FC = () => (
|
||||||
const [createViewModal, setCreateViewModal] = useState<any>(null);
|
<WorkspaceAuthorizationLayout
|
||||||
|
breadcrumbs={
|
||||||
// create issue modal
|
<div className="flex gap-2 items-center">
|
||||||
const [createIssueModal, setCreateIssueModal] = useState(false);
|
<CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" />
|
||||||
const [preloadedData, setPreloadedData] = useState<
|
<span className="text-sm font-medium">Workspace Issues</span>
|
||||||
(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>
|
</div>
|
||||||
</WorkspaceAuthorizationLayout>
|
}
|
||||||
);
|
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>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<WorkspaceCreatedIssues />
|
||||||
|
</WorkspaceAuthorizationLayout>
|
||||||
|
);
|
||||||
|
|
||||||
export default WorkspaceViewCreatedIssue;
|
export default WorkspaceViewCreatedIssue;
|
||||||
|
@ -8,13 +8,13 @@ import useSWR from "swr";
|
|||||||
|
|
||||||
// services
|
// services
|
||||||
import workspaceService from "services/workspace.service";
|
import workspaceService from "services/workspace.service";
|
||||||
// hooks
|
|
||||||
import useUser from "hooks/use-user";
|
|
||||||
// layouts
|
// layouts
|
||||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||||
// components
|
// components
|
||||||
import { CreateUpdateViewModal, DeleteViewModal, SingleViewItem } from "components/views";
|
import { SingleViewItem } from "components/views";
|
||||||
import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option";
|
import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option";
|
||||||
|
import { CreateUpdateWorkspaceViewModal } from "components/workspace/views/modal";
|
||||||
|
import { DeleteWorkspaceViewModal } from "components/workspace/views/delete-workspace-view-modal";
|
||||||
// ui
|
// ui
|
||||||
import { EmptyState, Input, Loader, PrimaryButton } from "components/ui";
|
import { EmptyState, Input, Loader, PrimaryButton } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -25,7 +25,7 @@ import { PhotoFilterOutlined } from "@mui/icons-material";
|
|||||||
import emptyView from "public/empty-state/view.svg";
|
import emptyView from "public/empty-state/view.svg";
|
||||||
// types
|
// types
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
import { IView } from "types";
|
import { IWorkspaceView } from "types/workspace-views";
|
||||||
// constants
|
// constants
|
||||||
import { WORKSPACE_VIEWS_LIST } from "constants/fetch-keys";
|
import { WORKSPACE_VIEWS_LIST } from "constants/fetch-keys";
|
||||||
// helper
|
// helper
|
||||||
@ -35,16 +35,14 @@ const WorkspaceViews: NextPage = () => {
|
|||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
const [createUpdateViewModal, setCreateUpdateViewModal] = useState(false);
|
const [createUpdateViewModal, setCreateUpdateViewModal] = useState(false);
|
||||||
const [selectedViewToUpdate, setSelectedViewToUpdate] = useState<IView | null>(null);
|
const [selectedViewToUpdate, setSelectedViewToUpdate] = useState<IWorkspaceView | null>(null);
|
||||||
|
|
||||||
const [deleteViewModal, setDeleteViewModal] = useState(false);
|
const [deleteViewModal, setDeleteViewModal] = useState(false);
|
||||||
const [selectedViewToDelete, setSelectedViewToDelete] = useState<IView | null>(null);
|
const [selectedViewToDelete, setSelectedViewToDelete] = useState<IWorkspaceView | null>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const { user } = useUser();
|
|
||||||
|
|
||||||
const { data: workspaceViews } = useSWR(
|
const { data: workspaceViews } = useSWR(
|
||||||
workspaceSlug ? WORKSPACE_VIEWS_LIST(workspaceSlug as string) : null,
|
workspaceSlug ? WORKSPACE_VIEWS_LIST(workspaceSlug as string) : null,
|
||||||
workspaceSlug ? () => workspaceService.getAllViews(workspaceSlug as string) : null
|
workspaceSlug ? () => workspaceService.getAllViews(workspaceSlug as string) : null
|
||||||
@ -85,12 +83,12 @@ const WorkspaceViews: NextPage = () => {
|
|||||||
? workspaceViews
|
? workspaceViews
|
||||||
: workspaceViews?.filter((option) => option.name.toLowerCase().includes(query.toLowerCase()));
|
: workspaceViews?.filter((option) => option.name.toLowerCase().includes(query.toLowerCase()));
|
||||||
|
|
||||||
const handleEditView = (view: IView) => {
|
const handleEditView = (view: IWorkspaceView) => {
|
||||||
setSelectedViewToUpdate(view);
|
setSelectedViewToUpdate(view);
|
||||||
setCreateUpdateViewModal(true);
|
setCreateUpdateViewModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteView = (view: IView) => {
|
const handleDeleteView = (view: IWorkspaceView) => {
|
||||||
setSelectedViewToDelete(view);
|
setSelectedViewToDelete(view);
|
||||||
setDeleteViewModal(true);
|
setDeleteViewModal(true);
|
||||||
};
|
};
|
||||||
@ -116,22 +114,18 @@ const WorkspaceViews: NextPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<CreateUpdateViewModal
|
<CreateUpdateWorkspaceViewModal
|
||||||
isOpen={createUpdateViewModal}
|
isOpen={createUpdateViewModal}
|
||||||
handleClose={() => {
|
handleClose={() => {
|
||||||
setCreateUpdateViewModal(false);
|
setCreateUpdateViewModal(false);
|
||||||
setSelectedViewToUpdate(null);
|
setSelectedViewToUpdate(null);
|
||||||
}}
|
}}
|
||||||
data={selectedViewToUpdate}
|
data={selectedViewToUpdate}
|
||||||
viewType="workspace"
|
|
||||||
user={user}
|
|
||||||
/>
|
/>
|
||||||
<DeleteViewModal
|
<DeleteWorkspaceViewModal
|
||||||
isOpen={deleteViewModal}
|
isOpen={deleteViewModal}
|
||||||
data={selectedViewToDelete}
|
data={selectedViewToDelete}
|
||||||
setIsOpen={setDeleteViewModal}
|
setIsOpen={setDeleteViewModal}
|
||||||
viewType="workspace"
|
|
||||||
user={user}
|
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||||
|
@ -1,205 +1,40 @@
|
|||||||
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
|
// layouts
|
||||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||||
// components
|
// 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 { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option";
|
||||||
import { CreateUpdateViewModal } from "components/views";
|
import { WorkspaceSubscribedIssues } from "components/issues/workspace-views/workspace-subscribed-issue";
|
||||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
|
||||||
// ui
|
// ui
|
||||||
import { EmptyState, PrimaryButton } from "components/ui";
|
import { PrimaryButton } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
import { CheckCircle } from "lucide-react";
|
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 WorkspaceViewSubscribedIssue: React.FC = () => (
|
||||||
const [createViewModal, setCreateViewModal] = useState<any>(null);
|
<WorkspaceAuthorizationLayout
|
||||||
|
breadcrumbs={
|
||||||
// create issue modal
|
<div className="flex gap-2 items-center">
|
||||||
const [createIssueModal, setCreateIssueModal] = useState(false);
|
<CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" />
|
||||||
const [preloadedData, setPreloadedData] = useState<
|
<span className="text-sm font-medium">Workspace Issue</span>
|
||||||
(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>
|
</div>
|
||||||
</WorkspaceAuthorizationLayout>
|
}
|
||||||
);
|
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>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<WorkspaceSubscribedIssues />
|
||||||
|
</WorkspaceAuthorizationLayout>
|
||||||
|
);
|
||||||
|
|
||||||
export default WorkspaceViewSubscribedIssue;
|
export default WorkspaceViewSubscribedIssue;
|
||||||
|
@ -14,9 +14,9 @@ import {
|
|||||||
ICurrentUserResponse,
|
ICurrentUserResponse,
|
||||||
IWorkspaceBulkInviteFormData,
|
IWorkspaceBulkInviteFormData,
|
||||||
IWorkspaceViewProps,
|
IWorkspaceViewProps,
|
||||||
IView,
|
IWorkspaceViewIssuesParams,
|
||||||
IIssueFilterOptions,
|
|
||||||
} from "types";
|
} from "types";
|
||||||
|
import { IWorkspaceView } from "types/workspace-views";
|
||||||
|
|
||||||
class WorkspaceService extends APIService {
|
class WorkspaceService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -264,7 +264,7 @@ class WorkspaceService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createView(workspaceSlug: string, data: IView): Promise<any> {
|
async createView(workspaceSlug: string, data: IWorkspaceView): Promise<any> {
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/views/`, data)
|
return this.post(`/api/workspaces/${workspaceSlug}/views/`, data)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -272,7 +272,11 @@ class WorkspaceService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateView(workspaceSlug: string, viewId: string, data: Partial<IView>): Promise<any> {
|
async updateView(
|
||||||
|
workspaceSlug: string,
|
||||||
|
viewId: string,
|
||||||
|
data: Partial<IWorkspaceView>
|
||||||
|
): Promise<any> {
|
||||||
return this.patch(`/api/workspaces/${workspaceSlug}/views/${viewId}/`, data)
|
return this.patch(`/api/workspaces/${workspaceSlug}/views/${viewId}/`, data)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -288,7 +292,7 @@ class WorkspaceService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllViews(workspaceSlug: string): Promise<IView[]> {
|
async getAllViews(workspaceSlug: string): Promise<IWorkspaceView[]> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/views/`)
|
return this.get(`/api/workspaces/${workspaceSlug}/views/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -296,7 +300,7 @@ class WorkspaceService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getViewDetails(workspaceSlug: string, viewId: string): Promise<IView> {
|
async getViewDetails(workspaceSlug: string, viewId: string): Promise<IWorkspaceView> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/views/${viewId}/`)
|
return this.get(`/api/workspaces/${workspaceSlug}/views/${viewId}/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -304,7 +308,7 @@ class WorkspaceService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getViewIssues(workspaceSlug: string, params: IIssueFilterOptions): Promise<any> {
|
async getViewIssues(workspaceSlug: string, params: IWorkspaceViewIssuesParams): Promise<any> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/issues/`, {
|
return this.get(`/api/workspaces/${workspaceSlug}/issues/`, {
|
||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
|
62
web/types/view-props.d.ts
vendored
62
web/types/view-props.d.ts
vendored
@ -1,3 +1,5 @@
|
|||||||
|
import { Properties } from "./workspace";
|
||||||
|
|
||||||
export type TIssueViewOptions = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt_chart";
|
export type TIssueViewOptions = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt_chart";
|
||||||
|
|
||||||
export type TIssueGroupByOptions =
|
export type TIssueGroupByOptions =
|
||||||
@ -12,19 +14,22 @@ export type TIssueGroupByOptions =
|
|||||||
|
|
||||||
export type TIssueOrderByOptions =
|
export type TIssueOrderByOptions =
|
||||||
| "-created_at"
|
| "-created_at"
|
||||||
|
| "created_at"
|
||||||
|
| "updated_at"
|
||||||
| "-updated_at"
|
| "-updated_at"
|
||||||
| "priority"
|
| "priority"
|
||||||
|
| "-priority"
|
||||||
| "sort_order"
|
| "sort_order"
|
||||||
| "state__name"
|
| "state__name"
|
||||||
| "-state__name"
|
| "-state__name"
|
||||||
| "assignees__name"
|
| "assignees__first_name"
|
||||||
| "-assignees__name"
|
| "-assignees__first_name"
|
||||||
| "labels__name"
|
| "labels__name"
|
||||||
| "-labels__name"
|
| "-labels__name"
|
||||||
| "target_date"
|
| "target_date"
|
||||||
| "-target_date"
|
| "-target_date"
|
||||||
| "estimate__point"
|
| "estimate_point"
|
||||||
| "-estimate__point"
|
| "-estimate_point"
|
||||||
| "start_date"
|
| "start_date"
|
||||||
| "-start_date";
|
| "-start_date";
|
||||||
|
|
||||||
@ -33,12 +38,11 @@ export interface IIssueFilterOptions {
|
|||||||
created_by?: string[] | null;
|
created_by?: string[] | null;
|
||||||
labels?: string[] | null;
|
labels?: string[] | null;
|
||||||
priority?: string[] | null;
|
priority?: string[] | null;
|
||||||
start_date?: string[] | null;
|
start_date?: TStateGroups[] | null;
|
||||||
state?: string[] | null;
|
state?: string[] | null;
|
||||||
state_group?: string[] | null;
|
state_group?: TStateGroups[] | null;
|
||||||
subscriber?: string[] | null;
|
subscriber?: string[] | null;
|
||||||
target_date?: string[] | null;
|
target_date?: string[] | null;
|
||||||
project?: string[] | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IIssueDisplayFilterOptions {
|
export interface IIssueDisplayFilterOptions {
|
||||||
@ -51,13 +55,53 @@ export interface IIssueDisplayFilterOptions {
|
|||||||
type?: "active" | "backlog" | null;
|
type?: "active" | "backlog" | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IWorkspaceIssueFilterOptions {
|
||||||
|
assignees?: string[] | null;
|
||||||
|
created_by?: string[] | null;
|
||||||
|
labels?: string[] | null;
|
||||||
|
priority?: string[] | null;
|
||||||
|
state_group?: string[] | null;
|
||||||
|
subscriber?: string[] | null;
|
||||||
|
start_date?: string[] | null;
|
||||||
|
target_date?: string[] | null;
|
||||||
|
project?: string[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWorkspaceGlobalViewDisplayFilterOptions {
|
||||||
|
order_by?: string | undefined;
|
||||||
|
type?: "active" | "backlog" | null;
|
||||||
|
sub_issue?: boolean;
|
||||||
|
layout?: TIssueViewOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWorkspaceViewIssuesParams {
|
||||||
|
assignees?: string | undefined;
|
||||||
|
created_by?: string | undefined;
|
||||||
|
labels?: string | undefined;
|
||||||
|
priority?: string | undefined;
|
||||||
|
start_date?: string | undefined;
|
||||||
|
state?: string | undefined;
|
||||||
|
state_group?: string | undefined;
|
||||||
|
subscriber?: string | undefined;
|
||||||
|
target_date?: string | undefined;
|
||||||
|
project?: string | undefined;
|
||||||
|
order_by?: string | undefined;
|
||||||
|
type?: "active" | "backlog" | undefined;
|
||||||
|
sub_issue?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IProjectViewProps {
|
export interface IProjectViewProps {
|
||||||
display_filters: IIssueDisplayFilterOptions | undefined;
|
display_filters: IIssueDisplayFilterOptions | undefined;
|
||||||
filters: IIssueFilterOptions;
|
filters: IIssueFilterOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IWorkspaceViewProps {
|
export interface IWorkspaceViewProps {
|
||||||
display_filters: IIssueDisplayFilterOptions | undefined;
|
|
||||||
display_properties: Properties | undefined;
|
|
||||||
filters: IIssueFilterOptions;
|
filters: IIssueFilterOptions;
|
||||||
|
display_filters: IIssueDisplayFilterOptions | undefined;
|
||||||
|
display_properties: Properties;
|
||||||
|
}
|
||||||
|
export interface IWorkspaceGlobalViewProps {
|
||||||
|
filters: IWorkspaceIssueFilterOptions;
|
||||||
|
display_filters: IWorkspaceIssueDisplayFilterOptions | undefined;
|
||||||
|
display_properties: Properties | undefined;
|
||||||
}
|
}
|
||||||
|
12
web/types/views.d.ts
vendored
12
web/types/views.d.ts
vendored
@ -1,5 +1,3 @@
|
|||||||
import { IIssueFilterOptions } from "./view-props";
|
|
||||||
|
|
||||||
export interface IView {
|
export interface IView {
|
||||||
id: string;
|
id: string;
|
||||||
access: string;
|
access: string;
|
||||||
@ -10,15 +8,10 @@ export interface IView {
|
|||||||
updated_by: string;
|
updated_by: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
query: IIssueFilterOptions;
|
query: IQuery;
|
||||||
query_data: IIssueFilterOptions;
|
query_data: IQuery;
|
||||||
project: string;
|
project: string;
|
||||||
workspace: string;
|
workspace: string;
|
||||||
workspace_detail: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IQuery {
|
export interface IQuery {
|
||||||
@ -30,5 +23,4 @@ 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;
|
|
||||||
}
|
}
|
||||||
|
22
web/types/workspace-views.d.ts
vendored
Normal file
22
web/types/workspace-views.d.ts
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { IWorkspaceGlobalViewProps } from "./view-props";
|
||||||
|
|
||||||
|
export interface IWorkspaceView {
|
||||||
|
id: string;
|
||||||
|
access: string;
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
is_favorite: boolean;
|
||||||
|
created_by: string;
|
||||||
|
updated_by: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
query: IWorkspaceGlobalViewProps;
|
||||||
|
query_data: IWorkspaceGlobalViewProps;
|
||||||
|
project: string;
|
||||||
|
workspace: string;
|
||||||
|
workspace_detail?: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user