forked from github/plane
chore: update favorite projects endpoint (#1522)
* chore: upate favorite projects list endpoint * chore: add project button on the sidebar * refactor: sidebar favorite projects workflow
This commit is contained in:
parent
7361657660
commit
c9a5893c3f
@ -7,7 +7,6 @@ import useSWR, { mutate } from "swr";
|
|||||||
// headless ui
|
// headless ui
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import projectService from "services/project.service";
|
|
||||||
import modulesService from "services/modules.service";
|
import modulesService from "services/modules.service";
|
||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
import inboxServices from "services/inbox.service";
|
import inboxServices from "services/inbox.service";
|
||||||
@ -18,6 +17,7 @@ import useCalendarIssuesView from "hooks/use-calendar-issues-view";
|
|||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useInboxView from "hooks/use-inbox-view";
|
import useInboxView from "hooks/use-inbox-view";
|
||||||
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||||
|
import useProjects from "hooks/use-projects";
|
||||||
// components
|
// components
|
||||||
import { IssueForm } from "components/issues";
|
import { IssueForm } from "components/issues";
|
||||||
// types
|
// types
|
||||||
@ -27,7 +27,6 @@ import {
|
|||||||
PROJECT_ISSUES_DETAILS,
|
PROJECT_ISSUES_DETAILS,
|
||||||
PROJECT_ISSUES_LIST,
|
PROJECT_ISSUES_LIST,
|
||||||
USER_ISSUE,
|
USER_ISSUE,
|
||||||
PROJECTS_LIST,
|
|
||||||
SUB_ISSUES,
|
SUB_ISSUES,
|
||||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
CYCLE_ISSUES_WITH_PARAMS,
|
CYCLE_ISSUES_WITH_PARAMS,
|
||||||
@ -83,6 +82,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
const { params: spreadsheetParams } = useSpreadsheetIssuesView();
|
const { params: spreadsheetParams } = useSpreadsheetIssuesView();
|
||||||
|
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
const { projects } = useProjects();
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string };
|
if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string };
|
||||||
@ -102,11 +103,6 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: projects } = useSWR(
|
|
||||||
workspaceSlug ? PROJECTS_LIST(workspaceSlug as string) : null,
|
|
||||||
workspaceSlug ? () => projectService.getProjects(workspaceSlug as string) : null
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (projects && projects.length > 0)
|
if (projects && projects.length > 0)
|
||||||
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
|
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
|
||||||
|
@ -1,15 +1,9 @@
|
|||||||
import { useRouter } from "next/router";
|
// hooks
|
||||||
|
import useProjects from "hooks/use-projects";
|
||||||
import useSWR from "swr";
|
|
||||||
|
|
||||||
// ui
|
// ui
|
||||||
import { CustomSelect } from "components/ui";
|
import { CustomSelect } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { ClipboardDocumentListIcon } from "@heroicons/react/24/outline";
|
import { ClipboardDocumentListIcon } from "@heroicons/react/24/outline";
|
||||||
// services
|
|
||||||
import projectService from "services/project.service";
|
|
||||||
// fetch-keys
|
|
||||||
import { PROJECTS_LIST } from "constants/fetch-keys";
|
|
||||||
|
|
||||||
export interface IssueProjectSelectProps {
|
export interface IssueProjectSelectProps {
|
||||||
value: string;
|
value: string;
|
||||||
@ -22,14 +16,7 @@ export const IssueProjectSelect: React.FC<IssueProjectSelectProps> = ({
|
|||||||
onChange,
|
onChange,
|
||||||
setActiveProject,
|
setActiveProject,
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const { projects } = useProjects();
|
||||||
const { workspaceSlug } = router.query;
|
|
||||||
|
|
||||||
// Fetching Projects List
|
|
||||||
const { data: projects } = useSWR(
|
|
||||||
workspaceSlug ? PROJECTS_LIST(workspaceSlug as string) : null,
|
|
||||||
() => (workspaceSlug ? projectService.getProjects(workspaceSlug as string) : null)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
|
@ -123,7 +123,7 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
|
|||||||
.createProject(workspaceSlug as string, payload, user)
|
.createProject(workspaceSlug as string, payload, user)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
mutate<IProject[]>(
|
mutate<IProject[]>(
|
||||||
PROJECTS_LIST(workspaceSlug as string),
|
PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }),
|
||||||
(prevData) => [res, ...(prevData ?? [])],
|
(prevData) => [res, ...(prevData ?? [])],
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
|
|
||||||
@ -13,7 +15,7 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
|||||||
// ui
|
// ui
|
||||||
import { DangerButton, Input, SecondaryButton } from "components/ui";
|
import { DangerButton, Input, SecondaryButton } from "components/ui";
|
||||||
// types
|
// types
|
||||||
import type { ICurrentUserResponse, IProject, IWorkspace } from "types";
|
import type { ICurrentUserResponse, IProject } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECTS_LIST } from "constants/fetch-keys";
|
import { PROJECTS_LIST } from "constants/fetch-keys";
|
||||||
|
|
||||||
@ -37,7 +39,8 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
|
|||||||
const [confirmDeleteMyProject, setConfirmDeleteMyProject] = useState(false);
|
const [confirmDeleteMyProject, setConfirmDeleteMyProject] = useState(false);
|
||||||
const [selectedProject, setSelectedProject] = useState<IProject | null>(null);
|
const [selectedProject, setSelectedProject] = useState<IProject | null>(null);
|
||||||
|
|
||||||
const workspaceSlug = (data?.workspace as IWorkspace)?.slug;
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -64,26 +67,31 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDeletion = async () => {
|
const handleDeletion = async () => {
|
||||||
setIsDeleteLoading(true);
|
|
||||||
if (!data || !workspaceSlug || !canDelete) return;
|
if (!data || !workspaceSlug || !canDelete) return;
|
||||||
|
|
||||||
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
|
if (data.is_favorite)
|
||||||
|
mutate<IProject[]>(
|
||||||
|
PROJECTS_LIST(workspaceSlug as string, { is_favorite: true }),
|
||||||
|
(prevData) => prevData?.filter((project: IProject) => project.id !== data.id),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
mutate<IProject[]>(
|
||||||
|
PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }),
|
||||||
|
(prevData) => prevData?.filter((project: IProject) => project.id !== data.id),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
await projectService
|
await projectService
|
||||||
.deleteProject(workspaceSlug, data.id, user)
|
.deleteProject(workspaceSlug as string, data.id, user)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
handleClose();
|
handleClose();
|
||||||
mutate<IProject[]>(PROJECTS_LIST(workspaceSlug), (prevData) =>
|
|
||||||
prevData?.filter((project: IProject) => project.id !== data.id)
|
|
||||||
);
|
|
||||||
if (onSuccess) onSuccess();
|
if (onSuccess) onSuccess();
|
||||||
setToastAlert({
|
|
||||||
title: "Success",
|
|
||||||
type: "success",
|
|
||||||
message: "Project deleted successfully",
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch(() => setIsDeleteLoading(false));
|
||||||
console.log(error);
|
|
||||||
setIsDeleteLoading(false);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -2,26 +2,19 @@ import React, { useState, FC } from "react";
|
|||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR, { mutate } from "swr";
|
|
||||||
|
|
||||||
// icons
|
|
||||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useTheme from "hooks/use-theme";
|
import useTheme from "hooks/use-theme";
|
||||||
import useUserAuth from "hooks/use-user-auth";
|
import useUserAuth from "hooks/use-user-auth";
|
||||||
// services
|
import useProjects from "hooks/use-projects";
|
||||||
import projectService from "services/project.service";
|
|
||||||
// components
|
// components
|
||||||
import { CreateProjectModal, DeleteProjectModal, SingleSidebarProject } from "components/project";
|
import { DeleteProjectModal, SingleSidebarProject } from "components/project";
|
||||||
// ui
|
// icons
|
||||||
import { Loader } from "components/ui";
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IFavoriteProject, IProject } from "types";
|
import { IProject } from "types";
|
||||||
// fetch-keys
|
|
||||||
import { FAVORITE_PROJECTS_LIST, PROJECTS_LIST } from "constants/fetch-keys";
|
|
||||||
|
|
||||||
export const ProjectSidebarList: FC = () => {
|
export const ProjectSidebarList: FC = () => {
|
||||||
const [deleteProjectModal, setDeleteProjectModal] = useState(false);
|
const [deleteProjectModal, setDeleteProjectModal] = useState(false);
|
||||||
@ -33,93 +26,11 @@ export const ProjectSidebarList: FC = () => {
|
|||||||
|
|
||||||
const { user } = useUserAuth();
|
const { user } = useUserAuth();
|
||||||
|
|
||||||
// states
|
|
||||||
const [isCreateProjectModal, setCreateProjectModal] = useState(false);
|
|
||||||
// theme
|
|
||||||
const { collapsed: sidebarCollapse } = useTheme();
|
const { collapsed: sidebarCollapse } = useTheme();
|
||||||
// toast handler
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const { data: favoriteProjects } = useSWR(
|
const { projects: favoriteProjects } = useProjects(true);
|
||||||
workspaceSlug ? FAVORITE_PROJECTS_LIST(workspaceSlug.toString()) : null,
|
const { projects: allProjects } = useProjects();
|
||||||
() => (workspaceSlug ? projectService.getFavoriteProjects(workspaceSlug.toString()) : null)
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: projects } = useSWR(
|
|
||||||
workspaceSlug ? PROJECTS_LIST(workspaceSlug as string) : null,
|
|
||||||
() => (workspaceSlug ? projectService.getProjects(workspaceSlug as string) : null)
|
|
||||||
);
|
|
||||||
const normalProjects = projects?.filter((p) => !p.is_favorite) ?? [];
|
|
||||||
|
|
||||||
const handleAddToFavorites = (project: IProject) => {
|
|
||||||
if (!workspaceSlug) return;
|
|
||||||
|
|
||||||
projectService
|
|
||||||
.addProjectToFavorites(workspaceSlug as string, {
|
|
||||||
project: project.id,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
mutate<IProject[]>(
|
|
||||||
PROJECTS_LIST(workspaceSlug as string),
|
|
||||||
(prevData) =>
|
|
||||||
(prevData ?? []).map((p) => ({
|
|
||||||
...p,
|
|
||||||
is_favorite: p.id === project.id ? true : p.is_favorite,
|
|
||||||
})),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
mutate(FAVORITE_PROJECTS_LIST(workspaceSlug as string));
|
|
||||||
|
|
||||||
setToastAlert({
|
|
||||||
type: "success",
|
|
||||||
title: "Success!",
|
|
||||||
message: "Successfully added the project to favorites.",
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "Couldn't remove the project from favorites. Please try again.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveFromFavorites = (project: IProject) => {
|
|
||||||
if (!workspaceSlug) return;
|
|
||||||
|
|
||||||
projectService
|
|
||||||
.removeProjectFromFavorites(workspaceSlug as string, project.id)
|
|
||||||
.then(() => {
|
|
||||||
mutate<IProject[]>(
|
|
||||||
PROJECTS_LIST(workspaceSlug as string),
|
|
||||||
(prevData) =>
|
|
||||||
(prevData ?? []).map((p) => ({
|
|
||||||
...p,
|
|
||||||
is_favorite: p.id === project.id ? false : p.is_favorite,
|
|
||||||
})),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
mutate<IFavoriteProject[]>(
|
|
||||||
FAVORITE_PROJECTS_LIST(workspaceSlug as string),
|
|
||||||
(prevData) => (prevData ?? []).filter((p) => p.project !== project.id),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
setToastAlert({
|
|
||||||
type: "success",
|
|
||||||
title: "Success!",
|
|
||||||
message: "Successfully removed the project from favorites.",
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "Couldn't remove the project from favorites. Please try again.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteProject = (project: IProject) => {
|
const handleDeleteProject = (project: IProject) => {
|
||||||
setProjectToDelete(project);
|
setProjectToDelete(project);
|
||||||
@ -140,93 +51,61 @@ export const ProjectSidebarList: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreateProjectModal
|
|
||||||
isOpen={isCreateProjectModal}
|
|
||||||
setIsOpen={setCreateProjectModal}
|
|
||||||
user={user}
|
|
||||||
/>
|
|
||||||
<DeleteProjectModal
|
<DeleteProjectModal
|
||||||
isOpen={deleteProjectModal}
|
isOpen={deleteProjectModal}
|
||||||
onClose={() => setDeleteProjectModal(false)}
|
onClose={() => setDeleteProjectModal(false)}
|
||||||
data={projectToDelete}
|
data={projectToDelete}
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
<div className="h-full overflow-y-auto mt-5 px-4">
|
<div className="h-full overflow-y-auto px-4">
|
||||||
{favoriteProjects && favoriteProjects.length > 0 && (
|
{favoriteProjects && favoriteProjects.length > 0 && (
|
||||||
<div className="flex flex-col space-y-2">
|
<div className="flex flex-col space-y-2 mt-5">
|
||||||
{!sidebarCollapse && (
|
{!sidebarCollapse && (
|
||||||
<h5 className="text-sm font-medium text-custom-sidebar-text-200">Favorites</h5>
|
<h5 className="text-sm font-medium text-custom-sidebar-text-200">Favorites</h5>
|
||||||
)}
|
)}
|
||||||
{favoriteProjects.map((favoriteProject) => {
|
{favoriteProjects.map((project) => (
|
||||||
const project = favoriteProject.project_detail;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SingleSidebarProject
|
<SingleSidebarProject
|
||||||
key={project.id}
|
key={project.id}
|
||||||
project={project}
|
project={project}
|
||||||
sidebarCollapse={sidebarCollapse}
|
sidebarCollapse={sidebarCollapse}
|
||||||
handleDeleteProject={() => handleDeleteProject(project)}
|
handleDeleteProject={() => handleDeleteProject(project)}
|
||||||
handleCopyText={() => handleCopyText(project.id)}
|
handleCopyText={() => handleCopyText(project.id)}
|
||||||
handleRemoveFromFavorites={() => handleRemoveFromFavorites(project)}
|
shortContextMenu
|
||||||
/>
|
/>
|
||||||
);
|
))}
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{allProjects && allProjects.length > 0 && (
|
||||||
<div className="flex flex-col space-y-2 mt-5">
|
<div className="flex flex-col space-y-2 mt-5">
|
||||||
{!sidebarCollapse && (
|
{!sidebarCollapse && (
|
||||||
<h5 className="text-sm font-medium text-custom-sidebar-text-200">Projects</h5>
|
<h5 className="text-sm font-medium text-custom-sidebar-text-200">Projects</h5>
|
||||||
)}
|
)}
|
||||||
{projects ? (
|
{allProjects.map((project) => (
|
||||||
<>
|
|
||||||
{normalProjects.length > 0 ? (
|
|
||||||
normalProjects.map((project) => (
|
|
||||||
<SingleSidebarProject
|
<SingleSidebarProject
|
||||||
key={project.id}
|
key={project.id}
|
||||||
project={project}
|
project={project}
|
||||||
sidebarCollapse={sidebarCollapse}
|
sidebarCollapse={sidebarCollapse}
|
||||||
handleDeleteProject={() => handleDeleteProject(project)}
|
handleDeleteProject={() => handleDeleteProject(project)}
|
||||||
handleCopyText={() => handleCopyText(project.id)}
|
handleCopyText={() => handleCopyText(project.id)}
|
||||||
handleAddToFavorites={() => handleAddToFavorites(project)}
|
|
||||||
/>
|
/>
|
||||||
))
|
))}
|
||||||
) : (
|
</div>
|
||||||
<div className="space-y-3 text-center">
|
|
||||||
{!sidebarCollapse && (
|
|
||||||
<h4 className="text-sm text-custom-text-200">
|
|
||||||
You don{"'"}t have any project yet
|
|
||||||
</h4>
|
|
||||||
)}
|
)}
|
||||||
|
{allProjects && allProjects.length === 0 && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="group flex w-full items-center justify-center gap-2 rounded-md bg-custom-background-80 p-2 text-xs text-custom-text-100"
|
className="flex w-full items-center gap-2 px-3 py-2 text-sm text-custom-sidebar-text-200 mt-5"
|
||||||
onClick={() => setCreateProjectModal(true)}
|
onClick={() => {
|
||||||
|
const e = new KeyboardEvent("keydown", {
|
||||||
|
key: "p",
|
||||||
|
});
|
||||||
|
document.dispatchEvent(e);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-5 w-5" />
|
||||||
{!sidebarCollapse && "Create Project"}
|
{!sidebarCollapse && "Add Project"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="w-full">
|
|
||||||
<Loader className="space-y-5">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Loader.Item height="30px" />
|
|
||||||
<Loader.Item height="15px" width="80%" />
|
|
||||||
<Loader.Item height="15px" width="80%" />
|
|
||||||
<Loader.Item height="15px" width="80%" />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Loader.Item height="30px" />
|
|
||||||
<Loader.Item height="15px" width="80%" />
|
|
||||||
<Loader.Item height="15px" width="80%" />
|
|
||||||
<Loader.Item height="15px" width="80%" />
|
|
||||||
</div>
|
|
||||||
</Loader>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -26,9 +26,9 @@ import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
|||||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||||
import { renderEmoji } from "helpers/emoji.helper";
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
// types
|
// types
|
||||||
import type { IFavoriteProject, IProject } from "types";
|
import type { IProject } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { FAVORITE_PROJECTS_LIST, PROJECTS_LIST } from "constants/fetch-keys";
|
import { PROJECTS_LIST } from "constants/fetch-keys";
|
||||||
|
|
||||||
export type ProjectCardProps = {
|
export type ProjectCardProps = {
|
||||||
project: IProject;
|
project: IProject;
|
||||||
@ -55,22 +55,23 @@ export const SingleProjectCard: React.FC<ProjectCardProps> = ({
|
|||||||
const handleAddToFavorites = () => {
|
const handleAddToFavorites = () => {
|
||||||
if (!workspaceSlug) return;
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
|
mutate<IProject[]>(
|
||||||
|
PROJECTS_LIST(workspaceSlug as string, { is_favorite: true }),
|
||||||
|
(prevData) => [...(prevData ?? []), { ...project, is_favorite: true }],
|
||||||
|
false
|
||||||
|
);
|
||||||
|
mutate<IProject[]>(
|
||||||
|
PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }),
|
||||||
|
(prevData) =>
|
||||||
|
(prevData ?? []).map((p) => (p.id === project.id ? { ...p, is_favorite: true } : p)),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
projectService
|
projectService
|
||||||
.addProjectToFavorites(workspaceSlug as string, {
|
.addProjectToFavorites(workspaceSlug as string, {
|
||||||
project: project.id,
|
project: project.id,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
mutate<IProject[]>(
|
|
||||||
PROJECTS_LIST(workspaceSlug as string),
|
|
||||||
(prevData) =>
|
|
||||||
(prevData ?? []).map((p) => ({
|
|
||||||
...p,
|
|
||||||
is_favorite: p.id === project.id ? true : p.is_favorite,
|
|
||||||
})),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
mutate(FAVORITE_PROJECTS_LIST(workspaceSlug as string));
|
|
||||||
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
@ -89,24 +90,21 @@ export const SingleProjectCard: React.FC<ProjectCardProps> = ({
|
|||||||
const handleRemoveFromFavorites = () => {
|
const handleRemoveFromFavorites = () => {
|
||||||
if (!workspaceSlug || !project) return;
|
if (!workspaceSlug || !project) return;
|
||||||
|
|
||||||
projectService
|
|
||||||
.removeProjectFromFavorites(workspaceSlug as string, project.id)
|
|
||||||
.then(() => {
|
|
||||||
mutate<IProject[]>(
|
mutate<IProject[]>(
|
||||||
PROJECTS_LIST(workspaceSlug as string),
|
PROJECTS_LIST(workspaceSlug as string, { is_favorite: true }),
|
||||||
(prevData) =>
|
(prevData) => (prevData ?? []).filter((p) => p.id !== project.id),
|
||||||
(prevData ?? []).map((p) => ({
|
|
||||||
...p,
|
|
||||||
is_favorite: p.id === project.id ? false : p.is_favorite,
|
|
||||||
})),
|
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
mutate<IFavoriteProject[]>(
|
mutate<IProject[]>(
|
||||||
FAVORITE_PROJECTS_LIST(workspaceSlug as string),
|
PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }),
|
||||||
(prevData) => (prevData ?? []).filter((p) => p.project !== project.id),
|
(prevData) =>
|
||||||
|
(prevData ?? []).map((p) => (p.id === project.id ? { ...p, is_favorite: false } : p)),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
projectService
|
||||||
|
.removeProjectFromFavorites(workspaceSlug as string, project.id)
|
||||||
|
.then(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { mutate } from "swr";
|
||||||
|
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Disclosure, Transition } from "@headlessui/react";
|
import { Disclosure, Transition } from "@headlessui/react";
|
||||||
|
// services
|
||||||
|
import projectService from "services/project.service";
|
||||||
|
// hooks
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu, Icon, Tooltip } from "components/ui";
|
import { CustomMenu, Icon, Tooltip } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -12,14 +18,15 @@ import { truncateText } from "helpers/string.helper";
|
|||||||
import { renderEmoji } from "helpers/emoji.helper";
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
// types
|
// types
|
||||||
import { IProject } from "types";
|
import { IProject } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { PROJECTS_LIST } from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
project: IProject;
|
project: IProject;
|
||||||
sidebarCollapse: boolean;
|
sidebarCollapse: boolean;
|
||||||
handleDeleteProject: () => void;
|
handleDeleteProject: () => void;
|
||||||
handleCopyText: () => void;
|
handleCopyText: () => void;
|
||||||
handleAddToFavorites?: () => void;
|
shortContextMenu?: boolean;
|
||||||
handleRemoveFromFavorites?: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const navigation = (workspaceSlug: string, projectId: string) => [
|
const navigation = (workspaceSlug: string, projectId: string) => [
|
||||||
@ -60,12 +67,65 @@ export const SingleSidebarProject: React.FC<Props> = ({
|
|||||||
sidebarCollapse,
|
sidebarCollapse,
|
||||||
handleDeleteProject,
|
handleDeleteProject,
|
||||||
handleCopyText,
|
handleCopyText,
|
||||||
handleAddToFavorites,
|
shortContextMenu = false,
|
||||||
handleRemoveFromFavorites,
|
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const handleAddToFavorites = () => {
|
||||||
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
|
mutate<IProject[]>(
|
||||||
|
PROJECTS_LIST(workspaceSlug as string, { is_favorite: true }),
|
||||||
|
(prevData) => [...(prevData ?? []), { ...project, is_favorite: true }],
|
||||||
|
false
|
||||||
|
);
|
||||||
|
mutate<IProject[]>(
|
||||||
|
PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }),
|
||||||
|
(prevData) =>
|
||||||
|
(prevData ?? []).map((p) => (p.id === project.id ? { ...p, is_favorite: true } : p)),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
projectService
|
||||||
|
.addProjectToFavorites(workspaceSlug as string, {
|
||||||
|
project: project.id,
|
||||||
|
})
|
||||||
|
.catch(() =>
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Couldn't remove the project from favorites. Please try again.",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFromFavorites = () => {
|
||||||
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
|
mutate<IProject[]>(
|
||||||
|
PROJECTS_LIST(workspaceSlug as string, { is_favorite: true }),
|
||||||
|
(prevData) => (prevData ?? []).filter((p) => p.id !== project.id),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
mutate<IProject[]>(
|
||||||
|
PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }),
|
||||||
|
(prevData) =>
|
||||||
|
(prevData ?? []).map((p) => (p.id === project.id ? { ...p, is_favorite: false } : p)),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
projectService.removeProjectFromFavorites(workspaceSlug as string, project.id).catch(() =>
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Couldn't remove the project from favorites. Please try again.",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Disclosure key={project?.id} defaultOpen={projectId === project?.id}>
|
<Disclosure key={project?.id} defaultOpen={projectId === project?.id}>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
@ -124,13 +184,15 @@ export const SingleSidebarProject: React.FC<Props> = ({
|
|||||||
|
|
||||||
{!sidebarCollapse && (
|
{!sidebarCollapse && (
|
||||||
<CustomMenu ellipsis>
|
<CustomMenu ellipsis>
|
||||||
|
{!shortContextMenu && (
|
||||||
<CustomMenu.MenuItem onClick={handleDeleteProject}>
|
<CustomMenu.MenuItem onClick={handleDeleteProject}>
|
||||||
<span className="flex items-center justify-start gap-2 ">
|
<span className="flex items-center justify-start gap-2 ">
|
||||||
<TrashIcon className="h-4 w-4" />
|
<TrashIcon className="h-4 w-4" />
|
||||||
<span>Delete project</span>
|
<span>Delete project</span>
|
||||||
</span>
|
</span>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
{handleAddToFavorites && (
|
)}
|
||||||
|
{!project.is_favorite && (
|
||||||
<CustomMenu.MenuItem onClick={handleAddToFavorites}>
|
<CustomMenu.MenuItem onClick={handleAddToFavorites}>
|
||||||
<span className="flex items-center justify-start gap-2">
|
<span className="flex items-center justify-start gap-2">
|
||||||
<StarIcon className="h-4 w-4" />
|
<StarIcon className="h-4 w-4" />
|
||||||
@ -138,7 +200,7 @@ export const SingleSidebarProject: React.FC<Props> = ({
|
|||||||
</span>
|
</span>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
)}
|
)}
|
||||||
{handleRemoveFromFavorites && (
|
{project.is_favorite && (
|
||||||
<CustomMenu.MenuItem onClick={handleRemoveFromFavorites}>
|
<CustomMenu.MenuItem onClick={handleRemoveFromFavorites}>
|
||||||
<span className="flex items-center justify-start gap-2">
|
<span className="flex items-center justify-start gap-2">
|
||||||
<StarIcon className="h-4 w-4" />
|
<StarIcon className="h-4 w-4" />
|
||||||
|
@ -52,10 +52,18 @@ export const WORKSPACE_INVITATIONS = "WORKSPACE_INVITATIONS";
|
|||||||
export const WORKSPACE_INVITATION = "WORKSPACE_INVITATION";
|
export const WORKSPACE_INVITATION = "WORKSPACE_INVITATION";
|
||||||
export const LAST_ACTIVE_WORKSPACE_AND_PROJECTS = "LAST_ACTIVE_WORKSPACE_AND_PROJECTS";
|
export const LAST_ACTIVE_WORKSPACE_AND_PROJECTS = "LAST_ACTIVE_WORKSPACE_AND_PROJECTS";
|
||||||
|
|
||||||
export const PROJECTS_LIST = (workspaceSlug: string) =>
|
export const PROJECTS_LIST = (
|
||||||
`PROJECTS_LIST_${workspaceSlug.toUpperCase()}`;
|
workspaceSlug: string,
|
||||||
export const FAVORITE_PROJECTS_LIST = (workspaceSlug: string) =>
|
params: {
|
||||||
`FAVORITE_PROJECTS_LIST_${workspaceSlug.toUpperCase()}`;
|
is_favorite: "all" | boolean;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
if (!params) return `PROJECTS_LIST_${workspaceSlug.toUpperCase()}`;
|
||||||
|
|
||||||
|
return `PROJECTS_LIST_${workspaceSlug.toUpperCase()}_${params.is_favorite
|
||||||
|
.toString()
|
||||||
|
.toUpperCase()}`;
|
||||||
|
};
|
||||||
export const PROJECT_DETAILS = (projectId: string) => `PROJECT_DETAILS_${projectId.toUpperCase()}`;
|
export const PROJECT_DETAILS = (projectId: string) => `PROJECT_DETAILS_${projectId.toUpperCase()}`;
|
||||||
|
|
||||||
export const PROJECT_MEMBERS = (projectId: string) => `PROJECT_MEMBERS_${projectId.toUpperCase()}`;
|
export const PROJECT_MEMBERS = (projectId: string) => `PROJECT_MEMBERS_${projectId.toUpperCase()}`;
|
||||||
|
@ -11,14 +11,15 @@ import { IProject } from "types";
|
|||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECTS_LIST } from "constants/fetch-keys";
|
import { PROJECTS_LIST } from "constants/fetch-keys";
|
||||||
|
|
||||||
const useProjects = () => {
|
const useProjects = (type?: "all" | boolean) => {
|
||||||
// router
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
// api fetching
|
|
||||||
const { data: projects, mutate: mutateProjects } = useSWR(
|
const { data: projects, mutate: mutateProjects } = useSWR(
|
||||||
workspaceSlug ? PROJECTS_LIST(workspaceSlug as string) : null,
|
workspaceSlug ? PROJECTS_LIST(workspaceSlug as string, { is_favorite: type ?? "all" }) : null,
|
||||||
workspaceSlug ? () => projectService.getProjects(workspaceSlug as string) : null
|
workspaceSlug
|
||||||
|
? () => projectService.getProjects(workspaceSlug as string, { is_favorite: type ?? "all" })
|
||||||
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const recentProjects = [...(projects ?? [])]
|
const recentProjects = [...(projects ?? [])]
|
||||||
|
@ -59,7 +59,7 @@ const ControlSettings: NextPage = () => {
|
|||||||
} = useForm<IProject>({ defaultValues });
|
} = useForm<IProject>({ defaultValues });
|
||||||
|
|
||||||
const onSubmit = async (formData: IProject) => {
|
const onSubmit = async (formData: IProject) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId || !projectDetails) return;
|
||||||
|
|
||||||
const payload: Partial<IProject> = {
|
const payload: Partial<IProject> = {
|
||||||
default_assignee: formData.default_assignee,
|
default_assignee: formData.default_assignee,
|
||||||
@ -70,7 +70,19 @@ const ControlSettings: NextPage = () => {
|
|||||||
.updateProject(workspaceSlug as string, projectId as string, payload, user)
|
.updateProject(workspaceSlug as string, projectId as string, payload, user)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
mutate(PROJECT_DETAILS(projectId as string));
|
mutate(PROJECT_DETAILS(projectId as string));
|
||||||
mutate(PROJECTS_LIST(workspaceSlug as string));
|
|
||||||
|
if (projectDetails.is_favorite)
|
||||||
|
mutate(
|
||||||
|
PROJECTS_LIST(workspaceSlug as string, {
|
||||||
|
is_favorite: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
mutate(
|
||||||
|
PROJECTS_LIST(workspaceSlug as string, {
|
||||||
|
is_favorite: "all",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
|
@ -21,10 +21,10 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
|||||||
import { ContrastIcon, PeopleGroupIcon, ViewListIcon, InboxIcon } from "components/icons";
|
import { ContrastIcon, PeopleGroupIcon, ViewListIcon, InboxIcon } from "components/icons";
|
||||||
import { DocumentTextIcon } from "@heroicons/react/24/outline";
|
import { DocumentTextIcon } from "@heroicons/react/24/outline";
|
||||||
// types
|
// types
|
||||||
import { IFavoriteProject, IProject } from "types";
|
import { IProject } from "types";
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { FAVORITE_PROJECTS_LIST, PROJECTS_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
|
import { PROJECTS_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
|
||||||
|
|
||||||
const featuresList = [
|
const featuresList = [
|
||||||
{
|
{
|
||||||
@ -100,17 +100,16 @@ const FeaturesSettings: NextPage = () => {
|
|||||||
if (!workspaceSlug || !projectId || !projectDetails) return;
|
if (!workspaceSlug || !projectId || !projectDetails) return;
|
||||||
|
|
||||||
if (projectDetails.is_favorite)
|
if (projectDetails.is_favorite)
|
||||||
mutate<IFavoriteProject[]>(
|
mutate<IProject[]>(
|
||||||
FAVORITE_PROJECTS_LIST(workspaceSlug.toString()),
|
PROJECTS_LIST(workspaceSlug.toString(), {
|
||||||
|
is_favorite: true,
|
||||||
|
}),
|
||||||
(prevData) =>
|
(prevData) =>
|
||||||
prevData?.map((p) => {
|
prevData?.map((p) => {
|
||||||
if (p.project === projectId)
|
if (p.id === projectId)
|
||||||
return {
|
return {
|
||||||
...p,
|
...p,
|
||||||
project_detail: {
|
|
||||||
...p.project_detail,
|
|
||||||
...formData,
|
...formData,
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return p;
|
return p;
|
||||||
@ -119,7 +118,9 @@ const FeaturesSettings: NextPage = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
mutate<IProject[]>(
|
mutate<IProject[]>(
|
||||||
PROJECTS_LIST(workspaceSlug.toString()),
|
PROJECTS_LIST(workspaceSlug.toString(), {
|
||||||
|
is_favorite: "all",
|
||||||
|
}),
|
||||||
(prevData) =>
|
(prevData) =>
|
||||||
prevData?.map((p) => {
|
prevData?.map((p) => {
|
||||||
if (p.id === projectId)
|
if (p.id === projectId)
|
||||||
@ -147,21 +148,13 @@ const FeaturesSettings: NextPage = () => {
|
|||||||
|
|
||||||
await projectService
|
await projectService
|
||||||
.updateProject(workspaceSlug as string, projectId as string, formData, user)
|
.updateProject(workspaceSlug as string, projectId as string, formData, user)
|
||||||
.then(() => {
|
.catch(() =>
|
||||||
mutate(
|
|
||||||
projectDetails.is_favorite
|
|
||||||
? FAVORITE_PROJECTS_LIST(workspaceSlug.toString())
|
|
||||||
: PROJECTS_LIST(workspaceSlug.toString())
|
|
||||||
);
|
|
||||||
mutate(PROJECT_DETAILS(projectId as string));
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "Project feature could not be updated. Please try again.",
|
message: "Project feature could not be updated. Please try again.",
|
||||||
});
|
})
|
||||||
});
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -94,7 +94,20 @@ const GeneralSettings: NextPage = () => {
|
|||||||
(prevData) => ({ ...prevData, ...res }),
|
(prevData) => ({ ...prevData, ...res }),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
mutate(PROJECTS_LIST(workspaceSlug as string));
|
|
||||||
|
if (projectDetails.is_favorite)
|
||||||
|
mutate(
|
||||||
|
PROJECTS_LIST(workspaceSlug as string, {
|
||||||
|
is_favorite: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
mutate(
|
||||||
|
PROJECTS_LIST(workspaceSlug as string, {
|
||||||
|
is_favorite: "all",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
|
@ -6,7 +6,6 @@ import trackEventServices from "services/track-event.service";
|
|||||||
import type {
|
import type {
|
||||||
GithubRepositoriesResponse,
|
GithubRepositoriesResponse,
|
||||||
ICurrentUserResponse,
|
ICurrentUserResponse,
|
||||||
IFavoriteProject,
|
|
||||||
IProject,
|
IProject,
|
||||||
IProjectBulkInviteFormData,
|
IProjectBulkInviteFormData,
|
||||||
IProjectMember,
|
IProjectMember,
|
||||||
@ -53,8 +52,15 @@ class ProjectServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjects(workspaceSlug: string): Promise<IProject[]> {
|
async getProjects(
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/`)
|
workspaceSlug: string,
|
||||||
|
params: {
|
||||||
|
is_favorite: "all" | boolean;
|
||||||
|
}
|
||||||
|
): Promise<IProject[]> {
|
||||||
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/`, {
|
||||||
|
params,
|
||||||
|
})
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
@ -298,14 +304,6 @@ class ProjectServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFavoriteProjects(workspaceSlug: string): Promise<IFavoriteProject[]> {
|
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/user-favorite-projects/`)
|
|
||||||
.then((response) => response?.data)
|
|
||||||
.catch((error) => {
|
|
||||||
throw error?.response?.data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async addProjectToFavorites(
|
async addProjectToFavorites(
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
data: {
|
data: {
|
||||||
|
12
apps/app/types/projects.d.ts
vendored
12
apps/app/types/projects.d.ts
vendored
@ -60,18 +60,6 @@ export interface IProjectLite {
|
|||||||
identifier: string;
|
identifier: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFavoriteProject {
|
|
||||||
created_at: Date;
|
|
||||||
created_by: string;
|
|
||||||
id: string;
|
|
||||||
project: string;
|
|
||||||
project_detail: IProject;
|
|
||||||
updated_at: Date;
|
|
||||||
updated_by: string;
|
|
||||||
user: string;
|
|
||||||
workspace: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProjectViewTheme = {
|
type ProjectViewTheme = {
|
||||||
issueView: TIssueViewOptions;
|
issueView: TIssueViewOptions;
|
||||||
groupByProperty: TIssueGroupByOptions;
|
groupByProperty: TIssueGroupByOptions;
|
||||||
|
Loading…
Reference in New Issue
Block a user