[WEB-2388] dev: workspace draft issues (#5772)

* chore: workspace draft page added

* chore: workspace draft issues services added

* chore: workspace draft issue store added

* chore: workspace draft issue filter store added

* chore: issue rendering

* conflicts: resolved merge conflicts

* conflicts: handled draft issue store

* chore: draft issue modal

* chore: code optimisation

* chore: ui changes

* chore: workspace draft store and modal updated

* chore: workspace draft issue component added

* chore: updated store and workflow in draft issues

* chore: updated issue draft store

* chore: updated issue type cleanup in components

* chore: code refactor

* fix: build error

* fix: quick actions

* fix: update mutation

* fix: create update modal

* chore: commented project draft issue code

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
Anmol Singh Bhatia 2024-10-10 19:12:34 +05:30 committed by GitHub
parent e9158f820f
commit 332d2d5c68
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 1895 additions and 190 deletions

View file

@ -23,7 +23,8 @@ import {
IProjectViewIssues,
ProjectViewIssues,
} from "./project-views";
import { IWorkspaceIssuesFilter, WorkspaceIssuesFilter, IWorkspaceIssues, WorkspaceIssues } from "./workspace";
import { WorkspaceIssuesFilter, IWorkspaceIssues, WorkspaceIssues, IWorkspaceIssuesFilter } from "./workspace";
import { IWorkspaceDraftIssues, IWorkspaceDraftIssuesFilter, WorkspaceDraftIssues, WorkspaceDraftIssuesFilter } from "./workspace-draft";
export interface IIssueRootStore {
currentUserId: string | undefined;
@ -55,6 +56,9 @@ export interface IIssueRootStore {
workspaceIssuesFilter: IWorkspaceIssuesFilter;
workspaceIssues: IWorkspaceIssues;
workspaceDraftIssuesFilter: IWorkspaceDraftIssuesFilter;
workspaceDraftIssues: IWorkspaceDraftIssues;
profileIssuesFilter: IProfileIssuesFilter;
profileIssues: IProfileIssues;
@ -110,6 +114,9 @@ export class IssueRootStore implements IIssueRootStore {
workspaceIssuesFilter: IWorkspaceIssuesFilter;
workspaceIssues: IWorkspaceIssues;
workspaceDraftIssuesFilter: IWorkspaceDraftIssuesFilter;
workspaceDraftIssues: IWorkspaceDraftIssues;
profileIssuesFilter: IProfileIssuesFilter;
profileIssues: IProfileIssues;
@ -190,6 +197,9 @@ export class IssueRootStore implements IIssueRootStore {
this.profileIssuesFilter = new ProfileIssuesFilter(this);
this.profileIssues = new ProfileIssues(this, this.profileIssuesFilter);
this.workspaceDraftIssuesFilter = new WorkspaceDraftIssuesFilter(this);
this.workspaceDraftIssues = new WorkspaceDraftIssues(this, this.workspaceDraftIssuesFilter);
this.projectIssuesFilter = new ProjectIssuesFilter(this);
this.projectIssues = new ProjectIssues(this, this.projectIssuesFilter);

View file

@ -0,0 +1,254 @@
import isEmpty from "lodash/isEmpty";
import set from "lodash/set";
import { action, computed, makeObservable, observable, runInAction } from "mobx";
// base class
import { computedFn } from "mobx-utils";
import {
IIssueFilterOptions,
IIssueDisplayFilterOptions,
IIssueDisplayProperties,
TIssueKanbanFilters,
IIssueFilters,
TIssueParams,
IssuePaginationOptions,
} from "@plane/types";
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
import { handleIssueQueryParamsByLayout } from "@/helpers/issue.helper";
import { IssueFiltersService } from "@/services/issue_filter.service";
import { IBaseIssueFilterStore, IssueFilterHelperStore } from "../helpers/issue-filter-helper.store";
// helpers
// types
import { IIssueRootStore } from "../root.store";
// constants
// services
export interface IWorkspaceDraftIssuesFilter extends IBaseIssueFilterStore {
// observables
workspaceSlug: string;
//helper actions
getFilterParams: (
options: IssuePaginationOptions,
userId: string,
cursor: string | undefined,
groupId: string | undefined,
subGroupId: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>;
// action
fetchFilters: (workspaceSlug: string) => Promise<void>;
updateFilters: (
workspaceSlug: string,
filterType: EIssueFilterType,
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
) => Promise<void>;
}
export class WorkspaceDraftIssuesFilter extends IssueFilterHelperStore implements IWorkspaceDraftIssuesFilter {
// observables
workspaceSlug: string = "";
filters: { [userId: string]: IIssueFilters } = {};
// root store
rootIssueStore: IIssueRootStore;
// services
issueFilterService;
constructor(_rootStore: IIssueRootStore) {
super();
makeObservable(this, {
// observables
workspaceSlug: observable.ref,
filters: observable,
// computed
issueFilters: computed,
appliedFilters: computed,
// actions
fetchFilters: action,
updateFilters: action,
});
// root store
this.rootIssueStore = _rootStore;
// services
this.issueFilterService = new IssueFiltersService();
}
get issueFilters() {
const workspaceSlug = this.rootIssueStore.workspaceSlug;
if (!workspaceSlug) return undefined;
return this.getIssueFilters(workspaceSlug);
}
get appliedFilters() {
const workspaceSlug = this.rootIssueStore.workspaceSlug;
if (!workspaceSlug) return undefined;
return this.getAppliedFilters(workspaceSlug);
}
getIssueFilters(workspaceSlug: string) {
const displayFilters = this.filters[workspaceSlug] || undefined;
if (isEmpty(displayFilters)) return undefined;
const _filters: IIssueFilters = this.computedIssueFilters(displayFilters);
return _filters;
}
getAppliedFilters(workspaceSlug: string) {
const userFilters = this.getIssueFilters(workspaceSlug);
if (!userFilters) return undefined;
const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "profile_issues");
if (!filteredParams) return undefined;
const filteredRouteParams: Partial<Record<TIssueParams, string | boolean>> = this.computedFilteredParams(
userFilters?.filters as IIssueFilterOptions,
userFilters?.displayFilters as IIssueDisplayFilterOptions,
filteredParams
);
return filteredRouteParams;
}
getFilterParams = computedFn(
(
options: IssuePaginationOptions,
userId: string,
cursor: string | undefined,
groupId: string | undefined,
subGroupId: string | undefined
) => {
const filterParams = this.getAppliedFilters(this.workspaceSlug);
const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId);
return paginationParams;
}
);
fetchFilters = async (workspaceSlug: string) => {
this.workspaceSlug = workspaceSlug;
const _filters = this.handleIssuesLocalFilters.get(
EIssuesStoreType.PROFILE,
workspaceSlug,
workspaceSlug,
undefined
);
const filters: IIssueFilterOptions = this.computedFilters(_filters?.filters);
const displayFilters: IIssueDisplayFilterOptions = this.computedDisplayFilters(_filters?.display_filters);
const displayProperties: IIssueDisplayProperties = this.computedDisplayProperties(_filters?.display_properties);
const kanbanFilters = {
group_by: _filters?.kanban_filters?.group_by || [],
sub_group_by: _filters?.kanban_filters?.sub_group_by || [],
};
runInAction(() => {
set(this.filters, [workspaceSlug, "filters"], filters);
set(this.filters, [workspaceSlug, "displayFilters"], displayFilters);
set(this.filters, [workspaceSlug, "displayProperties"], displayProperties);
set(this.filters, [workspaceSlug, "kanbanFilters"], kanbanFilters);
});
};
updateFilters = async (
workspaceSlug: string,
type: EIssueFilterType,
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
) => {
try {
if (isEmpty(this.filters) || isEmpty(this.filters[workspaceSlug]) || isEmpty(filters)) return;
const _filters = {
filters: this.filters[workspaceSlug].filters as IIssueFilterOptions,
displayFilters: this.filters[workspaceSlug].displayFilters as IIssueDisplayFilterOptions,
displayProperties: this.filters[workspaceSlug].displayProperties as IIssueDisplayProperties,
kanbanFilters: this.filters[workspaceSlug].kanbanFilters as TIssueKanbanFilters,
};
switch (type) {
case EIssueFilterType.FILTERS: {
const updatedFilters = filters as IIssueFilterOptions;
_filters.filters = { ..._filters.filters, ...updatedFilters };
runInAction(() => {
Object.keys(updatedFilters).forEach((_key) => {
set(this.filters, [workspaceSlug, "filters", _key], updatedFilters[_key as keyof IIssueFilterOptions]);
});
});
this.rootIssueStore.profileIssues.fetchIssuesWithExistingPagination(workspaceSlug, workspaceSlug, "mutation");
this.handleIssuesLocalFilters.set(EIssuesStoreType.PROFILE, type, workspaceSlug, workspaceSlug, undefined, {
filters: _filters.filters,
});
break;
}
case EIssueFilterType.DISPLAY_FILTERS: {
const updatedDisplayFilters = filters as IIssueDisplayFilterOptions;
_filters.displayFilters = { ..._filters.displayFilters, ...updatedDisplayFilters };
// set sub_group_by to null if group_by is set to null
if (_filters.displayFilters.group_by === null) {
_filters.displayFilters.sub_group_by = null;
updatedDisplayFilters.sub_group_by = null;
}
// set sub_group_by to null if layout is switched to kanban group_by and sub_group_by are same
if (
_filters.displayFilters.layout === "kanban" &&
_filters.displayFilters.group_by === _filters.displayFilters.sub_group_by
) {
_filters.displayFilters.sub_group_by = null;
updatedDisplayFilters.sub_group_by = null;
}
// set group_by to priority if layout is switched to kanban and group_by is null
if (_filters.displayFilters.layout === "kanban" && _filters.displayFilters.group_by === null) {
_filters.displayFilters.group_by = "priority";
updatedDisplayFilters.group_by = "priority";
}
runInAction(() => {
Object.keys(updatedDisplayFilters).forEach((_key) => {
set(
this.filters,
[workspaceSlug, "displayFilters", _key],
updatedDisplayFilters[_key as keyof IIssueDisplayFilterOptions]
);
});
});
this.rootIssueStore.profileIssues.fetchIssuesWithExistingPagination(workspaceSlug, workspaceSlug, "mutation");
this.handleIssuesLocalFilters.set(EIssuesStoreType.PROFILE, type, workspaceSlug, workspaceSlug, undefined, {
display_filters: _filters.displayFilters,
});
break;
}
case EIssueFilterType.DISPLAY_PROPERTIES: {
const updatedDisplayProperties = filters as IIssueDisplayProperties;
_filters.displayProperties = { ..._filters.displayProperties, ...updatedDisplayProperties };
runInAction(() => {
Object.keys(updatedDisplayProperties).forEach((_key) => {
set(
this.filters,
[workspaceSlug, "displayProperties", _key],
updatedDisplayProperties[_key as keyof IIssueDisplayProperties]
);
});
});
this.handleIssuesLocalFilters.set(EIssuesStoreType.PROFILE, type, workspaceSlug, workspaceSlug, undefined, {
display_properties: _filters.displayProperties,
});
break;
}
default:
break;
}
} catch (error) {
if (workspaceSlug) this.fetchFilters(workspaceSlug);
throw error;
}
};
}

View file

@ -0,0 +1,2 @@
export * from "./issue.store";
export * from "./filter.store";

View file

@ -0,0 +1,280 @@
import orderBy from "lodash/orderBy";
import set from "lodash/set";
import unset from "lodash/unset";
import update from "lodash/update";
import { action, computed, makeObservable, observable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
import {
TWorkspaceDraftIssue,
TWorkspaceDraftPaginationInfo,
TWorkspaceDraftIssueLoader,
TWorkspaceDraftQueryParams,
} from "@plane/types";
// constants
import { EDraftIssuePaginationType } from "@/constants/workspace-drafts";
// helpers
import { getCurrentDateTimeInISO, convertToISODateString } from "@/helpers/date-time.helper";
// services
import workspaceDraftService from "@/services/issue/workspace_draft.service";
import { IIssueDetail } from "../issue-details/root.store";
import { clone } from "lodash";
export type TDraftIssuePaginationType = EDraftIssuePaginationType;
export interface IWorkspaceDraftIssues {
// observables
issuesMap: Record<string, TWorkspaceDraftIssue>;
paginationInfo: Omit<TWorkspaceDraftPaginationInfo<TWorkspaceDraftIssue>, "results"> | undefined;
loader: TWorkspaceDraftIssueLoader;
// computed
issueIds: string[];
// computed functions
getIssueById: (issueId: string) => TWorkspaceDraftIssue | undefined;
// helper actions
addIssue: (issues: TWorkspaceDraftIssue[]) => void;
mutateIssue: (issueId: string, data: Partial<TWorkspaceDraftIssue>) => void;
removeIssue: (issueId: string) => void;
// actions
fetchIssues: (
workspaceSlug: string,
loadType: TWorkspaceDraftIssueLoader,
paginationType?: TDraftIssuePaginationType
) => Promise<TWorkspaceDraftPaginationInfo<TWorkspaceDraftIssue> | undefined>;
createIssue: (
workspaceSlug: string,
payload: Partial<TWorkspaceDraftIssue>
) => Promise<TWorkspaceDraftIssue | undefined>;
updateIssue: (
workspaceSlug: string,
issueId: string,
payload: Partial<TWorkspaceDraftIssue>
) => Promise<TWorkspaceDraftIssue | undefined>;
deleteIssue: (workspaceSlug: string, issueId: string) => Promise<void>;
moveIssue: (workspaceSlug: string, issueId: string, payload: Partial<TWorkspaceDraftIssue>) => Promise<void>;
addCycleToIssue: (
workspaceSlug: string,
issueId: string,
cycleId: string
) => Promise<TWorkspaceDraftIssue | undefined>;
addModulesToIssue: (
workspaceSlug: string,
issueId: string,
moduleIds: string[]
) => Promise<TWorkspaceDraftIssue | undefined>;
}
export class WorkspaceDraftIssues implements IWorkspaceDraftIssues {
// local constants
paginatedCount = 50;
// observables
paginationInfo: Omit<TWorkspaceDraftPaginationInfo<TWorkspaceDraftIssue>, "results"> | undefined = undefined;
loader: TWorkspaceDraftIssueLoader = undefined;
issuesMap: Record<string, TWorkspaceDraftIssue> = {};
constructor(private store: IIssueDetail) {
makeObservable(this, {
paginationInfo: observable,
loader: observable.ref,
issuesMap: observable,
// computed
issueIds: computed,
// action
fetchIssues: action,
createIssue: action,
updateIssue: action,
deleteIssue: action,
moveIssue: action,
addCycleToIssue: action,
addModulesToIssue: action,
});
}
// computed
get issueIds() {
if (Object.keys(this.issuesMap).length <= 0) return [];
return orderBy(Object.values(this.issuesMap), (issue) => convertToISODateString(issue["created_at"]), ["asc"]).map(
(issue) => issue?.id
);
}
// computed functions
getIssueById = computedFn((issueId: string) => {
if (!issueId || !this.issuesMap[issueId]) return undefined;
return this.issuesMap[issueId];
});
// helper actions
addIssue = (issues: TWorkspaceDraftIssue[]) => {
if (issues && issues.length <= 0) return;
runInAction(() => {
issues.forEach((issue) => {
if (!this.issuesMap[issue.id]) set(this.issuesMap, issue.id, issue);
else update(this.issuesMap, issue.id, (prevIssue) => ({ ...prevIssue, ...issue }));
});
});
};
mutateIssue = (issueId: string, issue: Partial<TWorkspaceDraftIssue>) => {
if (!issue || !issueId || !this.issuesMap[issueId]) return;
runInAction(() => {
set(this.issuesMap, [issueId, "updated_at"], getCurrentDateTimeInISO());
Object.keys(issue).forEach((key) => {
set(this.issuesMap, [issueId, key], issue[key as keyof TWorkspaceDraftIssue]);
});
});
};
removeIssue = (issueId: string) => {
if (!issueId || !this.issuesMap[issueId]) return;
runInAction(() => unset(this.issuesMap, issueId));
};
generateNotificationQueryParams = (
paramType: TDraftIssuePaginationType,
filterParams = {}
): TWorkspaceDraftQueryParams => {
const queryCursorNext: string =
paramType === EDraftIssuePaginationType.INIT
? `${this.paginatedCount}:0:0`
: paramType === EDraftIssuePaginationType.CURRENT
? `${this.paginatedCount}:${0}:0`
: paramType === EDraftIssuePaginationType.NEXT && this.paginationInfo
? (this.paginationInfo?.next_cursor ?? `${this.paginatedCount}:${0}:0`)
: `${this.paginatedCount}:${0}:0`;
const queryParams: TWorkspaceDraftQueryParams = {
per_page: this.paginatedCount,
cursor: queryCursorNext,
...filterParams,
};
return queryParams;
};
// actions
fetchIssues = async (
workspaceSlug: string,
loadType: TWorkspaceDraftIssueLoader,
paginationType: TDraftIssuePaginationType = EDraftIssuePaginationType.INIT
) => {
try {
this.loader = loadType;
// filter params and pagination params
const filterParams = {};
const params = this.generateNotificationQueryParams(paginationType, filterParams);
// fetching the paginated workspace draft issues
const draftIssuesResponse = await workspaceDraftService.getIssues(workspaceSlug, { ...params });
if (!draftIssuesResponse) return undefined;
const { results, ...paginationInfo } = draftIssuesResponse;
runInAction(() => {
if (results && results.length > 0) {
this.addIssue(results as TWorkspaceDraftIssue[]);
this.loader = undefined;
} else {
this.loader = "empty-state";
}
set(this, "paginationInfo", paginationInfo);
});
return draftIssuesResponse;
} catch (error) {
// set loader to undefined if errored out
this.loader = undefined;
throw error;
}
};
createIssue = async (
workspaceSlug: string,
payload: Partial<TWorkspaceDraftIssue>
): Promise<TWorkspaceDraftIssue | undefined> => {
try {
this.loader = "create";
const response = await workspaceDraftService.createIssue(workspaceSlug, payload);
if (response) {
runInAction(() => set(this.issuesMap, response.id, response));
}
this.loader = undefined;
return response;
} catch (error) {
this.loader = undefined;
throw error;
}
};
updateIssue = async (workspaceSlug: string, issueId: string, payload: Partial<TWorkspaceDraftIssue>) => {
const issueBeforeUpdate = clone(this.getIssueById(issueId));
try {
this.loader = "update";
runInAction(() => {
set(this.issuesMap, [issueId, "updated_at"], getCurrentDateTimeInISO());
set(this.issuesMap, [issueId], { ...issueBeforeUpdate, ...payload });
});
const response = await workspaceDraftService.updateIssue(workspaceSlug, issueId, payload);
this.loader = undefined;
return response;
} catch (error) {
this.loader = undefined;
runInAction(() => {
set(this.issuesMap, [issueId], issueBeforeUpdate);
});
throw error;
}
};
deleteIssue = async (workspaceSlug: string, issueId: string) => {
try {
this.loader = "delete";
const response = await workspaceDraftService.deleteIssue(workspaceSlug, issueId);
runInAction(() => unset(this.issuesMap, issueId));
this.loader = undefined;
return response;
} catch (error) {
this.loader = undefined;
throw error;
}
};
moveIssue = async (workspaceSlug: string, issueId: string, payload: Partial<TWorkspaceDraftIssue>) => {
try {
this.loader = "move";
const response = await workspaceDraftService.moveIssue(workspaceSlug, issueId, payload);
runInAction(() => unset(this.issuesMap, issueId));
this.loader = undefined;
return response;
} catch (error) {
this.loader = undefined;
throw error;
}
};
addCycleToIssue = async (workspaceSlug: string, issueId: string, cycleId: string) => {
try {
this.loader = "update";
const response = await this.updateIssue(workspaceSlug, issueId, { cycle_id: cycleId });
return response;
} catch (error) {
this.loader = undefined;
throw error;
}
};
addModulesToIssue = async (workspaceSlug: string, issueId: string, moduleIds: string[]) => {
try {
this.loader = "update";
const response = this.updateIssue(workspaceSlug, issueId, { module_ids: moduleIds });
return response;
} catch (error) {
this.loader = undefined;
throw error;
}
};
}