[WEB-578] feat: projects list filtering and ordering (#3926)

* style: project card UI updated

* dev: initialize project filter store and types

* chore: implemented filtering logic

* chore: implemented ordering

* chore: my projects filter added

* chore: update created at date filter options

* refactor: order by dropdown

* style: revert project card UI

* fix: project card z-index

* fix: members filtering

* fix: build errors
This commit is contained in:
Aaryan Khandelwal 2024-03-12 19:36:40 +05:30 committed by GitHub
parent c3c6ef8830
commit 69e110f4a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 1452 additions and 186 deletions

View file

@ -1,18 +1,22 @@
import { RootStore } from "store/root.store";
import { IProjectPublishStore, ProjectPublishStore } from "./project-publish.store";
import { IProjectStore, ProjectStore } from "./project.store";
import { IProjectFilterStore, ProjectFilterStore } from "./project_filter.store";
export interface IProjectRootStore {
project: IProjectStore;
projectFilter: IProjectFilterStore;
publish: IProjectPublishStore;
}
export class ProjectRootStore {
project: IProjectStore;
projectFilter: IProjectFilterStore;
publish: IProjectPublishStore;
constructor(_root: RootStore) {
this.project = new ProjectStore(_root);
this.projectFilter = new ProjectFilterStore(_root);
this.publish = new ProjectPublishStore(this);
}
}

View file

@ -1,4 +1,3 @@
import { cloneDeep, update } from "lodash";
import set from "lodash/set";
import sortBy from "lodash/sortBy";
import { observable, action, computed, makeObservable, runInAction } from "mobx";
@ -8,40 +7,38 @@ import { IssueLabelService, IssueService } from "services/issue";
import { ProjectService, ProjectStateService } from "services/project";
import { IProject } from "@plane/types";
import { RootStore } from "../root.store";
import { orderProjects, shouldFilterProject } from "helpers/project.helper";
// services
export interface IProjectStore {
// observables
searchQuery: string;
projectMap: {
[projectId: string]: IProject; // projectId: project Info
};
// computed
searchedProjects: string[];
workspaceProjectIds: string[] | null;
filteredProjectIds: string[] | undefined;
workspaceProjectIds: string[] | undefined;
joinedProjectIds: string[];
favoriteProjectIds: string[];
currentProjectDetails: IProject | undefined;
// actions
setSearchQuery: (query: string) => void;
getProjectById: (projectId: string) => IProject | null;
getProjectIdentifierById: (projectId: string) => string;
// fetch actions
fetchProjects: (workspaceSlug: string) => Promise<IProject[]>;
fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise<any>;
fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise<IProject>;
// favorites actions
addProjectToFavorites: (workspaceSlug: string, projectId: string) => Promise<any>;
removeProjectFromFavorites: (workspaceSlug: string, projectId: string) => Promise<any>;
// project-view action
updateProjectView: (workspaceSlug: string, projectId: string, viewProps: any) => Promise<any>;
// CRUD actions
createProject: (workspaceSlug: string, data: any) => Promise<any>;
updateProject: (workspaceSlug: string, projectId: string, data: Partial<IProject>) => Promise<any>;
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>;
}
export class ProjectStore implements IProjectStore {
// observables
searchQuery: string = "";
projectMap: {
[projectId: string]: IProject; // projectId: project Info
} = {};
@ -56,16 +53,13 @@ export class ProjectStore implements IProjectStore {
constructor(_rootStore: RootStore) {
makeObservable(this, {
// observables
searchQuery: observable.ref,
projectMap: observable,
// computed
searchedProjects: computed,
filteredProjectIds: computed,
workspaceProjectIds: computed,
currentProjectDetails: computed,
joinedProjectIds: computed,
favoriteProjectIds: computed,
// actions
setSearchQuery: action.bound,
// fetch actions
fetchProjects: action,
fetchProjectDetails: action,
@ -88,17 +82,24 @@ export class ProjectStore implements IProjectStore {
}
/**
* Returns searched projects based on search query
* @description returns filtered projects based on filters and search query
*/
get searchedProjects() {
get filteredProjectIds() {
const workspaceDetails = this.rootStore.workspaceRoot.currentWorkspace;
if (!workspaceDetails) return [];
const workspaceProjects = Object.values(this.projectMap).filter(
const {
currentWorkspaceDisplayFilters: displayFilters,
currentWorkspaceFilters: filters,
searchQuery,
} = this.rootStore.projectRoot.projectFilter;
if (!workspaceDetails || !displayFilters || !filters) return;
let workspaceProjects = Object.values(this.projectMap).filter(
(p) =>
p.workspace === workspaceDetails.id &&
(p.name.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
p.identifier.toLowerCase().includes(this.searchQuery.toLowerCase()))
(p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
p.identifier.toLowerCase().includes(searchQuery.toLowerCase())) &&
shouldFilterProject(p, displayFilters, filters)
);
workspaceProjects = orderProjects(workspaceProjects, displayFilters.order_by);
return workspaceProjects.map((p) => p.id);
}
@ -107,7 +108,7 @@ export class ProjectStore implements IProjectStore {
*/
get workspaceProjectIds() {
const workspaceDetails = this.rootStore.workspaceRoot.currentWorkspace;
if (!workspaceDetails) return null;
if (!workspaceDetails) return;
const workspaceProjects = Object.values(this.projectMap).filter((p) => p.workspace === workspaceDetails.id);
const projectIds = workspaceProjects.map((p) => p.id);
return projectIds ?? null;
@ -153,14 +154,6 @@ export class ProjectStore implements IProjectStore {
return projectIds;
}
/**
* Sets search query
* @param query
*/
setSearchQuery = (query: string) => {
this.searchQuery = query;
};
/**
* get Workspace projects using workspace slug
* @param workspaceSlug

View file

@ -0,0 +1,144 @@
import { action, computed, observable, makeObservable, runInAction, autorun } from "mobx";
import { computedFn } from "mobx-utils";
import set from "lodash/set";
// types
import { RootStore } from "store/root.store";
import { TProjectDisplayFilters, TProjectFilters } from "@plane/types";
export interface IProjectFilterStore {
// observables
displayFilters: Record<string, TProjectDisplayFilters>;
filters: Record<string, TProjectFilters>;
searchQuery: string;
// computed
currentWorkspaceDisplayFilters: TProjectDisplayFilters | undefined;
currentWorkspaceFilters: TProjectFilters | undefined;
// computed functions
getDisplayFiltersByWorkspaceSlug: (workspaceSlug: string) => TProjectDisplayFilters | undefined;
getFiltersByWorkspaceSlug: (workspaceSlug: string) => TProjectFilters | undefined;
// actions
updateDisplayFilters: (workspaceSlug: string, displayFilters: TProjectDisplayFilters) => void;
updateFilters: (workspaceSlug: string, filters: TProjectFilters) => void;
updateSearchQuery: (query: string) => void;
clearAllFilters: (workspaceSlug: string) => void;
}
export class ProjectFilterStore implements IProjectFilterStore {
// observables
displayFilters: Record<string, TProjectDisplayFilters> = {};
filters: Record<string, TProjectFilters> = {};
searchQuery: string = "";
// root store
rootStore: RootStore;
constructor(_rootStore: RootStore) {
makeObservable(this, {
// observables
displayFilters: observable,
filters: observable,
searchQuery: observable.ref,
// computed
currentWorkspaceDisplayFilters: computed,
currentWorkspaceFilters: computed,
// actions
updateDisplayFilters: action,
updateFilters: action,
updateSearchQuery: action,
clearAllFilters: action,
});
// root store
this.rootStore = _rootStore;
// initialize display filters of the current workspace
autorun(() => {
const workspaceSlug = this.rootStore.app.router.workspaceSlug;
if (!workspaceSlug) return;
this.initWorkspaceFilters(workspaceSlug);
});
}
/**
* @description get display filters of the current workspace
*/
get currentWorkspaceDisplayFilters() {
const workspaceSlug = this.rootStore.app.router.workspaceSlug;
if (!workspaceSlug) return;
return this.displayFilters[workspaceSlug];
}
/**
* @description get filters of the current workspace
*/
get currentWorkspaceFilters() {
const workspaceSlug = this.rootStore.app.router.workspaceSlug;
if (!workspaceSlug) return;
return this.filters[workspaceSlug];
}
/**
* @description get display filters of a workspace by workspaceSlug
* @param {string} workspaceSlug
*/
getDisplayFiltersByWorkspaceSlug = computedFn((workspaceSlug: string) => this.displayFilters[workspaceSlug]);
/**
* @description get filters of a workspace by workspaceSlug
* @param {string} workspaceSlug
*/
getFiltersByWorkspaceSlug = computedFn((workspaceSlug: string) => this.filters[workspaceSlug]);
/**
* @description initialize display filters and filters of a workspace
* @param {string} workspaceSlug
*/
initWorkspaceFilters = (workspaceSlug: string) => {
const displayFilters = this.getDisplayFiltersByWorkspaceSlug(workspaceSlug);
runInAction(() => {
this.displayFilters[workspaceSlug] = {
order_by: displayFilters?.order_by || "created_at",
};
this.filters[workspaceSlug] = {};
});
};
/**
* @description update display filters of a workspace
* @param {string} workspaceSlug
* @param {TProjectDisplayFilters} displayFilters
*/
updateDisplayFilters = (workspaceSlug: string, displayFilters: TProjectDisplayFilters) => {
runInAction(() => {
Object.keys(displayFilters).forEach((key) => {
set(this.displayFilters, [workspaceSlug, key], displayFilters[key as keyof TProjectDisplayFilters]);
});
});
};
/**
* @description update filters of a workspace
* @param {string} workspaceSlug
* @param {TProjectFilters} filters
*/
updateFilters = (workspaceSlug: string, filters: TProjectFilters) => {
runInAction(() => {
Object.keys(filters).forEach((key) => {
set(this.filters, [workspaceSlug, key], filters[key as keyof TProjectFilters]);
});
});
};
/**
* @description update search query
* @param {string} query
*/
updateSearchQuery = (query: string) => (this.searchQuery = query);
/**
* @description clear all filters of a workspace
* @param {string} workspaceSlug
*/
clearAllFilters = (workspaceSlug: string) => {
runInAction(() => {
this.filters[workspaceSlug] = {};
});
};
}