refactor: project card component refactor

This commit is contained in:
sriramveeraghanta 2023-09-26 01:03:36 +05:30
parent 2b419c02a5
commit 310a2ca904
13 changed files with 457 additions and 403 deletions

View File

@ -0,0 +1,66 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
// lib
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { ProjectCard } from "components/project";
import { Loader, EmptyState } from "components/ui";
// images
import emptyProject from "public/empty-state/project.svg";
// icons
import { Plus } from "lucide-react";
export interface IProjectCardList {
workspaceSlug: string;
}
export const ProjectCardList: FC<IProjectCardList> = observer((props) => {
const { workspaceSlug } = props;
// store
const { project: projectStore } = useMobxStore();
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null;
if (!projects) {
return (
<Loader className="grid grid-cols-3 gap-4">
<Loader.Item height="100px" />
<Loader.Item height="100px" />
<Loader.Item height="100px" />
<Loader.Item height="100px" />
<Loader.Item height="100px" />
<Loader.Item height="100px" />
</Loader>
);
}
return (
<>
{projects.length > 0 ? (
<div className="h-full p-8 overflow-y-auto">
<div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
{projectStore.searchedProjects.map((project) => (
<ProjectCard key={project.id} project={project} />
))}
</div>
</div>
) : (
<EmptyState
image={emptyProject}
title="No projects yet"
description="Get started by creating your first project"
primaryButton={{
icon: <Plus className="h-4 w-4" />,
text: "New Project",
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "p",
});
document.dispatchEvent(e);
},
}}
/>
)}
</>
);
});

View File

@ -1,18 +1,16 @@
import React from "react";
import React, { useState } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
import { mutate } from "swr";
import { observer } from "mobx-react-lite";
// icons
import { CalendarDaysIcon, LinkIcon, PencilIcon, PlusIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
// services
import projectService from "services/project.service";
// hooks
import useToast from "hooks/use-toast";
// ui
import { CustomMenu, Tooltip } from "components/ui";
// icons
import { CalendarDaysIcon, LinkIcon, PencilIcon, PlusIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
// helpers
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
@ -21,18 +19,23 @@ import { renderEmoji } from "helpers/emoji.helper";
import type { IProject } from "types";
// fetch-keys
import { PROJECTS_LIST } from "constants/fetch-keys";
// components
import { DeleteProjectModal, JoinProjectModal } from "components/project";
export type ProjectCardProps = {
project: IProject;
setToJoinProject: (id: string | null) => void;
setDeleteProject: (id: string | null) => void;
};
export const SingleProjectCard: React.FC<ProjectCardProps> = ({ project, setToJoinProject, setDeleteProject }) => {
export const ProjectCard: React.FC<ProjectCardProps> = observer((props) => {
const { project } = props;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// toast
const { setToastAlert } = useToast();
// states
const [deleteProjectModalOpen, setDeleteProjectModal] = useState(false);
const [joinProjectModalOpen, setJoinProjectModal] = useState(false);
const isOwner = project.member_role === 20;
const isMember = project.member_role === 15;
@ -105,6 +108,22 @@ export const SingleProjectCard: React.FC<ProjectCardProps> = ({ project, setToJo
return (
<>
{/* Delete Project Modal */}
<DeleteProjectModal
project={project}
isOpen={deleteProjectModalOpen}
onClose={() => setDeleteProjectModal(false)}
/>
{workspaceSlug && (
<JoinProjectModal
workspaceSlug={workspaceSlug?.toString()}
project={project}
isOpen={joinProjectModalOpen}
handleClose={() => setJoinProjectModal(false)}
/>
)}
{/* Card Information */}
<div className="flex flex-col rounded-[10px] bg-custom-background-90 shadow">
<Link href={`/${workspaceSlug as string}/projects/${project.id}/issues`}>
<a>
@ -124,7 +143,7 @@ export const SingleProjectCard: React.FC<ProjectCardProps> = ({ project, setToJo
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setToJoinProject(project.id);
setJoinProjectModal(true);
}}
className="flex cursor-pointer items-center gap-1 rounded bg-green-600 px-2 py-1 text-xs"
>
@ -180,7 +199,7 @@ export const SingleProjectCard: React.FC<ProjectCardProps> = ({ project, setToJo
)}
<CustomMenu width="auto" verticalEllipsis>
{isOwner && (
<CustomMenu.MenuItem onClick={() => setDeleteProject(project.id)}>
<CustomMenu.MenuItem onClick={() => setDeleteProjectModal(true)}>
<span className="flex items-center justify-start gap-2">
<TrashIcon className="h-4 w-4" />
<span>Delete project</span>
@ -216,4 +235,4 @@ export const SingleProjectCard: React.FC<ProjectCardProps> = ({ project, setToJo
</div>
</>
);
};
});

View File

@ -3,10 +3,12 @@ export * from "./delete-project-modal";
export * from "./sidebar-list";
export * from "./settings-sidebar";
export * from "./single-integration-card";
export * from "./single-project-card";
export * from "./sidebar-list-item";
export * from "./leave-project-modal";
export * from "./member-select";
export * from "./members-select";
export * from "./label-select";
export * from "./priority-select";
export * from "./card-list";
export * from "./card";
export * from "./join-project-modal";

View File

@ -1,26 +1,32 @@
import React, { useState } from "react";
// headless ui
import { useState, Fragment } from "react";
import { Transition, Dialog } from "@headlessui/react";
// ui
import { PrimaryButton, SecondaryButton } from "components/ui";
// types
import type { IProject } from "types";
// lib
import { useMobxStore } from "lib/mobx/store-provider";
// type
type TJoinProjectModalProps = {
data?: IProject;
onClose: () => void;
onJoin: () => Promise<void>;
isOpen: boolean;
workspaceSlug: string;
project: IProject;
handleClose: () => void;
};
export const JoinProjectModal: React.FC<TJoinProjectModalProps> = ({ onClose, onJoin, data }) => {
export const JoinProjectModal: React.FC<TJoinProjectModalProps> = (props) => {
const { handleClose, isOpen, project, workspaceSlug } = props;
// store
const { project: projectStore } = useMobxStore();
// states
const [isJoiningLoading, setIsJoiningLoading] = useState(false);
const handleJoin = () => {
setIsJoiningLoading(true);
onJoin()
projectStore
.joinProject(workspaceSlug, [project.id])
.then(() => {
setIsJoiningLoading(false);
handleClose();
@ -30,15 +36,11 @@ export const JoinProjectModal: React.FC<TJoinProjectModalProps> = ({ onClose, on
});
};
const handleClose = () => {
onClose();
};
return (
<Transition.Root show={Boolean(data)} as={React.Fragment}>
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
@ -52,7 +54,7 @@ export const JoinProjectModal: React.FC<TJoinProjectModalProps> = ({ onClose, on
<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}
as={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"
@ -62,15 +64,11 @@ export const JoinProjectModal: React.FC<TJoinProjectModalProps> = ({ onClose, on
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 border border-custom-border-300 px-5 py-8 text-left shadow-xl transition-all sm:w-full sm:max-w-xl sm:p-6">
<div className="space-y-5">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-custom-text-100"
>
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
Join Project?
</Dialog.Title>
<p>
Are you sure you want to join{" "}
<span className="font-semibold">{data?.name}</span>?
Are you sure you want to join <span className="font-semibold">{project?.name}</span>?
</p>
<div className="space-y-3" />
</div>

View File

@ -34,12 +34,11 @@ export const ProjectSidebarList: FC = observer(() => {
// swr
useSWR(
workspaceSlug ? "PROJECTS_LIST" : null,
workspaceSlug ? () => workspaceStore.getWorkspaceProjects(workspaceSlug?.toString()) : null
workspaceSlug ? () => projectStore.fetchProjects(workspaceSlug?.toString()) : null
);
// states
const [isFavoriteProjectCreate, setIsFavoriteProjectCreate] = useState(false);
const [isProjectModalOpen, setIsProjectModalOpen] = useState(false);
const [deleteProjectModal, setDeleteProjectModal] = useState(false);
const [isScrolled, setIsScrolled] = useState(false); // scroll animation state
@ -48,8 +47,8 @@ export const ProjectSidebarList: FC = observer(() => {
const { user } = useUserAuth();
const { setToastAlert } = useToast();
const joinedProjects = workspaceSlug && workspaceStore.workspaceJoinedProjects;
const favoriteProjects = workspaceSlug && workspaceStore.workspaceFavoriteProjects;
const joinedProjects = workspaceSlug && projectStore.joinedProjects;
const favoriteProjects = workspaceSlug && projectStore.favoriteProjects;
const orderedJoinedProjects: IProject[] | undefined = joinedProjects
? orderArrayBy(joinedProjects, "sort_order", "ascending")

View File

@ -1,9 +1,7 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
import type { NextPage } from "next";
// services
import projectService from "services/project.service";
// hooks
@ -12,48 +10,26 @@ import useWorkspaces from "hooks/use-workspaces";
import useUserAuth from "hooks/use-user-auth";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// components
import { JoinProjectModal } from "components/project/join-project-modal";
import { DeleteProjectModal, SingleProjectCard } from "components/project";
// ui
import { EmptyState, Icon, Loader, PrimaryButton } from "components/ui";
import { Icon, PrimaryButton } from "components/ui";
import { Breadcrumbs, BreadcrumbItem } from "components/breadcrumbs";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// images
import emptyProject from "public/empty-state/project.svg";
// types
import type { NextPage } from "next";
// fetch-keys
import { PROJECT_MEMBERS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
// types
import { IProject } from "types";
// lib
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { ProjectCardList } from "components/project";
const ProjectsPage: NextPage = () => {
// router
const router = useRouter();
const { workspaceSlug } = router.query;
const [query, setQuery] = useState("");
const { user } = useUserAuth();
// store
const { project: projectStore } = useMobxStore();
// context data
const { activeWorkspace } = useWorkspaces();
const { projects, mutateProjects } = useProjects();
// states
const [deleteProject, setDeleteProject] = useState<string | null>(null);
const [selectedProjectToJoin, setSelectedProjectToJoin] = useState<string | null>(null);
const filteredProjectList =
query === ""
? projects
: projects?.filter(
(project) =>
project.name.toLowerCase().includes(query.toLowerCase()) ||
project.identifier.toLowerCase().includes(query.toLowerCase())
);
return (
<WorkspaceAuthorizationLayout
@ -71,8 +47,8 @@ const ProjectsPage: NextPage = () => {
<Icon iconName="search" className="!text-xl !leading-5 !text-custom-sidebar-text-400" />
<input
className="w-full border-none bg-transparent text-xs text-custom-text-200 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
value={projectStore.searchQuery}
onChange={(e) => projectStore.setSearchQuery(e.target.value)}
placeholder="Search"
/>
</div>
@ -90,83 +66,7 @@ const ProjectsPage: NextPage = () => {
</div>
}
>
<JoinProjectModal
data={projects?.find((item) => item.id === selectedProjectToJoin)}
onClose={() => setSelectedProjectToJoin(null)}
onJoin={async () => {
const project = projects?.find((item) => item.id === selectedProjectToJoin);
if (!project) return;
await projectService
.joinProject(workspaceSlug as string, {
project_ids: [project.id],
})
.then(async () => {
mutate(PROJECT_MEMBERS(project.id));
mutateProjects<IProject[]>(
(prevData) =>
(prevData ?? []).map((p) => ({
...p,
is_member: p.id === project.id ? true : p.is_member,
})),
false
);
setSelectedProjectToJoin(null);
})
.catch(() => {
setSelectedProjectToJoin(null);
});
}}
/>
<DeleteProjectModal
isOpen={!!deleteProject}
onClose={() => setDeleteProject(null)}
data={projects?.find((item) => item.id === deleteProject) ?? null}
user={user}
/>
{filteredProjectList ? (
<div className="h-full w-full overflow-hidden">
{filteredProjectList.length > 0 ? (
<div className="h-full p-8 overflow-y-auto">
<div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
{filteredProjectList.map((project) => (
<SingleProjectCard
key={project.id}
project={project}
setToJoinProject={setSelectedProjectToJoin}
setDeleteProject={setDeleteProject}
/>
))}
</div>
</div>
) : (
<EmptyState
image={emptyProject}
title="No projects yet"
description="Get started by creating your first project"
primaryButton={{
icon: <PlusIcon className="h-4 w-4" />,
text: "New Project",
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "p",
});
document.dispatchEvent(e);
},
}}
/>
)}
</div>
) : (
<Loader className="grid grid-cols-3 gap-4">
<Loader.Item height="100px" />
<Loader.Item height="100px" />
<Loader.Item height="100px" />
<Loader.Item height="100px" />
<Loader.Item height="100px" />
<Loader.Item height="100px" />
</Loader>
)}
{workspaceSlug && <ProjectCardList workspaceSlug={workspaceSlug.toString()} />}
</WorkspaceAuthorizationLayout>
);
};

View File

@ -124,8 +124,8 @@ export class ProjectService extends APIService {
});
}
async joinProject(workspaceSlug: string, data: any): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/join/`, data)
async joinProject(workspaceSlug: string, project_ids: string[]): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/join/`, { project_ids })
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;

View File

@ -5,12 +5,17 @@ import { RootStore } from "./root";
import { ProjectService } from "services/project.service";
import { IssueService } from "services/issue.service";
import { ICycle } from "types";
import { CycleService } from "services/cycles.service";
export interface ICycleStore {
loader: boolean;
error: any | null;
cycles: {
[project_id: string]: ICycle[];
};
cycle_details: {
[cycle_id: string]: ICycle;
};
}
@ -20,6 +25,10 @@ class CycleStore implements ICycleStore {
error: any | null = null;
cycles: {
[project_id: string]: ICycle[];
} = {};
cycle_details: {
[cycle_id: string]: ICycle;
} = {};
@ -28,6 +37,7 @@ class CycleStore implements ICycleStore {
// services
projectService;
issueService;
cycleService;
constructor(_rootStore: RootStore) {
makeObservable(this, {
@ -44,11 +54,37 @@ class CycleStore implements ICycleStore {
this.rootStore = _rootStore;
this.projectService = new ProjectService();
this.issueService = new IssueService();
this.cycleService = new CycleService();
}
// computed
get projectCycles() {
if (!this.rootStore.project.projectId) return null;
return this.cycles[this.rootStore.project.projectId] || null;
}
// actions
fetchCycles = async (workspaceSlug: string, projectSlug: string) => {
try {
this.loader = true;
this.error = null;
const cyclesResponse = await this.cycleService.getCyclesWithParams(workspaceSlug, projectSlug, "all");
runInAction(() => {
this.cycles = {
...this.cycles,
[projectSlug]: cyclesResponse,
};
this.loader = false;
this.error = null;
});
} catch (error) {
console.error("Failed to fetch project cycles in project store", error);
this.loader = false;
this.error = error;
}
};
}
export default CycleStore;

View File

@ -3,15 +3,24 @@ import { action, computed, observable, makeObservable, runInAction } from "mobx"
import { RootStore } from "./root";
// services
import { ProjectService } from "services/project.service";
import { IssueService } from "services/issue.service";
import { ModuleService } from "services/modules.service";
import { IModule } from "@/types";
export interface IModuleStore {
loader: boolean;
error: any | null;
moduleId: string | null;
modules: {
[project_id: string]: IModule[];
};
module_details: {
[module_id: string]: IModule;
};
setModuleId: (moduleSlug: string) => void;
fetchModules: (workspaceSlug: string, projectSlug: string) => void;
}
class ModuleStore implements IModuleStore {
@ -20,11 +29,19 @@ class ModuleStore implements IModuleStore {
moduleId: string | null = null;
modules: {
[project_id: string]: IModule[];
} = {};
module_details: {
[module_id: string]: IModule;
} = {};
// root store
rootStore;
// services
projectService;
issueService;
moduleService;
constructor(_rootStore: RootStore) {
makeObservable(this, {
@ -41,15 +58,41 @@ class ModuleStore implements IModuleStore {
this.rootStore = _rootStore;
this.projectService = new ProjectService();
this.issueService = new IssueService();
this.moduleService = new ModuleService();
}
// computed
get projectModules() {
if (!this.rootStore.project.projectId) return null;
return this.modules[this.rootStore.project.projectId] || null;
}
// actions
setModuleId = (moduleSlug: string) => {
this.moduleId = moduleSlug ?? null;
};
fetchModules = async (workspaceSlug: string, projectSlug: string) => {
try {
this.loader = true;
this.error = null;
const modulesResponse = await this.moduleService.getModules(workspaceSlug, projectSlug);
runInAction(() => {
this.modules = {
...this.modules,
[projectSlug]: modulesResponse,
};
this.loader = false;
this.error = null;
});
} catch (error) {
console.error("Failed to fetch modules list in project store", error);
this.loader = false;
this.error = error;
}
};
}
export default ModuleStore;

104
web/store/page.ts Normal file
View File

@ -0,0 +1,104 @@
import { observable, action, computed, makeObservable, runInAction } from "mobx";
// types
import { RootStore } from "./root";
import { IProject, IIssueLabels, IProjectMember, IStateResponse, IState, IPage } from "types";
// services
import { ProjectService } from "services/project.service";
import { IssueService } from "services/issue.service";
import { ProjectStateServices } from "services/project_state.service";
import { CycleService } from "services/cycles.service";
import { ModuleService } from "services/modules.service";
import { ViewService } from "services/views.service";
import { PageService } from "services/page.service";
export interface IPageStore {
loader: boolean;
error: any | null;
pageId: string | null;
pages: {
[project_id: string]: IPage[];
};
page_details: {
[page_id: string]: IPage;
};
//computed
projectPages: IPage[];
// actions
setPageId: (pageId: string) => void;
fetchPages: (workspaceSlug: string, projectSlug: string) => void;
}
class PageStore implements IPageStore {
loader: boolean = false;
error: any | null = null;
pageId: string | null = null;
pages: {
[project_id: string]: IPage[];
} = {};
page_details: {
[page_id: string]: IPage;
} = {};
// root store
rootStore;
// service
projectService;
pageService;
constructor(_rootStore: RootStore) {
makeObservable(this, {
// observable
loader: observable,
error: observable,
pageId: observable.ref,
pages: observable.ref,
// computed
projectPages: computed,
// action
setPageId: action,
fetchPages: action,
});
this.rootStore = _rootStore;
this.projectService = new ProjectService();
this.pageService = new PageService();
}
get projectPages() {
if (!this.rootStore.project.projectId) return [];
return this.pages?.[this.rootStore.project.projectId] || [];
}
setPageId = (pageId: string) => {
this.pageId = pageId;
};
fetchPages = async (workspaceSlug: string, projectSlug: string) => {
try {
this.loader = true;
this.error = null;
const pagesResponse = await this.pageService.getPagesWithParams(workspaceSlug, projectSlug, "all");
runInAction(() => {
this.pages = {
...this.pages,
[projectSlug]: pagesResponse,
};
this.loader = false;
this.error = null;
});
} catch (error) {
console.error("Failed to fetch project pages in project store", error);
this.loader = false;
this.error = error;
}
};
}
export default PageStore;

View File

@ -1,7 +1,7 @@
import { observable, action, computed, makeObservable, runInAction } from "mobx";
// types
import { RootStore } from "./root";
import { IProject, IIssueLabels, IProjectMember, IStateResponse, IState, ICycle, IModule, IView, IPage } from "types";
import { IProject, IIssueLabels, IProjectMember, IStateResponse, IState } from "types";
// services
import { ProjectService } from "services/project.service";
import { IssueService } from "services/issue.service";
@ -15,9 +15,10 @@ export interface IProjectStore {
loader: boolean;
error: any | null;
searchQuery: string;
projectId: string | null;
projects: {
projects: { [key: string]: IProject[] };
project_details: {
[projectId: string]: IProject; // projectId: project Info
} | null;
states: {
@ -29,32 +30,26 @@ export interface IProjectStore {
members: {
[projectId: string]: IProjectMember[] | null; // project_id: members
} | null;
cycles: {
[projectId: string]: ICycle[] | null; // project_id: cycles
} | null;
modules: {
[projectId: string]: IModule[] | null; // project_id: modules
} | null;
views: {
[projectId: string]: IView[] | null; // project_id: views
} | null;
pages: {
[projectId: string]: IPage[] | null; // project_id: pages
} | null;
// computed
searchedProjects: IProject[];
projectStatesByGroups: IStateResponse | null;
projectStates: IState[] | null;
projectLabels: IIssueLabels[] | null;
projectMembers: IProjectMember[] | null;
joinedProjects: IProject[];
favoriteProjects: IProject[];
// actions
setProjectId: (projectId: string) => void;
setSearchQuery: (query: string) => void;
getProjectStateById: (stateId: string) => IState | null;
getProjectLabelById: (labelId: string) => IIssueLabels | null;
getProjectMemberById: (memberId: string) => IProjectMember | null;
fetchProjects: (workspaceSlug: string) => Promise<void>;
fetchProjectStates: (workspaceSlug: string, projectSlug: string) => Promise<void>;
fetchProjectLabels: (workspaceSlug: string, projectSlug: string) => Promise<void>;
fetchProjectMembers: (workspaceSlug: string, projectSlug: string) => Promise<void>;
@ -65,8 +60,7 @@ export interface IProjectStore {
orderProjectsWithSortOrder: (sourceIndex: number, destinationIndex: number, projectId: string) => number;
updateProjectView: (workspaceSlug: string, projectId: string, viewProps: any) => Promise<any>;
handleProjectLeaveModal: (project: any | null) => void;
joinProject: (workspaceSlug: string, projectIds: string[]) => Promise<void>;
leaveProject: (workspaceSlug: string, projectSlug: string) => Promise<void>;
deleteProject: (workspaceSlug: string, projectSlug: string) => Promise<void>;
}
@ -75,26 +69,12 @@ class ProjectStore implements IProjectStore {
loader: boolean = false;
error: any | null = null;
projectLeaveModal: boolean = false;
projectLeaveDetails: IProject | null = null;
searchQuery: string = "";
projectId: string | null = null;
projects: {
projects: { [workspaceSlug: string]: IProject[] } = {}; // workspace_id: project[]
project_details: {
[key: string]: IProject; // project_id: project
} | null = {};
cycles: {
[key: string]: ICycle[]; // project_id: cycles
} = {};
modules: {
[key: string]: IModule[]; // project_id: modules
} = {};
views: {
[key: string]: IView[]; // project_id: views
} = {};
pages: {
[key: string]: IPage[]; // project_id: pages
} = {};
states: {
[key: string]: IStateResponse; // project_id: states
} | null = {};
@ -122,23 +102,27 @@ class ProjectStore implements IProjectStore {
loader: observable,
error: observable,
searchQuery: observable.ref,
projectId: observable.ref,
projects: observable.ref,
project_details: observable.ref,
states: observable.ref,
labels: observable.ref,
members: observable.ref,
projectLeaveModal: observable,
projectLeaveDetails: observable.ref,
// computed
searchedProjects: computed,
projectStatesByGroups: computed,
projectStates: computed,
projectLabels: computed,
projectMembers: computed,
joinedProjects: computed,
favoriteProjects: computed,
// action
setProjectId: action,
setSearchQuery: action,
getProjectStateById: action,
getProjectLabelById: action,
@ -153,8 +137,6 @@ class ProjectStore implements IProjectStore {
orderProjectsWithSortOrder: action,
updateProjectView: action,
handleProjectLeaveModal: action,
leaveProject: action,
});
@ -168,6 +150,30 @@ class ProjectStore implements IProjectStore {
this.cycleService = new CycleService();
}
get searchedProjects() {
if (!this.rootStore.workspace.workspaceSlug) return [];
const projects = this.projects[this.rootStore.workspace.workspaceSlug];
return this.searchQuery === ""
? projects
: projects?.filter(
(project) =>
project.name.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
project.identifier.toLowerCase().includes(this.searchQuery.toLowerCase())
);
}
get joinedProjects() {
if (!this.rootStore.workspace.workspaceSlug) return [];
return this.projects?.[this.rootStore.workspace.workspaceSlug]?.filter((p) => p.is_member);
}
get favoriteProjects() {
if (!this.rootStore.workspace.workspaceSlug) return [];
return this.projects?.[this.rootStore.workspace.workspaceSlug]?.filter((p) => p.is_favorite);
}
get projectStatesByGroups() {
if (!this.projectId) return null;
return this.states?.[this.projectId] || null;
@ -196,31 +202,34 @@ class ProjectStore implements IProjectStore {
return this.members?.[this.projectId] || null;
}
get projectCycles() {
if (!this.projectId) return null;
return this.cycles[this.projectId] || null;
}
get projectModules() {
if (!this.projectId) return null;
return this.modules[this.projectId] || null;
}
get projectViews() {
if (!this.projectId) return null;
return this.views[this.projectId] || null;
}
get projectPages() {
if (!this.projectId) return null;
return this.pages[this.projectId] || null;
}
// actions
setProjectId = (projectSlug: string) => {
this.projectId = projectSlug ?? null;
};
setSearchQuery = (query: string) => {
this.searchQuery = query;
};
/**
* get Workspace projects using workspace slug
* @param workspaceSlug
* @returns
*/
fetchProjects = async (workspaceSlug: string) => {
try {
const projects = await this.projectService.getProjects(workspaceSlug, { is_favorite: "all" });
runInAction(() => {
this.projects = {
...this.projects,
[workspaceSlug]: projects,
};
});
} catch (error) {
console.log("Failed to fetch project from workspace store");
}
};
getProjectStateById = (stateId: string) => {
if (!this.projectId) return null;
const states = this.projectStates;
@ -314,99 +323,10 @@ class ProjectStore implements IProjectStore {
}
};
fetchProjectCycles = async (workspaceSlug: string, projectSlug: string) => {
try {
this.loader = true;
this.error = null;
const cyclesResponse = await this.cycleService.getCyclesWithParams(workspaceSlug, projectSlug, "all");
runInAction(() => {
this.cycles = {
...this.cycles,
[projectSlug]: cyclesResponse,
};
this.loader = false;
this.error = null;
});
} catch (error) {
console.error("Failed to fetch project cycles in project store", error);
this.loader = false;
this.error = error;
}
};
fetchProjectModules = async (workspaceSlug: string, projectSlug: string) => {
try {
this.loader = true;
this.error = null;
const modulesResponse = await this.moduleService.getModules(workspaceSlug, projectSlug);
runInAction(() => {
this.modules = {
...this.modules,
[projectSlug]: modulesResponse,
};
this.loader = false;
this.error = null;
});
} catch (error) {
console.error("Failed to fetch modules list in project store", error);
this.loader = false;
this.error = error;
}
};
fetchProjectViews = async (workspaceSlug: string, projectSlug: string) => {
try {
this.loader = true;
this.error = null;
const viewsResponse = await this.viewService.getViews(workspaceSlug, projectSlug);
runInAction(() => {
this.views = {
...this.views,
[projectSlug]: viewsResponse,
};
this.loader = false;
this.error = null;
});
} catch (error) {
console.error("Failed to fetch project views in project store", error);
this.loader = false;
this.error = error;
}
};
fetchProjectPages = async (workspaceSlug: string, projectSlug: string) => {
try {
this.loader = true;
this.error = null;
const pagesResponse = await this.pageService.getPagesWithParams(workspaceSlug, projectSlug, "all");
runInAction(() => {
this.pages = {
...this.pages,
[projectSlug]: pagesResponse,
};
this.loader = false;
this.error = null;
});
} catch (error) {
console.error("Failed to fetch project pages in project store", error);
this.loader = false;
this.error = error;
}
};
addProjectToFavorites = async (workspaceSlug: string, projectId: string) => {
try {
const response = await this.projectService.addProjectToFavorites(workspaceSlug, projectId);
console.log("res", response);
await this.rootStore.workspace.getWorkspaceProjects(workspaceSlug);
await this.fetchProjects(workspaceSlug);
return response;
} catch (error) {
console.log("Failed to add project to favorite");
@ -416,8 +336,8 @@ class ProjectStore implements IProjectStore {
removeProjectFromFavorites = async (workspaceSlug: string, projectId: string) => {
try {
const response = this.projectService.removeProjectFromFavorites(workspaceSlug, projectId);
this.rootStore.workspace.getWorkspaceProjects(workspaceSlug);
const response = await this.projectService.removeProjectFromFavorites(workspaceSlug, projectId);
await this.fetchProjects(workspaceSlug);
return response;
} catch (error) {
console.log("Failed to add project to favorite");
@ -430,7 +350,7 @@ class ProjectStore implements IProjectStore {
const workspaceSlug = this.rootStore.workspace.workspaceSlug;
if (!workspaceSlug) return 0;
const projectsList = this.rootStore.workspace.projects[workspaceSlug] || [];
const projectsList = this.projects[workspaceSlug] || [];
let updatedSortOrder = projectsList[sortIndex].sort_order;
if (destinationIndex === 0) updatedSortOrder = (projectsList[0].sort_order as number) - 1000;
@ -451,8 +371,8 @@ class ProjectStore implements IProjectStore {
);
runInAction(() => {
this.rootStore.workspace.projects = {
...this.rootStore.workspace.projects,
this.projects = {
...this.projects,
[workspaceSlug]: updatedProjectsList,
};
});
@ -467,7 +387,8 @@ class ProjectStore implements IProjectStore {
updateProjectView = async (workspaceSlug: string, projectId: string, viewProps: any) => {
try {
const response = await this.projectService.setProjectView(workspaceSlug, projectId, viewProps);
await this.rootStore.workspace.getWorkspaceProjects(workspaceSlug);
await this.fetchProjects(workspaceSlug);
return response;
} catch (error) {
console.log("Failed to update sort order of the projects");
@ -475,13 +396,24 @@ class ProjectStore implements IProjectStore {
}
};
handleProjectLeaveModal = (project: IProject | null = null) => {
if (project && project?.id) {
this.projectLeaveModal = !this.projectLeaveModal;
this.projectLeaveDetails = project;
} else {
this.projectLeaveModal = !this.projectLeaveModal;
this.projectLeaveDetails = null;
joinProject = async (workspaceSlug: string, projectIds: string[]) => {
try {
this.loader = true;
this.error = null;
const response = await this.projectService.joinProject(workspaceSlug, projectIds);
await this.fetchProjects(workspaceSlug);
runInAction(() => {
this.loader = false;
this.error = null;
});
return response;
} catch (error) {
this.loader = false;
this.error = error;
return error;
}
};
@ -491,7 +423,7 @@ class ProjectStore implements IProjectStore {
this.error = null;
const response = await this.projectService.leaveProject(workspaceSlug, projectSlug, this.rootStore.user);
await this.rootStore.workspace.getWorkspaceProjects(workspaceSlug);
await this.fetchProjects(workspaceSlug);
runInAction(() => {
this.loader = false;
@ -509,7 +441,7 @@ class ProjectStore implements IProjectStore {
deleteProject = async (workspaceSlug: string, projectId: string) => {
try {
await this.projectService.deleteProject(workspaceSlug, projectId, this.rootStore.user.currentUser);
await this.rootStore.workspace.getWorkspaceProjects(workspaceSlug);
await this.fetchProjects(workspaceSlug);
} catch (error) {
console.log("Failed to delete project from project store");
}

View File

@ -4,14 +4,20 @@ import { RootStore } from "./root";
// services
import { ProjectService } from "services/project.service";
import { IssueService } from "services/issue.service";
import { ViewService } from "services/views.service";
export interface IViewStore {
loader: boolean;
error: any | null;
viewId: string | null;
views: {
[project_id: string]: any[];
};
setViewId: (viewSlug: string) => void;
fetchViews: (workspaceSlug: string, projectSlug: string) => Promise<any>;
}
class ViewStore implements IViewStore {
@ -19,12 +25,15 @@ class ViewStore implements IViewStore {
error: any | null = null;
viewId: string | null = null;
views: {
[project_id: string]: any[];
} = {};
// root store
rootStore;
// services
projectService;
issueService;
viewService;
constructor(_rootStore: RootStore) {
makeObservable(this, {
@ -32,24 +41,51 @@ class ViewStore implements IViewStore {
error: observable.ref,
viewId: observable.ref,
views: observable.ref,
// computed
projectViews: computed,
// actions
setViewId: action,
});
this.rootStore = _rootStore;
this.projectService = new ProjectService();
this.issueService = new IssueService();
this.viewService = new ViewService();
}
// computed
get projectViews() {
if (!this.rootStore.project.projectId) return null;
return this.views[this.rootStore.project.projectId] || null;
}
// actions
setViewId = (viewSlug: string) => {
this.viewId = viewSlug ?? null;
};
fetchViews = async (workspaceSlug: string, projectId: string) => {
try {
this.loader = true;
this.error = null;
const viewsResponse = await this.viewService.getViews(workspaceSlug, projectId);
runInAction(() => {
this.views = {
...this.views,
[projectId]: viewsResponse,
};
this.loader = false;
this.error = null;
});
} catch (error) {
console.error("Failed to fetch project views in project store", error);
this.loader = false;
this.error = error;
}
};
}
export default ViewStore;

View File

@ -12,19 +12,15 @@ export interface IWorkspaceStore {
error: any | null;
// observables
workspaces: IWorkspace[];
projects: { [key: string]: IProject[] };
labels: { [key: string]: IIssueLabels[] } | {}; // workspace_id: labels[]
workspaceSlug: string | null;
// computed
currentWorkspace: IWorkspace | null;
workspaceLabels: IIssueLabels[];
workspaceJoinedProjects: IProject[];
workspaceFavoriteProjects: IProject[];
// actions
setWorkspaceSlug: (workspaceSlug: string) => void;
getWorkspaceBySlug: (workspaceSlug: string) => IWorkspace | null;
getWorkspaceLabelById: (workspaceSlug: string, labelId: string) => IIssueLabels | null;
getWorkspaceProjects: (workspaceSlug: string) => void;
fetchWorkspaces: () => Promise<void>;
fetchWorkspaceLabels: (workspaceSlug: string) => Promise<void>;
}
@ -52,17 +48,13 @@ class WorkspaceStore implements IWorkspaceStore {
workspaces: observable.ref,
labels: observable.ref,
workspaceSlug: observable.ref,
projects: observable.ref,
// computed
currentWorkspace: computed,
workspaceLabels: computed,
workspaceJoinedProjects: computed,
workspaceFavoriteProjects: computed,
// actions
setWorkspaceSlug: action,
getWorkspaceBySlug: action,
getWorkspaceLabelById: action,
getWorkspaceProjects: action,
fetchWorkspaces: action,
fetchWorkspaceLabels: action,
});
@ -90,16 +82,6 @@ class WorkspaceStore implements IWorkspaceStore {
return _labels && Object.keys(_labels).length > 0 ? _labels : [];
}
get workspaceJoinedProjects() {
if (!this.workspaceSlug) return [];
return this.projects?.[this.workspaceSlug]?.filter((p) => p.is_member);
}
get workspaceFavoriteProjects() {
if (!this.workspaceSlug) return [];
return this.projects?.[this.workspaceSlug]?.filter((p) => p.is_favorite);
}
/**
* set workspace slug in the store
* @param workspaceSlug
@ -113,25 +95,6 @@ class WorkspaceStore implements IWorkspaceStore {
*/
getWorkspaceBySlug = (workspaceSlug: string) => this.workspaces.find((w) => w.slug == workspaceSlug) || null;
/**
* get Workspace projects using workspace slug
* @param workspaceSlug
* @returns
*/
getWorkspaceProjects = async (workspaceSlug: string) => {
try {
const projects = await this.projectService.getProjects(workspaceSlug, { is_favorite: "all" });
runInAction(() => {
this.projects = {
...this.projects,
[workspaceSlug]: projects,
};
});
} catch (error) {
console.log("Failed to fetch project from workspace store");
}
};
/**
* get workspace label information from the workspace labels
* @param labelId
@ -188,50 +151,6 @@ class WorkspaceStore implements IWorkspaceStore {
this.error = error;
}
};
// getMyIssuesAsync = async (workspaceId: string, fetchFilterToggle: boolean = true) => {
// try {
// this.loader = true;
// this.error = null;
// if (fetchFilterToggle) await this.rootStore.issueFilters.getWorkspaceMyIssuesFilters(workspaceId);
// const filteredParams = this.rootStore.issueFilters.getComputedFilters(
// workspaceId,
// null,
// null,
// null,
// null,
// "my_issues"
// );
// const issuesResponse = await this.userService.userIssues(workspaceId, filteredParams);
// if (issuesResponse) {
// const _issueResponse: any = {
// ...this.issues,
// [workspaceId]: {
// ...this?.issues[workspaceId],
// my_issues: {
// ...this?.issues[workspaceId]?.my_issues,
// [this.rootStore?.issueFilters?.userFilters?.display_filters?.layout as string]: issuesResponse,
// },
// },
// };
// runInAction(() => {
// this.issues = _issueResponse;
// this.loader = false;
// this.error = null;
// });
// }
// return issuesResponse;
// } catch (error) {
// console.warn("error in fetching the my issues", error);
// this.loader = false;
// this.error = null;
// return error;
// }
// };
}
export default WorkspaceStore;