[WEB-447] feat: projects archive. (#4014)

* dev: project archive response

* feat: projects archive.

* dev: response changes for cycle and module

* chore: status message changed

* chore: update clear all applied display filters logic.

* style: archived project card UI update.

* chore: archive/ restore taost message update.

* fix: clear all applied display filter logic.

* chore: project empty state update to handle archived projects.

* chore: minor typo fix in cycles and modules archive.

* chore: close cycle/ module overview sidebar if it's already open when clicked on overview button.

* chore: optimize current workspace applied display filter logic.

* chore: update all `archived_at` type from `Date` to `string`.

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
Prateek Shourya 2024-03-21 20:59:34 +05:30 committed by GitHub
parent 9642b761b7
commit 231fd52992
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 749 additions and 162 deletions

View file

@ -3,12 +3,15 @@ import sortBy from "lodash/sortBy";
import { observable, action, computed, makeObservable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
// types
import { IssueLabelService, IssueService } from "@/services/issue";
import { ProjectService, ProjectStateService } from "@/services/project";
import { IProject } from "@plane/types";
import { RootStore } from "../root.store";
// helpers
import { orderProjects, shouldFilterProject } from "@/helpers/project.helper";
// services
import { IssueLabelService, IssueService } from "@/services/issue";
import { ProjectService, ProjectStateService, ProjectArchiveService } from "@/services/project";
// store
import { RootStore } from "../root.store";
export interface IProjectStore {
// observables
projectMap: {
@ -17,6 +20,8 @@ export interface IProjectStore {
// computed
filteredProjectIds: string[] | undefined;
workspaceProjectIds: string[] | undefined;
archivedProjectIds: string[] | undefined;
totalProjectIds: string[] | undefined;
joinedProjectIds: string[];
favoriteProjectIds: string[];
currentProjectDetails: IProject | undefined;
@ -35,6 +40,9 @@ export interface IProjectStore {
createProject: (workspaceSlug: string, data: Partial<IProject>) => Promise<IProject>;
updateProject: (workspaceSlug: string, projectId: string, data: Partial<IProject>) => Promise<IProject>;
deleteProject: (workspaceSlug: string, projectId: string) => Promise<void>;
// archive actions
archiveProject: (workspaceSlug: string, projectId: string) => Promise<void>;
restoreProject: (workspaceSlug: string, projectId: string) => Promise<void>;
}
export class ProjectStore implements IProjectStore {
@ -46,6 +54,7 @@ export class ProjectStore implements IProjectStore {
rootStore: RootStore;
// service
projectService;
projectArchiveService;
issueLabelService;
issueService;
stateService;
@ -57,6 +66,8 @@ export class ProjectStore implements IProjectStore {
// computed
filteredProjectIds: computed,
workspaceProjectIds: computed,
archivedProjectIds: computed,
totalProjectIds: computed,
currentProjectDetails: computed,
joinedProjectIds: computed,
favoriteProjectIds: computed,
@ -76,6 +87,7 @@ export class ProjectStore implements IProjectStore {
this.rootStore = _rootStore;
// services
this.projectService = new ProjectService();
this.projectArchiveService = new ProjectArchiveService();
this.issueService = new IssueService();
this.issueLabelService = new IssueLabelService();
this.stateService = new ProjectStateService();
@ -109,11 +121,42 @@ export class ProjectStore implements IProjectStore {
get workspaceProjectIds() {
const workspaceDetails = this.rootStore.workspaceRoot.currentWorkspace;
if (!workspaceDetails) return;
const workspaceProjects = Object.values(this.projectMap).filter((p) => p.workspace === workspaceDetails.id);
const workspaceProjects = Object.values(this.projectMap).filter(
(p) => p.workspace === workspaceDetails.id && !p.archived_at
);
const projectIds = workspaceProjects.map((p) => p.id);
return projectIds ?? null;
}
/**
* Returns archived project IDs belong to current workspace.
*/
get archivedProjectIds() {
const currentWorkspace = this.rootStore.workspaceRoot.currentWorkspace;
if (!currentWorkspace) return;
let projects = Object.values(this.projectMap ?? {});
projects = sortBy(projects, "archived_at");
const projectIds = projects
.filter((project) => project.workspace === currentWorkspace.id && !!project.archived_at)
.map((project) => project.id);
return projectIds;
}
/**
* Returns total project IDs belong to the current workspace
*/
// workspaceProjectIds + archivedProjectIds
get totalProjectIds() {
const currentWorkspace = this.rootStore.workspaceRoot.currentWorkspace;
if (!currentWorkspace) return;
const workspaceProjects = this.workspaceProjectIds ?? [];
const archivedProjects = this.archivedProjectIds ?? [];
return [...workspaceProjects, ...archivedProjects];
}
/**
* Returns current project details
*/
@ -133,7 +176,7 @@ export class ProjectStore implements IProjectStore {
projects = sortBy(projects, "sort_order");
const projectIds = projects
.filter((project) => project.workspace === currentWorkspace.id && project.is_member)
.filter((project) => project.workspace === currentWorkspace.id && project.is_member && !project.archived_at)
.map((project) => project.id);
return projectIds;
}
@ -149,7 +192,10 @@ export class ProjectStore implements IProjectStore {
projects = sortBy(projects, "created_at");
const projectIds = projects
.filter((project) => project.workspace === currentWorkspace.id && project.is_member && project.is_favorite)
.filter(
(project) =>
project.workspace === currentWorkspace.id && project.is_member && project.is_favorite && !project.archived_at
)
.map((project) => project.id);
return projectIds;
}
@ -348,4 +394,48 @@ export class ProjectStore implements IProjectStore {
this.fetchProjects(workspaceSlug);
}
};
/**
* Archives a project from specific workspace and updates it in the store
* @param workspaceSlug
* @param projectId
* @returns Promise<void>
*/
archiveProject = async (workspaceSlug: string, projectId: string) => {
await this.projectArchiveService
.archiveProject(workspaceSlug, projectId)
.then((response) => {
runInAction(() => {
set(this.projectMap, [projectId, "archived_at"], response.archived_at);
});
})
.catch((error) => {
console.log("Failed to archive project from project store");
this.fetchProjects(workspaceSlug);
this.fetchProjectDetails(workspaceSlug, projectId);
throw error;
});
};
/**
* Restores a project from specific workspace and updates it in the store
* @param workspaceSlug
* @param projectId
* @returns Promise<void>
*/
restoreProject = async (workspaceSlug: string, projectId: string) => {
await this.projectArchiveService
.restoreProject(workspaceSlug, projectId)
.then(() => {
runInAction(() => {
set(this.projectMap, [projectId, "archived_at"], null);
});
})
.catch((error) => {
console.log("Failed to restore project from project store");
this.fetchProjects(workspaceSlug);
this.fetchProjectDetails(workspaceSlug, projectId);
throw error;
});
};
}

View file

@ -1,9 +1,10 @@
import set from "lodash/set";
import { action, computed, observable, makeObservable, runInAction, reaction } from "mobx";
import { computedFn } from "mobx-utils";
import set from "lodash/set";
// types
import { TProjectDisplayFilters, TProjectFilters, TProjectAppliedDisplayFilterKeys } from "@plane/types";
// store
import { RootStore } from "@/store/root.store";
import { TProjectDisplayFilters, TProjectFilters } from "@plane/types";
export interface IProjectFilterStore {
// observables
@ -12,6 +13,7 @@ export interface IProjectFilterStore {
searchQuery: string;
// computed
currentWorkspaceDisplayFilters: TProjectDisplayFilters | undefined;
currentWorkspaceAppliedDisplayFilters: TProjectAppliedDisplayFilterKeys[] | undefined;
currentWorkspaceFilters: TProjectFilters | undefined;
// computed functions
getDisplayFiltersByWorkspaceSlug: (workspaceSlug: string) => TProjectDisplayFilters | undefined;
@ -21,6 +23,7 @@ export interface IProjectFilterStore {
updateFilters: (workspaceSlug: string, filters: TProjectFilters) => void;
updateSearchQuery: (query: string) => void;
clearAllFilters: (workspaceSlug: string) => void;
clearAllAppliedDisplayFilters: (workspaceSlug: string) => void;
}
export class ProjectFilterStore implements IProjectFilterStore {
@ -39,12 +42,14 @@ export class ProjectFilterStore implements IProjectFilterStore {
searchQuery: observable.ref,
// computed
currentWorkspaceDisplayFilters: computed,
currentWorkspaceAppliedDisplayFilters: computed,
currentWorkspaceFilters: computed,
// actions
updateDisplayFilters: action,
updateFilters: action,
updateSearchQuery: action,
clearAllFilters: action,
clearAllAppliedDisplayFilters: action,
});
// root store
this.rootStore = _rootStore;
@ -67,6 +72,21 @@ export class ProjectFilterStore implements IProjectFilterStore {
return this.displayFilters[workspaceSlug];
}
/**
* @description get project state applied display filter of the current workspace
* @returns {TProjectAppliedDisplayFilterKeys[] | undefined} // An array of keys of applied display filters
*/
// TODO: Figure out a better approach for this
get currentWorkspaceAppliedDisplayFilters() {
const workspaceSlug = this.rootStore.app.router.workspaceSlug;
if (!workspaceSlug) return;
const displayFilters = this.displayFilters[workspaceSlug];
return Object.keys(displayFilters).filter(
(key): key is TProjectAppliedDisplayFilterKeys =>
["my_projects", "archived_projects"].includes(key) && !!displayFilters[key as keyof TProjectDisplayFilters]
);
}
/**
* @description get filters of the current workspace
*/
@ -143,4 +163,17 @@ export class ProjectFilterStore implements IProjectFilterStore {
this.filters[workspaceSlug] = {};
});
};
/**
* @description clear project display filters of a workspace
* @param {string} workspaceSlug
*/
clearAllAppliedDisplayFilters = (workspaceSlug: string) => {
runInAction(() => {
if (!this.currentWorkspaceAppliedDisplayFilters) return;
this.currentWorkspaceAppliedDisplayFilters.forEach((key) => {
set(this.displayFilters, [workspaceSlug, key], false);
});
});
};
}