Initial commit: Plane
Some checks failed
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Sync Repositories / sync_changes (push) Has been cancelled
Some checks failed
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Sync Repositories / sync_changes (push) Has been cancelled
Synced from upstream: 8853637e981ed7d8a6cff32bd98e7afe20f54362
This commit is contained in:
294
apps/web/core/store/issue/archived/filter.store.ts
Normal file
294
apps/web/core/store/issue/archived/filter.store.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
import { isEmpty, set } from "lodash-es";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
// base class
|
||||
import { computedFn } from "mobx-utils";
|
||||
import type { TSupportedFilterTypeForUpdate } from "@plane/constants";
|
||||
import { EIssueFilterType } from "@plane/constants";
|
||||
import type {
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
TIssueKanbanFilters,
|
||||
IIssueFilters,
|
||||
TIssueParams,
|
||||
IssuePaginationOptions,
|
||||
TWorkItemFilterExpression,
|
||||
TSupportedFilterForUpdate,
|
||||
} from "@plane/types";
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
import { handleIssueQueryParamsByLayout } from "@plane/utils";
|
||||
import { IssueFiltersService } from "@/services/issue_filter.service";
|
||||
import type { IBaseIssueFilterStore } from "../helpers/issue-filter-helper.store";
|
||||
import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store";
|
||||
// helpers
|
||||
// types
|
||||
import type { IIssueRootStore } from "../root.store";
|
||||
// constants
|
||||
// services
|
||||
|
||||
export interface IArchivedIssuesFilter extends IBaseIssueFilterStore {
|
||||
//helper actions
|
||||
getFilterParams: (
|
||||
options: IssuePaginationOptions,
|
||||
projectId: string,
|
||||
cursor: string | undefined,
|
||||
groupId: string | undefined,
|
||||
subGroupId: string | undefined
|
||||
) => Partial<Record<TIssueParams, string | boolean>>;
|
||||
getIssueFilters(projectId: string): IIssueFilters | undefined;
|
||||
// action
|
||||
fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||
updateFilterExpression: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
filters: TWorkItemFilterExpression
|
||||
) => Promise<void>;
|
||||
updateFilters: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
filterType: TSupportedFilterTypeForUpdate,
|
||||
filters: TSupportedFilterForUpdate
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export class ArchivedIssuesFilter extends IssueFilterHelperStore implements IArchivedIssuesFilter {
|
||||
// observables
|
||||
filters: { [projectId: string]: IIssueFilters } = {};
|
||||
// root store
|
||||
rootIssueStore;
|
||||
// services
|
||||
issueFilterService;
|
||||
|
||||
constructor(_rootStore: IIssueRootStore) {
|
||||
super();
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
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 projectId = this.rootIssueStore.projectId;
|
||||
if (!projectId) return undefined;
|
||||
|
||||
return this.getIssueFilters(projectId);
|
||||
}
|
||||
|
||||
get appliedFilters() {
|
||||
const projectId = this.rootIssueStore.projectId;
|
||||
if (!projectId) return undefined;
|
||||
|
||||
return this.getAppliedFilters(projectId);
|
||||
}
|
||||
|
||||
getIssueFilters(projectId: string) {
|
||||
const displayFilters = this.filters[projectId] || undefined;
|
||||
if (isEmpty(displayFilters)) return undefined;
|
||||
|
||||
const _filters: IIssueFilters = this.computedIssueFilters(displayFilters);
|
||||
|
||||
return _filters;
|
||||
}
|
||||
|
||||
getAppliedFilters(projectId: string) {
|
||||
const userFilters = this.getIssueFilters(projectId);
|
||||
if (!userFilters) return undefined;
|
||||
|
||||
const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "issues");
|
||||
if (!filteredParams) return undefined;
|
||||
|
||||
const filteredRouteParams: Partial<Record<TIssueParams, string | boolean>> = this.computedFilteredParams(
|
||||
userFilters?.richFilters,
|
||||
userFilters?.displayFilters,
|
||||
filteredParams
|
||||
);
|
||||
|
||||
return filteredRouteParams;
|
||||
}
|
||||
|
||||
getFilterParams = computedFn(
|
||||
(
|
||||
options: IssuePaginationOptions,
|
||||
projectId: string,
|
||||
cursor: string | undefined,
|
||||
groupId: string | undefined,
|
||||
subGroupId: string | undefined
|
||||
) => {
|
||||
const filterParams = this.getAppliedFilters(projectId);
|
||||
|
||||
const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId);
|
||||
return paginationParams;
|
||||
}
|
||||
);
|
||||
|
||||
fetchFilters = async (workspaceSlug: string, projectId: string) => {
|
||||
const _filters = this.handleIssuesLocalFilters.get(EIssuesStoreType.ARCHIVED, workspaceSlug, projectId, undefined);
|
||||
|
||||
const richFilters: TWorkItemFilterExpression = _filters?.richFilters;
|
||||
const displayFilters: IIssueDisplayFilterOptions = this.computedDisplayFilters({
|
||||
..._filters?.display_filters,
|
||||
sub_issue: true,
|
||||
});
|
||||
const displayProperties: IIssueDisplayProperties = this.computedDisplayProperties(_filters?.display_properties);
|
||||
const kanbanFilters = {
|
||||
group_by: [],
|
||||
sub_group_by: [],
|
||||
};
|
||||
kanbanFilters.group_by = _filters?.kanban_filters?.group_by || [];
|
||||
kanbanFilters.sub_group_by = _filters?.kanban_filters?.sub_group_by || [];
|
||||
|
||||
runInAction(() => {
|
||||
set(this.filters, [projectId, "richFilters"], richFilters);
|
||||
set(this.filters, [projectId, "displayFilters"], displayFilters);
|
||||
set(this.filters, [projectId, "displayProperties"], displayProperties);
|
||||
set(this.filters, [projectId, "kanbanFilters"], kanbanFilters);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* NOTE: This method is designed as a fallback function for the work item filter store.
|
||||
* Only use this method directly when initializing filter instances.
|
||||
* For regular filter updates, use this method as a fallback function for the work item filter store methods instead.
|
||||
*/
|
||||
updateFilterExpression: IArchivedIssuesFilter["updateFilterExpression"] = async (
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
filters
|
||||
) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
set(this.filters, [projectId, "richFilters"], filters);
|
||||
});
|
||||
|
||||
this.rootIssueStore.archivedIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation");
|
||||
this.handleIssuesLocalFilters.set(
|
||||
EIssuesStoreType.ARCHIVED,
|
||||
EIssueFilterType.FILTERS,
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
undefined,
|
||||
{
|
||||
rich_filters: filters,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("error while updating rich filters", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
updateFilters: IArchivedIssuesFilter["updateFilters"] = async (workspaceSlug, projectId, type, filters) => {
|
||||
try {
|
||||
if (isEmpty(this.filters) || isEmpty(this.filters[projectId])) return;
|
||||
|
||||
const _filters = {
|
||||
richFilters: this.filters[projectId].richFilters,
|
||||
displayFilters: this.filters[projectId].displayFilters as IIssueDisplayFilterOptions,
|
||||
displayProperties: this.filters[projectId].displayProperties as IIssueDisplayProperties,
|
||||
kanbanFilters: this.filters[projectId].kanbanFilters as TIssueKanbanFilters,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
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 state 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 = "state";
|
||||
updatedDisplayFilters.group_by = "state";
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
Object.keys(updatedDisplayFilters).forEach((_key) => {
|
||||
set(
|
||||
this.filters,
|
||||
[projectId, "displayFilters", _key],
|
||||
updatedDisplayFilters[_key as keyof IIssueDisplayFilterOptions]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
if (this.getShouldReFetchIssues(updatedDisplayFilters)) {
|
||||
this.rootIssueStore.archivedIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation");
|
||||
}
|
||||
|
||||
this.handleIssuesLocalFilters.set(EIssuesStoreType.ARCHIVED, type, workspaceSlug, projectId, 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,
|
||||
[projectId, "displayProperties", _key],
|
||||
updatedDisplayProperties[_key as keyof IIssueDisplayProperties]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
this.handleIssuesLocalFilters.set(EIssuesStoreType.ARCHIVED, type, workspaceSlug, projectId, undefined, {
|
||||
display_properties: _filters.displayProperties,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case EIssueFilterType.KANBAN_FILTERS: {
|
||||
const updatedKanbanFilters = filters as TIssueKanbanFilters;
|
||||
_filters.kanbanFilters = { ..._filters.kanbanFilters, ...updatedKanbanFilters };
|
||||
|
||||
const currentUserId = this.rootIssueStore.currentUserId;
|
||||
if (currentUserId)
|
||||
this.handleIssuesLocalFilters.set(EIssuesStoreType.ARCHIVED, type, workspaceSlug, projectId, undefined, {
|
||||
kanban_filters: _filters.kanbanFilters,
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
Object.keys(updatedKanbanFilters).forEach((_key) => {
|
||||
set(
|
||||
this.filters,
|
||||
[projectId, "kanbanFilters", _key],
|
||||
updatedKanbanFilters[_key as keyof TIssueKanbanFilters]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
this.fetchFilters(workspaceSlug, projectId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
2
apps/web/core/store/issue/archived/index.ts
Normal file
2
apps/web/core/store/issue/archived/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./filter.store";
|
||||
export * from "./issue.store";
|
||||
202
apps/web/core/store/issue/archived/issue.store.ts
Normal file
202
apps/web/core/store/issue/archived/issue.store.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import { action, makeObservable, runInAction } from "mobx";
|
||||
// base class
|
||||
import type { TLoader, IssuePaginationOptions, TIssuesResponse, ViewFlags, TBulkOperationsPayload } from "@plane/types";
|
||||
// services
|
||||
// types
|
||||
import type { IBaseIssuesStore } from "../helpers/base-issues.store";
|
||||
import { BaseIssuesStore } from "../helpers/base-issues.store";
|
||||
import type { IIssueRootStore } from "../root.store";
|
||||
import type { IArchivedIssuesFilter } from "./filter.store";
|
||||
|
||||
export interface IArchivedIssues extends IBaseIssuesStore {
|
||||
// observable
|
||||
viewFlags: ViewFlags;
|
||||
// actions
|
||||
fetchIssues: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
loadType: TLoader,
|
||||
option: IssuePaginationOptions
|
||||
) => Promise<TIssuesResponse | undefined>;
|
||||
fetchIssuesWithExistingPagination: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
loadType: TLoader
|
||||
) => Promise<TIssuesResponse | undefined>;
|
||||
fetchNextIssues: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
groupId?: string,
|
||||
subGroupId?: string
|
||||
) => Promise<TIssuesResponse | undefined>;
|
||||
|
||||
restoreIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
removeBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise<void>;
|
||||
bulkUpdateProperties: (workspaceSlug: string, projectId: string, data: TBulkOperationsPayload) => Promise<void>;
|
||||
|
||||
updateIssue: undefined;
|
||||
archiveIssue: undefined;
|
||||
archiveBulkIssues: undefined;
|
||||
quickAddIssue: undefined;
|
||||
}
|
||||
|
||||
export class ArchivedIssues extends BaseIssuesStore implements IArchivedIssues {
|
||||
// filter store
|
||||
issueFilterStore: IArchivedIssuesFilter;
|
||||
|
||||
//viewData
|
||||
viewFlags = {
|
||||
enableQuickAdd: false,
|
||||
enableIssueCreation: false,
|
||||
enableInlineEditing: true,
|
||||
};
|
||||
|
||||
constructor(_rootStore: IIssueRootStore, issueFilterStore: IArchivedIssuesFilter) {
|
||||
super(_rootStore, issueFilterStore, true);
|
||||
makeObservable(this, {
|
||||
// action
|
||||
fetchIssues: action,
|
||||
fetchNextIssues: action,
|
||||
fetchIssuesWithExistingPagination: action,
|
||||
|
||||
restoreIssue: action,
|
||||
});
|
||||
// filter store
|
||||
this.issueFilterStore = issueFilterStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the project details
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
*/
|
||||
fetchParentStats = async (workspaceSlug: string, projectId?: string) => {
|
||||
projectId && this.rootIssueStore.rootStore.projectRoot.project.fetchProjectDetails(workspaceSlug, projectId);
|
||||
};
|
||||
|
||||
/** */
|
||||
updateParentStats = () => {};
|
||||
|
||||
/**
|
||||
* This method is called to fetch the first issues of pagination
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param loadType
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
fetchIssues = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
loadType: TLoader = "init-loader",
|
||||
options: IssuePaginationOptions,
|
||||
isExistingPaginationOptions: boolean = false
|
||||
) => {
|
||||
try {
|
||||
// set loader and clear store
|
||||
runInAction(() => {
|
||||
this.setLoader(loadType);
|
||||
});
|
||||
this.clear(!isExistingPaginationOptions);
|
||||
|
||||
// get params from pagination options
|
||||
const params = this.issueFilterStore?.getFilterParams(options, projectId, undefined, undefined, undefined);
|
||||
// call the fetch issues API with the params
|
||||
const response = await this.issueArchiveService.getArchivedIssues(workspaceSlug, projectId, params, {
|
||||
signal: this.controller.signal,
|
||||
});
|
||||
|
||||
// after fetching issues, call the base method to process the response further
|
||||
this.onfetchIssues(response, options, workspaceSlug, projectId, undefined, !isExistingPaginationOptions);
|
||||
return response;
|
||||
} catch (error) {
|
||||
// set loader to undefined if errored out
|
||||
this.setLoader(undefined);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This method is called subsequent pages of pagination
|
||||
* if groupId/subgroupId is provided, only that specific group's next page is fetched
|
||||
* else all the groups' next page is fetched
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param groupId
|
||||
* @param subGroupId
|
||||
* @returns
|
||||
*/
|
||||
fetchNextIssues = async (workspaceSlug: string, projectId: string, groupId?: string, subGroupId?: string) => {
|
||||
const cursorObject = this.getPaginationData(groupId, subGroupId);
|
||||
// if there are no pagination options and the next page results do not exist the return
|
||||
if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return;
|
||||
try {
|
||||
// set Loader
|
||||
this.setLoader("pagination", groupId, subGroupId);
|
||||
|
||||
// get params from stored pagination options
|
||||
const params = this.issueFilterStore?.getFilterParams(
|
||||
this.paginationOptions,
|
||||
projectId,
|
||||
this.getNextCursor(groupId, subGroupId),
|
||||
groupId,
|
||||
subGroupId
|
||||
);
|
||||
// call the fetch issues API with the params for next page in issues
|
||||
const response = await this.issueArchiveService.getArchivedIssues(workspaceSlug, projectId, params);
|
||||
|
||||
// after the next page of issues are fetched, call the base method to process the response
|
||||
this.onfetchNexIssues(response, groupId, subGroupId);
|
||||
return response;
|
||||
} catch (error) {
|
||||
// set Loader as undefined if errored out
|
||||
this.setLoader(undefined, groupId, subGroupId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This Method exists to fetch the first page of the issues with the existing stored pagination
|
||||
* This is useful for refetching when filters, groupBy, orderBy etc changes
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param loadType
|
||||
* @returns
|
||||
*/
|
||||
fetchIssuesWithExistingPagination = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
loadType: TLoader = "mutation"
|
||||
) => {
|
||||
if (!this.paginationOptions) return;
|
||||
return await this.fetchIssues(workspaceSlug, projectId, loadType, this.paginationOptions, true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Restored the current issue from the archived issue
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param issueId
|
||||
* @returns
|
||||
*/
|
||||
restoreIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
// call API to restore the issue
|
||||
const response = await this.issueArchiveService.restoreIssue(workspaceSlug, projectId, issueId);
|
||||
|
||||
// update the store and remove from the archived issues list once restored
|
||||
runInAction(() => {
|
||||
this.rootIssueStore.issues.updateIssue(issueId, {
|
||||
archived_at: null,
|
||||
});
|
||||
this.removeIssueFromList(issueId);
|
||||
});
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
// Setting them as undefined as they can not performed on Archived issues
|
||||
updateIssue = undefined;
|
||||
archiveIssue = undefined;
|
||||
archiveBulkIssues = undefined;
|
||||
quickAddIssue = undefined;
|
||||
}
|
||||
314
apps/web/core/store/issue/cycle/filter.store.ts
Normal file
314
apps/web/core/store/issue/cycle/filter.store.ts
Normal file
@@ -0,0 +1,314 @@
|
||||
import { isEmpty, set } from "lodash-es";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
// base class
|
||||
import { computedFn } from "mobx-utils";
|
||||
import type { TSupportedFilterTypeForUpdate } from "@plane/constants";
|
||||
import { EIssueFilterType } from "@plane/constants";
|
||||
import type {
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
TIssueKanbanFilters,
|
||||
IIssueFilters,
|
||||
TIssueParams,
|
||||
IssuePaginationOptions,
|
||||
TWorkItemFilterExpression,
|
||||
TSupportedFilterForUpdate,
|
||||
} from "@plane/types";
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
import { handleIssueQueryParamsByLayout } from "@plane/utils";
|
||||
import { IssueFiltersService } from "@/services/issue_filter.service";
|
||||
import type { IBaseIssueFilterStore } from "../helpers/issue-filter-helper.store";
|
||||
import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store";
|
||||
// helpers
|
||||
// types
|
||||
import type { IIssueRootStore } from "../root.store";
|
||||
// constants
|
||||
// services
|
||||
|
||||
export interface ICycleIssuesFilter extends IBaseIssueFilterStore {
|
||||
//helper actions
|
||||
getFilterParams: (
|
||||
options: IssuePaginationOptions,
|
||||
cycleId: string,
|
||||
cursor: string | undefined,
|
||||
groupId: string | undefined,
|
||||
subGroupId: string | undefined
|
||||
) => Partial<Record<TIssueParams, string | boolean>>;
|
||||
getIssueFilters(cycleId: string): IIssueFilters | undefined;
|
||||
// action
|
||||
fetchFilters: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<void>;
|
||||
updateFilterExpression: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
cycleId: string,
|
||||
filters: TWorkItemFilterExpression
|
||||
) => Promise<void>;
|
||||
updateFilters: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
filterType: TSupportedFilterTypeForUpdate,
|
||||
filters: TSupportedFilterForUpdate,
|
||||
cycleId: string
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleIssuesFilter {
|
||||
// observables
|
||||
filters: { [cycleId: string]: IIssueFilters } = {};
|
||||
// root store
|
||||
rootIssueStore: IIssueRootStore;
|
||||
// services
|
||||
issueFilterService;
|
||||
|
||||
constructor(_rootStore: IIssueRootStore) {
|
||||
super();
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
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 cycleId = this.rootIssueStore.cycleId;
|
||||
if (!cycleId) return undefined;
|
||||
|
||||
return this.getIssueFilters(cycleId);
|
||||
}
|
||||
|
||||
get appliedFilters() {
|
||||
const cycleId = this.rootIssueStore.cycleId;
|
||||
if (!cycleId) return undefined;
|
||||
|
||||
return this.getAppliedFilters(cycleId);
|
||||
}
|
||||
|
||||
getIssueFilters(cycleId: string) {
|
||||
const displayFilters = this.filters[cycleId] || undefined;
|
||||
if (isEmpty(displayFilters)) return undefined;
|
||||
|
||||
const _filters: IIssueFilters = this.computedIssueFilters(displayFilters);
|
||||
|
||||
return _filters;
|
||||
}
|
||||
|
||||
getAppliedFilters(cycleId: string) {
|
||||
const userFilters = this.getIssueFilters(cycleId);
|
||||
if (!userFilters) return undefined;
|
||||
|
||||
const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "issues");
|
||||
if (!filteredParams) return undefined;
|
||||
|
||||
if (filteredParams.includes("cycle")) filteredParams.splice(filteredParams.indexOf("cycle"), 1);
|
||||
|
||||
const filteredRouteParams: Partial<Record<TIssueParams, string | boolean>> = this.computedFilteredParams(
|
||||
userFilters?.richFilters,
|
||||
userFilters?.displayFilters,
|
||||
filteredParams
|
||||
);
|
||||
|
||||
return filteredRouteParams;
|
||||
}
|
||||
|
||||
getFilterParams = computedFn(
|
||||
(
|
||||
options: IssuePaginationOptions,
|
||||
cycleId: string,
|
||||
cursor: string | undefined,
|
||||
groupId: string | undefined,
|
||||
subGroupId: string | undefined
|
||||
) => {
|
||||
let filterParams = this.getAppliedFilters(cycleId);
|
||||
|
||||
if (!filterParams) {
|
||||
filterParams = {};
|
||||
}
|
||||
filterParams["cycle"] = cycleId;
|
||||
|
||||
const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId);
|
||||
return paginationParams;
|
||||
}
|
||||
);
|
||||
|
||||
fetchFilters = async (workspaceSlug: string, projectId: string, cycleId: string) => {
|
||||
const _filters = await this.issueFilterService.fetchCycleIssueFilters(workspaceSlug, projectId, cycleId);
|
||||
|
||||
const richFilters: TWorkItemFilterExpression = _filters?.rich_filters;
|
||||
const displayFilters: IIssueDisplayFilterOptions = this.computedDisplayFilters(_filters?.display_filters);
|
||||
const displayProperties: IIssueDisplayProperties = this.computedDisplayProperties(_filters?.display_properties);
|
||||
|
||||
// fetching the kanban toggle helpers in the local storage
|
||||
const kanbanFilters = {
|
||||
group_by: [],
|
||||
sub_group_by: [],
|
||||
};
|
||||
const currentUserId = this.rootIssueStore.currentUserId;
|
||||
if (currentUserId) {
|
||||
const _kanbanFilters = this.handleIssuesLocalFilters.get(
|
||||
EIssuesStoreType.CYCLE,
|
||||
workspaceSlug,
|
||||
cycleId,
|
||||
currentUserId
|
||||
);
|
||||
kanbanFilters.group_by = _kanbanFilters?.kanban_filters?.group_by || [];
|
||||
kanbanFilters.sub_group_by = _kanbanFilters?.kanban_filters?.sub_group_by || [];
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
set(this.filters, [cycleId, "richFilters"], richFilters);
|
||||
set(this.filters, [cycleId, "displayFilters"], displayFilters);
|
||||
set(this.filters, [cycleId, "displayProperties"], displayProperties);
|
||||
set(this.filters, [cycleId, "kanbanFilters"], kanbanFilters);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* NOTE: This method is designed as a fallback function for the work item filter store.
|
||||
* Only use this method directly when initializing filter instances.
|
||||
* For regular filter updates, use this method as a fallback function for the work item filter store methods instead.
|
||||
*/
|
||||
updateFilterExpression: ICycleIssuesFilter["updateFilterExpression"] = async (
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
cycleId,
|
||||
filters
|
||||
) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
set(this.filters, [cycleId, "richFilters"], filters);
|
||||
});
|
||||
|
||||
this.rootIssueStore.cycleIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation", cycleId);
|
||||
await this.issueFilterService.patchCycleIssueFilters(workspaceSlug, projectId, cycleId, {
|
||||
rich_filters: filters,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("error while updating rich filters", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
updateFilters: ICycleIssuesFilter["updateFilters"] = async (workspaceSlug, projectId, type, filters, cycleId) => {
|
||||
try {
|
||||
if (isEmpty(this.filters) || isEmpty(this.filters[cycleId])) return;
|
||||
|
||||
const _filters = {
|
||||
richFilters: this.filters[cycleId].richFilters as TWorkItemFilterExpression,
|
||||
displayFilters: this.filters[cycleId].displayFilters as IIssueDisplayFilterOptions,
|
||||
displayProperties: this.filters[cycleId].displayProperties as IIssueDisplayProperties,
|
||||
kanbanFilters: this.filters[cycleId].kanbanFilters as TIssueKanbanFilters,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
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 state 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 = "state";
|
||||
updatedDisplayFilters.group_by = "state";
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
Object.keys(updatedDisplayFilters).forEach((_key) => {
|
||||
set(
|
||||
this.filters,
|
||||
[cycleId, "displayFilters", _key],
|
||||
updatedDisplayFilters[_key as keyof IIssueDisplayFilterOptions]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
if (this.getShouldClearIssues(updatedDisplayFilters)) {
|
||||
this.rootIssueStore.cycleIssues.clear(true, true); // clear issues for local store when some filters like layout changes
|
||||
}
|
||||
|
||||
if (this.getShouldReFetchIssues(updatedDisplayFilters)) {
|
||||
this.rootIssueStore.cycleIssues.fetchIssuesWithExistingPagination(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
"mutation",
|
||||
cycleId
|
||||
);
|
||||
}
|
||||
|
||||
await this.issueFilterService.patchCycleIssueFilters(workspaceSlug, projectId, cycleId, {
|
||||
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,
|
||||
[cycleId, "displayProperties", _key],
|
||||
updatedDisplayProperties[_key as keyof IIssueDisplayProperties]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
await this.issueFilterService.patchCycleIssueFilters(workspaceSlug, projectId, cycleId, {
|
||||
display_properties: _filters.displayProperties,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case EIssueFilterType.KANBAN_FILTERS: {
|
||||
const updatedKanbanFilters = filters as TIssueKanbanFilters;
|
||||
_filters.kanbanFilters = { ..._filters.kanbanFilters, ...updatedKanbanFilters };
|
||||
|
||||
const currentUserId = this.rootIssueStore.currentUserId;
|
||||
if (currentUserId)
|
||||
this.handleIssuesLocalFilters.set(EIssuesStoreType.CYCLE, type, workspaceSlug, cycleId, currentUserId, {
|
||||
kanban_filters: _filters.kanbanFilters,
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
Object.keys(updatedKanbanFilters).forEach((_key) => {
|
||||
set(
|
||||
this.filters,
|
||||
[cycleId, "kanbanFilters", _key],
|
||||
updatedKanbanFilters[_key as keyof TIssueKanbanFilters]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
if (cycleId) this.fetchFilters(workspaceSlug, projectId, cycleId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
2
apps/web/core/store/issue/cycle/index.ts
Normal file
2
apps/web/core/store/issue/cycle/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./filter.store";
|
||||
export * from "./issue.store";
|
||||
437
apps/web/core/store/issue/cycle/issue.store.ts
Normal file
437
apps/web/core/store/issue/cycle/issue.store.ts
Normal file
@@ -0,0 +1,437 @@
|
||||
import { get, set, concat, uniq, update } from "lodash-es";
|
||||
import { action, observable, makeObservable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// plane imports
|
||||
import { ALL_ISSUES } from "@plane/constants";
|
||||
import type {
|
||||
TIssue,
|
||||
TLoader,
|
||||
IssuePaginationOptions,
|
||||
TIssuesResponse,
|
||||
ViewFlags,
|
||||
TBulkOperationsPayload,
|
||||
} from "@plane/types";
|
||||
// helpers
|
||||
import { getDistributionPathsPostUpdate } from "@plane/utils";
|
||||
//local
|
||||
import { storage } from "@/lib/local-storage";
|
||||
import { persistence } from "@/local-db/storage.sqlite";
|
||||
import type { IBaseIssuesStore } from "../helpers/base-issues.store";
|
||||
import { BaseIssuesStore } from "../helpers/base-issues.store";
|
||||
//
|
||||
import type { IIssueRootStore } from "../root.store";
|
||||
import type { ICycleIssuesFilter } from "./filter.store";
|
||||
|
||||
export const ACTIVE_CYCLE_ISSUES = "ACTIVE_CYCLE_ISSUES";
|
||||
|
||||
export interface ActiveCycleIssueDetails {
|
||||
issueIds: string[];
|
||||
issueCount: number;
|
||||
nextCursor: string;
|
||||
nextPageResults: boolean;
|
||||
perPageCount: number;
|
||||
}
|
||||
|
||||
export interface ICycleIssues extends IBaseIssuesStore {
|
||||
viewFlags: ViewFlags;
|
||||
activeCycleIds: Record<string, ActiveCycleIssueDetails>;
|
||||
//action helpers
|
||||
getActiveCycleById: (cycleId: string) => ActiveCycleIssueDetails | undefined;
|
||||
// actions
|
||||
getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined;
|
||||
fetchIssues: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
loadType: TLoader,
|
||||
options: IssuePaginationOptions,
|
||||
cycleId: string
|
||||
) => Promise<TIssuesResponse | undefined>;
|
||||
fetchIssuesWithExistingPagination: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
loadType: TLoader,
|
||||
cycleId: string
|
||||
) => Promise<TIssuesResponse | undefined>;
|
||||
fetchNextIssues: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
cycleId: string,
|
||||
groupId?: string,
|
||||
subGroupId?: string
|
||||
) => Promise<TIssuesResponse | undefined>;
|
||||
|
||||
fetchActiveCycleIssues: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
perPageCount: number,
|
||||
cycleId: string
|
||||
) => Promise<TIssuesResponse | undefined>;
|
||||
fetchNextActiveCycleIssues: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
cycleId: string
|
||||
) => Promise<TIssuesResponse | undefined>;
|
||||
|
||||
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>, cycleId: string) => Promise<TIssue>;
|
||||
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||
archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
quickAddIssue: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: TIssue,
|
||||
cycleId: string
|
||||
) => Promise<TIssue | undefined>;
|
||||
removeBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise<void>;
|
||||
archiveBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise<void>;
|
||||
bulkUpdateProperties: (workspaceSlug: string, projectId: string, data: TBulkOperationsPayload) => Promise<void>;
|
||||
|
||||
transferIssuesFromCycle: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
cycleId: string,
|
||||
payload: {
|
||||
new_cycle_id: string;
|
||||
}
|
||||
) => Promise<TIssue>;
|
||||
}
|
||||
|
||||
export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
|
||||
activeCycleIds: Record<string, ActiveCycleIssueDetails> = {};
|
||||
viewFlags = {
|
||||
enableQuickAdd: true,
|
||||
enableIssueCreation: true,
|
||||
enableInlineEditing: true,
|
||||
};
|
||||
// filter store
|
||||
issueFilterStore;
|
||||
|
||||
constructor(_rootStore: IIssueRootStore, issueFilterStore: ICycleIssuesFilter) {
|
||||
super(_rootStore, issueFilterStore);
|
||||
makeObservable(this, {
|
||||
// observable
|
||||
activeCycleIds: observable,
|
||||
// action
|
||||
fetchIssues: action,
|
||||
fetchNextIssues: action,
|
||||
fetchIssuesWithExistingPagination: action,
|
||||
|
||||
transferIssuesFromCycle: action,
|
||||
fetchActiveCycleIssues: action,
|
||||
|
||||
quickAddIssue: action,
|
||||
});
|
||||
// filter store
|
||||
this.issueFilterStore = issueFilterStore;
|
||||
}
|
||||
|
||||
getActiveCycleById = computedFn((cycleId: string) => this.activeCycleIds[cycleId]);
|
||||
|
||||
/**
|
||||
* Fetches the cycle details
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param id is the cycle Id
|
||||
*/
|
||||
fetchParentStats = (workspaceSlug: string, projectId?: string | undefined, id?: string | undefined) => {
|
||||
const cycleId = id ?? this.cycleId;
|
||||
|
||||
if (projectId && cycleId) {
|
||||
this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId);
|
||||
}
|
||||
// fetch cycle progress
|
||||
const isSidebarCollapsed = storage.get("cycle_sidebar_collapsed");
|
||||
if (
|
||||
projectId &&
|
||||
cycleId &&
|
||||
this.rootIssueStore.rootStore.cycle.getCycleById(cycleId)?.version === 2 &&
|
||||
isSidebarCollapsed &&
|
||||
JSON.parse(isSidebarCollapsed) === false
|
||||
) {
|
||||
this.rootIssueStore.rootStore.cycle.fetchActiveCycleProgressPro(workspaceSlug, projectId, cycleId);
|
||||
}
|
||||
};
|
||||
|
||||
updateParentStats = (prevIssueState?: TIssue, nextIssueState?: TIssue, id?: string | undefined) => {
|
||||
try {
|
||||
const distributionUpdates = getDistributionPathsPostUpdate(
|
||||
prevIssueState,
|
||||
nextIssueState,
|
||||
this.rootIssueStore.rootStore.state.stateMap,
|
||||
this.rootIssueStore.rootStore.projectEstimate?.currentActiveEstimate?.estimatePointById
|
||||
);
|
||||
|
||||
const cycleId = id ?? this.cycleId;
|
||||
if (cycleId) {
|
||||
this.rootIssueStore.rootStore.cycle.updateCycleDistribution(distributionUpdates, cycleId);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("could not update cycle statistics");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This method is called to fetch the first issues of pagination
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param loadType
|
||||
* @param options
|
||||
* @param cycleId
|
||||
* @returns
|
||||
*/
|
||||
fetchIssues = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
loadType: TLoader,
|
||||
options: IssuePaginationOptions,
|
||||
cycleId: string,
|
||||
isExistingPaginationOptions: boolean = false
|
||||
) => {
|
||||
try {
|
||||
// set loader and clear store
|
||||
runInAction(() => {
|
||||
this.setLoader(loadType);
|
||||
this.clear(!isExistingPaginationOptions, false); // clear while fetching from server.
|
||||
if (!this.groupBy) this.clear(!isExistingPaginationOptions, true); // clear while using local to have the no load effect.
|
||||
});
|
||||
|
||||
// get params from pagination options
|
||||
const params = this.issueFilterStore?.getFilterParams(options, cycleId, undefined, undefined, undefined);
|
||||
// call the fetch issues API with the params
|
||||
const response = await this.issueService.getIssues(workspaceSlug, projectId, params, {
|
||||
signal: this.controller.signal,
|
||||
});
|
||||
|
||||
// after fetching issues, call the base method to process the response further
|
||||
this.onfetchIssues(response, options, workspaceSlug, projectId, cycleId, !isExistingPaginationOptions);
|
||||
return response;
|
||||
} catch (error) {
|
||||
// set loader to undefined once errored out
|
||||
this.setLoader(undefined);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This method is called subsequent pages of pagination
|
||||
* if groupId/subgroupId is provided, only that specific group's next page is fetched
|
||||
* else all the groups' next page is fetched
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param cycleId
|
||||
* @param groupId
|
||||
* @param subGroupId
|
||||
* @returns
|
||||
*/
|
||||
fetchNextIssues = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
cycleId: string,
|
||||
groupId?: string,
|
||||
subGroupId?: string
|
||||
) => {
|
||||
const cursorObject = this.getPaginationData(groupId, subGroupId);
|
||||
// if there are no pagination options and the next page results do not exist the return
|
||||
if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return;
|
||||
try {
|
||||
// set Loader
|
||||
this.setLoader("pagination", groupId, subGroupId);
|
||||
|
||||
// get params from stored pagination options
|
||||
const params = this.issueFilterStore?.getFilterParams(
|
||||
this.paginationOptions,
|
||||
cycleId,
|
||||
this.getNextCursor(groupId, subGroupId),
|
||||
groupId,
|
||||
subGroupId
|
||||
);
|
||||
// call the fetch issues API with the params for next page in issues
|
||||
const response = await this.issueService.getIssues(workspaceSlug, projectId, params);
|
||||
|
||||
// after the next page of issues are fetched, call the base method to process the response
|
||||
this.onfetchNexIssues(response, groupId, subGroupId);
|
||||
return response;
|
||||
} catch (error) {
|
||||
// set Loader as undefined if errored out
|
||||
this.setLoader(undefined, groupId, subGroupId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This Method exists to fetch the first page of the issues with the existing stored pagination
|
||||
* This is useful for refetching when filters, groupBy, orderBy etc changes
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param loadType
|
||||
* @param cycleId
|
||||
* @returns
|
||||
*/
|
||||
fetchIssuesWithExistingPagination = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
loadType: TLoader,
|
||||
cycleId: string
|
||||
) => {
|
||||
if (!this.paginationOptions) return;
|
||||
return await this.fetchIssues(workspaceSlug, projectId, loadType, this.paginationOptions, cycleId, true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Override inherited create issue, to also add issue to cycle
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param data
|
||||
* @param cycleId
|
||||
* @returns
|
||||
*/
|
||||
override createIssue = async (workspaceSlug: string, projectId: string, data: Partial<TIssue>, cycleId: string) => {
|
||||
const response = await super.createIssue(workspaceSlug, projectId, data, cycleId, false);
|
||||
await this.addIssueToCycle(workspaceSlug, projectId, cycleId, [response.id], false);
|
||||
return response;
|
||||
};
|
||||
|
||||
/**
|
||||
* This method is used to transfer issues from completed cycles to a new cycle
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param cycleId
|
||||
* @param payload contains new cycle Id
|
||||
* @returns
|
||||
*/
|
||||
transferIssuesFromCycle = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
cycleId: string,
|
||||
payload: {
|
||||
new_cycle_id: string;
|
||||
}
|
||||
) => {
|
||||
// call API call to transfer issues
|
||||
const response = await this.cycleService.transferIssues(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
cycleId as string,
|
||||
payload
|
||||
);
|
||||
// call fetch issues
|
||||
if (this.paginationOptions) {
|
||||
await persistence.syncIssues(projectId.toString());
|
||||
await this.fetchIssues(workspaceSlug, projectId, "mutation", this.paginationOptions, cycleId);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
/**
|
||||
* This is Pagination for active cycle issues
|
||||
* This method is called to fetch the first page of issues pagination
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param perPageCount
|
||||
* @param cycleId
|
||||
* @returns
|
||||
*/
|
||||
fetchActiveCycleIssues = async (workspaceSlug: string, projectId: string, perPageCount: number, cycleId: string) => {
|
||||
// set loader
|
||||
set(this.activeCycleIds, [cycleId], undefined);
|
||||
|
||||
// set params for urgent and high
|
||||
const params = { priority: `urgent,high`, cursor: `${perPageCount}:0:0`, per_page: perPageCount };
|
||||
// call the fetch issues API
|
||||
const response = await this.cycleService.getCycleIssues(workspaceSlug, projectId, cycleId, params);
|
||||
|
||||
// Process issue response
|
||||
const { issueList, groupedIssues } = this.processIssueResponse(response);
|
||||
|
||||
// add issues to the main Issue Map
|
||||
this.rootIssueStore.issues.addIssue(issueList);
|
||||
const activeIssueIds = groupedIssues[ALL_ISSUES] as string[];
|
||||
|
||||
// store the processed data in the current store
|
||||
set(this.activeCycleIds, [cycleId], {
|
||||
issueIds: activeIssueIds,
|
||||
issueCount: response.total_count,
|
||||
nextCursor: response.next_cursor,
|
||||
nextPageResults: response.next_page_results,
|
||||
perPageCount: perPageCount,
|
||||
});
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
/**
|
||||
* This is Pagination for active cycle issues
|
||||
* This method is called subsequent pages of pagination
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param cycleId
|
||||
* @returns
|
||||
*/
|
||||
fetchNextActiveCycleIssues = async (workspaceSlug: string, projectId: string, cycleId: string) => {
|
||||
//get the previous pagination data for the cycle id
|
||||
const activeCycle = get(this.activeCycleIds, [cycleId]);
|
||||
|
||||
// if there is no active cycle and the next pages does not exist return
|
||||
if (!activeCycle || !activeCycle.nextPageResults) return;
|
||||
|
||||
// create params
|
||||
const params = { priority: `urgent,high`, cursor: activeCycle.nextCursor, per_page: activeCycle.perPageCount };
|
||||
// fetch API response
|
||||
const response = await this.cycleService.getCycleIssues(workspaceSlug, projectId, cycleId, params);
|
||||
|
||||
// Process the response
|
||||
const { issueList, groupedIssues } = this.processIssueResponse(response);
|
||||
|
||||
// add issues to main issue Map
|
||||
this.rootIssueStore.issues.addIssue(issueList);
|
||||
|
||||
const activeIssueIds = groupedIssues[ALL_ISSUES] as string[];
|
||||
|
||||
// store the processed data for subsequent pages
|
||||
set(this.activeCycleIds, [cycleId, "issueCount"], response.total_count);
|
||||
set(this.activeCycleIds, [cycleId, "nextCursor"], response.next_cursor);
|
||||
set(this.activeCycleIds, [cycleId, "nextPageResults"], response.next_page_results);
|
||||
set(this.activeCycleIds, [cycleId, "issueCount"], response.total_count);
|
||||
update(this.activeCycleIds, [cycleId, "issueIds"], (issueIds: string[] = []) =>
|
||||
this.issuesSortWithOrderBy(uniq(concat(issueIds, activeIssueIds)), this.orderBy)
|
||||
);
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
/**
|
||||
* This Method overrides the base quickAdd issue
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param data
|
||||
* @param cycleId
|
||||
* @returns
|
||||
*/
|
||||
quickAddIssue = async (workspaceSlug: string, projectId: string, data: TIssue, cycleId: string) => {
|
||||
// add temporary issue to store list
|
||||
this.addIssue(data);
|
||||
|
||||
// call overridden create issue
|
||||
const response = await this.createIssue(workspaceSlug, projectId, data, cycleId);
|
||||
|
||||
// remove temp Issue from store list
|
||||
runInAction(() => {
|
||||
this.removeIssueFromList(data.id);
|
||||
this.rootIssueStore.issues.removeIssue(data.id);
|
||||
});
|
||||
|
||||
const currentModuleIds =
|
||||
data.module_ids && data.module_ids.length > 0 ? data.module_ids.filter((moduleId) => moduleId != "None") : [];
|
||||
|
||||
if (currentModuleIds.length > 0) {
|
||||
await this.changeModulesInIssue(workspaceSlug, projectId, response.id, currentModuleIds, []);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
// Using aliased names as they cannot be overridden in other stores
|
||||
archiveBulkIssues = this.bulkArchiveIssues;
|
||||
updateIssue = this.issueUpdate;
|
||||
archiveIssue = this.issueArchive;
|
||||
}
|
||||
385
apps/web/core/store/issue/helpers/base-issues-utils.ts
Normal file
385
apps/web/core/store/issue/helpers/base-issues-utils.ts
Normal file
@@ -0,0 +1,385 @@
|
||||
import { uniq, orderBy, isEmpty, indexOf, groupBy, cloneDeep, set } from "lodash-es";
|
||||
import { ALL_ISSUES, EIssueFilterType, FILTER_TO_ISSUE_MAP, ISSUE_PRIORITIES } from "@plane/constants";
|
||||
import type {
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
ISubWorkItemFilters,
|
||||
TIssue,
|
||||
TIssueGroupByOptions,
|
||||
TIssueOrderByOptions,
|
||||
} from "@plane/types";
|
||||
import { checkDateCriteria, convertToISODateString, parseDateFilter } from "@plane/utils";
|
||||
import { store } from "@/lib/store-context";
|
||||
import { EIssueGroupedAction, ISSUE_GROUP_BY_KEY } from "./base-issues.store";
|
||||
|
||||
/**
|
||||
* returns,
|
||||
* A compound key, if both groupId & subGroupId are defined
|
||||
* groupId, only if groupId is defined
|
||||
* ALL_ISSUES, if both groupId & subGroupId are not defined
|
||||
* @param groupId
|
||||
* @param subGroupId
|
||||
* @returns
|
||||
*/
|
||||
export const getGroupKey = (groupId?: string, subGroupId?: string) => {
|
||||
if (groupId && subGroupId && subGroupId !== "null") return `${groupId}_${subGroupId}`;
|
||||
|
||||
if (groupId) return groupId;
|
||||
|
||||
return ALL_ISSUES;
|
||||
};
|
||||
|
||||
/**
|
||||
* This method returns the issue key actions for based on the difference in issue properties of grouped values
|
||||
* @param addArray Array of groupIds at which the issue needs to be added
|
||||
* @param deleteArray Array of groupIds at which the issue needs to be deleted
|
||||
* @returns an array of objects that contains the issue Path at which it needs to be updated and the action that needs to be performed at the path as well
|
||||
*/
|
||||
export const getGroupIssueKeyActions = (
|
||||
addArray: string[],
|
||||
deleteArray: string[]
|
||||
): { path: string[]; action: EIssueGroupedAction }[] => {
|
||||
const issueKeyActions = [];
|
||||
|
||||
// Add all the groupIds as IssueKey and action as Add
|
||||
for (const addKey of addArray) {
|
||||
issueKeyActions.push({ path: [addKey], action: EIssueGroupedAction.ADD });
|
||||
}
|
||||
|
||||
// Add all the groupIds as IssueKey and action as Delete
|
||||
for (const deleteKey of deleteArray) {
|
||||
issueKeyActions.push({ path: [deleteKey], action: EIssueGroupedAction.DELETE });
|
||||
}
|
||||
|
||||
return issueKeyActions;
|
||||
};
|
||||
|
||||
/**
|
||||
* This method returns the issue key actions for based on the difference in issue properties of grouped and subGrouped values
|
||||
* @param groupActionsArray Addition and Deletion arrays of groupIds at which the issue needs to be added and deleted
|
||||
* @param subGroupActionsArray Addition and Deletion arrays of subGroupIds at which the issue needs to be added and deleted
|
||||
* @param previousIssueGroupProperties previous value of the issue property that on which grouping is dependent on
|
||||
* @param currentIssueGroupProperties current value of the issue property that on which grouping is dependent on
|
||||
* @param previousIssueSubGroupProperties previous value of the issue property that on which subGrouping is dependent on
|
||||
* @param currentIssueSubGroupProperties current value of the issue property that on which subGrouping is dependent on
|
||||
* @returns an array of objects that contains the issue Path at which it needs to be updated and the action that needs to be performed at the path as well
|
||||
*/
|
||||
export const getSubGroupIssueKeyActions = (
|
||||
groupActionsArray: {
|
||||
[EIssueGroupedAction.ADD]: string[];
|
||||
[EIssueGroupedAction.DELETE]: string[];
|
||||
},
|
||||
subGroupActionsArray: {
|
||||
[EIssueGroupedAction.ADD]: string[];
|
||||
[EIssueGroupedAction.DELETE]: string[];
|
||||
},
|
||||
previousIssueGroupProperties: string[],
|
||||
currentIssueGroupProperties: string[],
|
||||
previousIssueSubGroupProperties: string[],
|
||||
currentIssueSubGroupProperties: string[]
|
||||
): { path: string[]; action: EIssueGroupedAction }[] => {
|
||||
const issueKeyActions: { [key: string]: { path: string[]; action: EIssueGroupedAction } } = {};
|
||||
|
||||
// For every groupId path for issue Id List, that needs to be added,
|
||||
// It needs to be added at all the current Issue Properties that on which subGrouping depends on
|
||||
for (const addKey of groupActionsArray[EIssueGroupedAction.ADD]) {
|
||||
for (const subGroupProperty of currentIssueSubGroupProperties) {
|
||||
issueKeyActions[getGroupKey(addKey, subGroupProperty)] = {
|
||||
path: [addKey, subGroupProperty],
|
||||
action: EIssueGroupedAction.ADD,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// For every groupId path for issue Id List, that needs to be deleted,
|
||||
// It needs to be deleted at all the previous Issue Properties that on which subGrouping depends on
|
||||
for (const deleteKey of groupActionsArray[EIssueGroupedAction.DELETE]) {
|
||||
for (const subGroupProperty of previousIssueSubGroupProperties) {
|
||||
issueKeyActions[getGroupKey(deleteKey, subGroupProperty)] = {
|
||||
path: [deleteKey, subGroupProperty],
|
||||
action: EIssueGroupedAction.DELETE,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// For every subGroupId path for issue Id List, that needs to be added,
|
||||
// It needs to be added at all the current Issue Properties that on which grouping depends on
|
||||
for (const addKey of subGroupActionsArray[EIssueGroupedAction.ADD]) {
|
||||
for (const groupProperty of currentIssueGroupProperties) {
|
||||
issueKeyActions[getGroupKey(groupProperty, addKey)] = {
|
||||
path: [groupProperty, addKey],
|
||||
action: EIssueGroupedAction.ADD,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// For every subGroupId path for issue Id List, that needs to be deleted,
|
||||
// It needs to be deleted at all the previous Issue Properties that on which grouping depends on
|
||||
for (const deleteKey of subGroupActionsArray[EIssueGroupedAction.DELETE]) {
|
||||
for (const groupProperty of previousIssueGroupProperties) {
|
||||
issueKeyActions[getGroupKey(groupProperty, deleteKey)] = {
|
||||
path: [groupProperty, deleteKey],
|
||||
action: EIssueGroupedAction.DELETE,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(issueKeyActions);
|
||||
};
|
||||
|
||||
/**
|
||||
* This Method is used to get the difference between two arrays
|
||||
* @param current
|
||||
* @param previous
|
||||
* @param action
|
||||
* @returns returns two arrays, ADD and DELETE.
|
||||
* Whatever is newly added to current is added to ADD array
|
||||
* Whatever is removed from previous is added to DELETE array
|
||||
*/
|
||||
export const getDifference = (
|
||||
current: string[],
|
||||
previous: string[],
|
||||
action?: EIssueGroupedAction.ADD | EIssueGroupedAction.DELETE
|
||||
): { [EIssueGroupedAction.ADD]: string[]; [EIssueGroupedAction.DELETE]: string[] } => {
|
||||
const ADD = [];
|
||||
const DELETE = [];
|
||||
|
||||
// For all the current issues values that are not in the previous array, Add them to the ADD array
|
||||
for (const currentValue of current) {
|
||||
if (previous.includes(currentValue)) continue;
|
||||
ADD.push(currentValue);
|
||||
}
|
||||
|
||||
// For all the previous issues values that are not in the current array, Add them to the ADD array
|
||||
for (const previousValue of previous) {
|
||||
if (current.includes(previousValue)) continue;
|
||||
DELETE.push(previousValue);
|
||||
}
|
||||
|
||||
// if there are no action provided, return the arrays
|
||||
if (!action) return { [EIssueGroupedAction.ADD]: ADD, [EIssueGroupedAction.DELETE]: DELETE };
|
||||
|
||||
// If there is an action provided, return the values of both arrays under that array
|
||||
if (action === EIssueGroupedAction.ADD)
|
||||
return { [EIssueGroupedAction.ADD]: uniq([...ADD]), [EIssueGroupedAction.DELETE]: [] };
|
||||
else return { [EIssueGroupedAction.DELETE]: uniq([...DELETE]), [EIssueGroupedAction.ADD]: [] };
|
||||
};
|
||||
|
||||
/**
|
||||
* This Method is mainly used to filter out empty values in the beginning
|
||||
* @param key key of the value that is to be checked if empty
|
||||
* @param object any object in which the key's value is to be checked
|
||||
* @returns 1 if empty, 0 if not empty
|
||||
*/
|
||||
export const getSortOrderToFilterEmptyValues = (key: string, object: any) => {
|
||||
const value = object?.[key];
|
||||
|
||||
if (typeof value !== "number" && isEmpty(value)) return 1;
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
// get IssueIds from Issue data List
|
||||
export const getIssueIds = (issues: TIssue[]) => issues.map((issue) => issue?.id);
|
||||
|
||||
/**
|
||||
* Checks if an issue meets the date filter criteria
|
||||
* @param issue The issue to check
|
||||
* @param filterKey The date field to check ('start_date' or 'target_date')
|
||||
* @param dateFilters Array of date filter strings
|
||||
* @returns boolean indicating if the issue meets the date criteria
|
||||
*/
|
||||
export const checkIssueDateFilter = (
|
||||
issue: TIssue,
|
||||
filterKey: "start_date" | "target_date",
|
||||
dateFilters: string[]
|
||||
): boolean => {
|
||||
if (!dateFilters || dateFilters.length === 0) return true;
|
||||
|
||||
const issueDate = issue[filterKey];
|
||||
if (!issueDate) return false;
|
||||
|
||||
// Issue should match all the date filters (AND operation)
|
||||
return dateFilters.every((filterValue) => {
|
||||
const parsed = parseDateFilter(filterValue);
|
||||
if (!parsed?.date || !parsed?.type) {
|
||||
// ignore invalid filter instead of failing the whole evaluation
|
||||
console.warn(`[filters] Ignoring unparsable date filter "${filterValue}"`);
|
||||
return true;
|
||||
}
|
||||
return checkDateCriteria(new Date(issueDate), parsed.date, parsed.type);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper method to get previous issues state
|
||||
* @param issues - The array of issues to get the previous state for.
|
||||
* @returns The previous state of the issues.
|
||||
*/
|
||||
export const getPreviousIssuesState = (issues: TIssue[]) => {
|
||||
const issueIds = issues.map((issue) => issue.id);
|
||||
const issuesPreviousState: Record<string, TIssue> = {};
|
||||
issueIds.forEach((issueId) => {
|
||||
if (store.issue.issues.issuesMap[issueId]) {
|
||||
issuesPreviousState[issueId] = cloneDeep(store.issue.issues.issuesMap[issueId]);
|
||||
}
|
||||
});
|
||||
return issuesPreviousState;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters the given work items based on the provided filters and display filters.
|
||||
* @param work items - The array of work items to be filtered.
|
||||
* @param filters - The filters to be applied to the issues.
|
||||
* @param displayFilters - The display filters to be applied to the issues.
|
||||
* @returns The filtered array of issues.
|
||||
*/
|
||||
export const getFilteredWorkItems = (workItems: TIssue[], filters: IIssueFilterOptions | undefined): TIssue[] => {
|
||||
if (!filters) return workItems;
|
||||
// Get all active filters
|
||||
const activeFilters = Object.entries(filters).filter(([, value]) => value && value.length > 0);
|
||||
// If no active filters, return all issues
|
||||
if (activeFilters.length === 0) {
|
||||
return workItems;
|
||||
}
|
||||
|
||||
return workItems.filter((workItem) =>
|
||||
// Check all filter conditions (AND operation between different filters)
|
||||
activeFilters.every(([filterKey, filterValues]) => {
|
||||
// Handle date filters separately
|
||||
if (filterKey === "start_date" || filterKey === "target_date") {
|
||||
return checkIssueDateFilter(workItem, filterKey as "start_date" | "target_date", filterValues as string[]);
|
||||
}
|
||||
// Handle regular filters
|
||||
const issueKey = FILTER_TO_ISSUE_MAP[filterKey as keyof IIssueFilterOptions];
|
||||
if (!issueKey) return true; // Skip if no mapping exists
|
||||
const issueValue = workItem[issueKey as keyof TIssue];
|
||||
// Handle array-based properties vs single value properties
|
||||
if (Array.isArray(issueValue)) {
|
||||
return filterValues!.some((filterValue: any) => issueValue.includes(filterValue));
|
||||
} else {
|
||||
return filterValues!.includes(issueValue as string);
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Orders the given work items based on the provided order by key.
|
||||
* @param workItems - The array of work items to be ordered.
|
||||
* @param orderByKey - The key to order the issues by.
|
||||
* @returns The ordered array of work items.
|
||||
*/
|
||||
export const getOrderedWorkItems = (workItems: TIssue[], orderByKey: TIssueOrderByOptions): string[] => {
|
||||
switch (orderByKey) {
|
||||
case "-updated_at":
|
||||
return getIssueIds(orderBy(workItems, (item) => convertToISODateString(item["updated_at"]), ["desc"]));
|
||||
|
||||
case "-created_at":
|
||||
return getIssueIds(orderBy(workItems, (item) => convertToISODateString(item["created_at"]), ["desc"]));
|
||||
|
||||
case "-start_date":
|
||||
return getIssueIds(
|
||||
orderBy(
|
||||
workItems,
|
||||
[getSortOrderToFilterEmptyValues.bind(null, "start_date"), "start_date"], //preferring sorting based on empty values to always keep the empty values below
|
||||
["asc", "desc"]
|
||||
)
|
||||
);
|
||||
|
||||
case "-priority": {
|
||||
const sortArray = ISSUE_PRIORITIES.map((i) => i.key);
|
||||
return getIssueIds(
|
||||
orderBy(workItems, (currentIssue: TIssue) => indexOf(sortArray, currentIssue?.priority), ["asc"])
|
||||
);
|
||||
}
|
||||
default:
|
||||
return getIssueIds(workItems);
|
||||
}
|
||||
};
|
||||
|
||||
export const getGroupedWorkItemIds = (
|
||||
workItems: TIssue[],
|
||||
groupByKey?: TIssueGroupByOptions,
|
||||
orderByKey: TIssueOrderByOptions = "-created_at"
|
||||
): Record<string, string[]> => {
|
||||
// If group by is not set set default as ALL ISSUES
|
||||
if (!groupByKey) {
|
||||
return {
|
||||
[ALL_ISSUES]: getOrderedWorkItems(workItems, orderByKey),
|
||||
};
|
||||
}
|
||||
|
||||
// Get the default key for the group by key
|
||||
const getDefaultGroupKey = (groupByKey: TIssueGroupByOptions) => {
|
||||
switch (groupByKey) {
|
||||
case "state_detail.group":
|
||||
return "state__group";
|
||||
case null:
|
||||
return null;
|
||||
default:
|
||||
return ISSUE_GROUP_BY_KEY[groupByKey];
|
||||
}
|
||||
};
|
||||
|
||||
// Group work items
|
||||
const groupKey = getDefaultGroupKey(groupByKey);
|
||||
const groupedWorkItems = groupBy(workItems, (item) => {
|
||||
const value = groupKey ? item[groupKey] : null;
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) return "None";
|
||||
// Sort & join to build deterministic set-like key
|
||||
return value.slice().sort().join(",");
|
||||
}
|
||||
return value ?? "None";
|
||||
});
|
||||
|
||||
// Convert to Record type
|
||||
const groupedWorkItemsRecord: Record<string, string[]> = {};
|
||||
Object.entries(groupedWorkItems).forEach(([key, items]) => {
|
||||
groupedWorkItemsRecord[key] = getOrderedWorkItems(items as TIssue[], orderByKey);
|
||||
});
|
||||
|
||||
return groupedWorkItemsRecord;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the filters for a given work item.
|
||||
* @param filtersMap - The map of filters for the work item.
|
||||
* @param filterType - The type of filter to update.
|
||||
* @param filters - The filters to update.
|
||||
* @param workItemId - The ID of the work item to update.
|
||||
*/
|
||||
export const updateSubWorkItemFilters = (
|
||||
filtersMap: Record<string, Partial<ISubWorkItemFilters>>,
|
||||
filterType: EIssueFilterType,
|
||||
filters: IIssueDisplayFilterOptions | IIssueDisplayProperties | IIssueFilterOptions,
|
||||
workItemId: string
|
||||
) => {
|
||||
const existingFilters = filtersMap[workItemId] ?? {};
|
||||
const _filters = {
|
||||
filters: existingFilters.filters,
|
||||
displayFilters: existingFilters.displayFilters,
|
||||
displayProperties: existingFilters.displayProperties,
|
||||
};
|
||||
|
||||
switch (filterType) {
|
||||
case EIssueFilterType.FILTERS: {
|
||||
const updatedFilters = filters as IIssueFilterOptions;
|
||||
_filters.filters = { ..._filters.filters, ...updatedFilters };
|
||||
set(filtersMap, [workItemId, "filters"], { ..._filters.filters, ...updatedFilters });
|
||||
break;
|
||||
}
|
||||
case EIssueFilterType.DISPLAY_FILTERS: {
|
||||
set(filtersMap, [workItemId, "displayFilters"], { ..._filters.displayFilters, ...filters });
|
||||
break;
|
||||
}
|
||||
case EIssueFilterType.DISPLAY_PROPERTIES:
|
||||
set(filtersMap, [workItemId, "displayProperties"], {
|
||||
..._filters.displayProperties,
|
||||
...filters,
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
1977
apps/web/core/store/issue/helpers/base-issues.store.ts
Normal file
1977
apps/web/core/store/issue/helpers/base-issues.store.ts
Normal file
File diff suppressed because it is too large
Load Diff
344
apps/web/core/store/issue/helpers/issue-filter-helper.store.ts
Normal file
344
apps/web/core/store/issue/helpers/issue-filter-helper.store.ts
Normal file
@@ -0,0 +1,344 @@
|
||||
import { isEmpty } from "lodash-es";
|
||||
// plane constants
|
||||
import type { EIssueFilterType } from "@plane/constants";
|
||||
import {
|
||||
EIssueGroupByToServerOptions,
|
||||
EServerGroupByToFilterOptions,
|
||||
ENABLE_ISSUE_DEPENDENCIES,
|
||||
} from "@plane/constants";
|
||||
import type {
|
||||
EIssuesStoreType,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
IIssueFilters,
|
||||
IIssueFiltersResponse,
|
||||
IssuePaginationOptions,
|
||||
TIssueKanbanFilters,
|
||||
TIssueParams,
|
||||
TStaticViewTypes,
|
||||
TWorkItemFilterExpression,
|
||||
} from "@plane/types";
|
||||
import { EIssueLayoutTypes } from "@plane/types";
|
||||
// helpers
|
||||
import { getComputedDisplayFilters, getComputedDisplayProperties } from "@plane/utils";
|
||||
// lib
|
||||
import { storage } from "@/lib/local-storage";
|
||||
import { getEnabledDisplayFilters } from "@/plane-web/store/issue/helpers/filter-utils";
|
||||
|
||||
interface ILocalStoreIssueFilters {
|
||||
key: EIssuesStoreType;
|
||||
workspaceSlug: string;
|
||||
viewId: string | undefined; // It can be projectId, moduleId, cycleId, projectViewId
|
||||
userId: string | undefined;
|
||||
filters: IIssueFilters;
|
||||
}
|
||||
|
||||
export interface IBaseIssueFilterStore {
|
||||
// observables
|
||||
filters: Record<string, IIssueFilters>;
|
||||
//computed
|
||||
appliedFilters: Partial<Record<TIssueParams, string | boolean>> | undefined;
|
||||
issueFilters: IIssueFilters | undefined;
|
||||
}
|
||||
|
||||
export interface IIssueFilterHelperStore {
|
||||
computedIssueFilters(filters: IIssueFilters): IIssueFilters;
|
||||
computedFilteredParams(
|
||||
richFilters: TWorkItemFilterExpression,
|
||||
displayFilters: IIssueDisplayFilterOptions | undefined,
|
||||
acceptableParamsByLayout: TIssueParams[]
|
||||
): Partial<Record<TIssueParams, string | boolean>>;
|
||||
computedFilters(filters: IIssueFilterOptions): IIssueFilterOptions;
|
||||
getFilterConditionBasedOnViews: (
|
||||
currentUserId: string | undefined,
|
||||
type: TStaticViewTypes
|
||||
) => Partial<Record<TIssueParams, string>> | undefined;
|
||||
computedDisplayFilters(
|
||||
displayFilters: IIssueDisplayFilterOptions,
|
||||
defaultValues?: IIssueDisplayFilterOptions
|
||||
): IIssueDisplayFilterOptions;
|
||||
computedDisplayProperties(filters: IIssueDisplayProperties): IIssueDisplayProperties;
|
||||
}
|
||||
|
||||
export class IssueFilterHelperStore implements IIssueFilterHelperStore {
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* @description This method is used to apply the display filters on the issues
|
||||
* @param {IIssueFilters} filters
|
||||
* @returns {IIssueFilters}
|
||||
*/
|
||||
computedIssueFilters = (filters: IIssueFilters): IIssueFilters => ({
|
||||
richFilters: isEmpty(filters?.richFilters) ? {} : filters?.richFilters,
|
||||
displayFilters: isEmpty(filters?.displayFilters) ? undefined : filters?.displayFilters,
|
||||
displayProperties: isEmpty(filters?.displayProperties) ? undefined : filters?.displayProperties,
|
||||
kanbanFilters: isEmpty(filters?.kanbanFilters) ? undefined : filters?.kanbanFilters,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description This method is used to convert the filters array params to string params
|
||||
* @param {TWorkItemFilterExpression} richFilters
|
||||
* @param {IIssueDisplayFilterOptions} displayFilters
|
||||
* @param {string[]} acceptableParamsByLayout
|
||||
* @returns {Partial<Record<TIssueParams, string | boolean>>}
|
||||
*/
|
||||
computedFilteredParams = (
|
||||
richFilters: TWorkItemFilterExpression,
|
||||
displayFilters: IIssueDisplayFilterOptions | undefined,
|
||||
acceptableParamsByLayout: TIssueParams[]
|
||||
): Partial<Record<TIssueParams, string | boolean>> => {
|
||||
const computedDisplayFilters: Partial<Record<TIssueParams, undefined | string[] | boolean | string>> = {
|
||||
group_by: displayFilters?.group_by ? EIssueGroupByToServerOptions[displayFilters.group_by] : undefined,
|
||||
sub_group_by: displayFilters?.sub_group_by
|
||||
? EIssueGroupByToServerOptions[displayFilters.sub_group_by]
|
||||
: undefined,
|
||||
order_by: displayFilters?.order_by || undefined,
|
||||
sub_issue: displayFilters?.sub_issue ?? true,
|
||||
};
|
||||
|
||||
const issueFiltersParams: Partial<Record<TIssueParams, boolean | string>> = {};
|
||||
Object.keys(computedDisplayFilters).forEach((key) => {
|
||||
const _key = key as TIssueParams;
|
||||
const _value: string | boolean | string[] | undefined = computedDisplayFilters[_key];
|
||||
const nonEmptyArrayValue = Array.isArray(_value) && _value.length === 0 ? undefined : _value;
|
||||
if (nonEmptyArrayValue != undefined && acceptableParamsByLayout.includes(_key))
|
||||
issueFiltersParams[_key] = Array.isArray(nonEmptyArrayValue)
|
||||
? nonEmptyArrayValue.join(",")
|
||||
: nonEmptyArrayValue;
|
||||
});
|
||||
|
||||
// work item filters
|
||||
if (richFilters) issueFiltersParams.filters = JSON.stringify(richFilters);
|
||||
|
||||
if (displayFilters?.layout) issueFiltersParams.layout = displayFilters?.layout;
|
||||
|
||||
if (ENABLE_ISSUE_DEPENDENCIES && displayFilters?.layout === EIssueLayoutTypes.GANTT)
|
||||
issueFiltersParams["expand"] = "issue_relation,issue_related";
|
||||
|
||||
return issueFiltersParams;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description This method is used to apply the filters on the issues
|
||||
* @param {IIssueFilterOptions} filters
|
||||
* @returns {IIssueFilterOptions}
|
||||
*/
|
||||
computedFilters = (filters: IIssueFilterOptions): IIssueFilterOptions => ({
|
||||
priority: filters?.priority || null,
|
||||
state: filters?.state || null,
|
||||
state_group: filters?.state_group || null,
|
||||
assignees: filters?.assignees || null,
|
||||
mentions: filters?.mentions || null,
|
||||
created_by: filters?.created_by || null,
|
||||
labels: filters?.labels || null,
|
||||
cycle: filters?.cycle || null,
|
||||
module: filters?.module || null,
|
||||
start_date: filters?.start_date || null,
|
||||
target_date: filters?.target_date || null,
|
||||
project: filters?.project || null,
|
||||
team_project: filters?.team_project || null,
|
||||
subscriber: filters?.subscriber || null,
|
||||
issue_type: filters?.issue_type || null,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description This method is used to get the filter conditions based on the views
|
||||
* @param currentUserId
|
||||
* @param type
|
||||
* @returns
|
||||
*/
|
||||
getFilterConditionBasedOnViews: IIssueFilterHelperStore["getFilterConditionBasedOnViews"] = (currentUserId, type) => {
|
||||
if (!currentUserId) return undefined;
|
||||
switch (type) {
|
||||
case "assigned":
|
||||
return {
|
||||
assignees: currentUserId,
|
||||
};
|
||||
case "created":
|
||||
return {
|
||||
created_by: currentUserId,
|
||||
};
|
||||
case "subscribed":
|
||||
return {
|
||||
subscriber: currentUserId,
|
||||
};
|
||||
case "all-issues":
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description This method is used to apply the display filters on the issues
|
||||
* @param {IIssueDisplayFilterOptions} displayFilters
|
||||
* @returns {IIssueDisplayFilterOptions}
|
||||
*/
|
||||
computedDisplayFilters = (
|
||||
displayFilters: IIssueDisplayFilterOptions,
|
||||
defaultValues?: IIssueDisplayFilterOptions
|
||||
): IIssueDisplayFilterOptions => {
|
||||
const computedFilters = getComputedDisplayFilters(displayFilters, defaultValues);
|
||||
return getEnabledDisplayFilters(computedFilters);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description This method is used to apply the display properties on the issues
|
||||
* @param {IIssueDisplayProperties} displayProperties
|
||||
* @returns {IIssueDisplayProperties}
|
||||
*/
|
||||
computedDisplayProperties = (displayProperties: IIssueDisplayProperties): IIssueDisplayProperties =>
|
||||
getComputedDisplayProperties(displayProperties);
|
||||
|
||||
handleIssuesLocalFilters = {
|
||||
fetchFiltersFromStorage: () => {
|
||||
const _filters = storage.get("issue_local_filters");
|
||||
return _filters ? JSON.parse(_filters) : [];
|
||||
},
|
||||
|
||||
get: (
|
||||
currentView: EIssuesStoreType,
|
||||
workspaceSlug: string,
|
||||
viewId: string | undefined, // It can be projectId, moduleId, cycleId, projectViewId
|
||||
userId: string | undefined
|
||||
) => {
|
||||
const storageFilters = this.handleIssuesLocalFilters.fetchFiltersFromStorage();
|
||||
const currentFilterIndex = storageFilters.findIndex(
|
||||
(filter: ILocalStoreIssueFilters) =>
|
||||
filter.key === currentView &&
|
||||
filter.workspaceSlug === workspaceSlug &&
|
||||
filter.viewId === viewId &&
|
||||
filter.userId === userId
|
||||
);
|
||||
if (!currentFilterIndex && currentFilterIndex.length < 0) return undefined;
|
||||
|
||||
return storageFilters[currentFilterIndex]?.filters || {};
|
||||
},
|
||||
|
||||
set: (
|
||||
currentView: EIssuesStoreType,
|
||||
filterType: EIssueFilterType,
|
||||
workspaceSlug: string,
|
||||
viewId: string | undefined, // It can be projectId, moduleId, cycleId, projectViewId
|
||||
userId: string | undefined,
|
||||
filters: Partial<IIssueFiltersResponse & { kanban_filters: TIssueKanbanFilters }>
|
||||
) => {
|
||||
const storageFilters = this.handleIssuesLocalFilters.fetchFiltersFromStorage();
|
||||
const currentFilterIndex = storageFilters.findIndex(
|
||||
(filter: ILocalStoreIssueFilters) =>
|
||||
filter.key === currentView &&
|
||||
filter.workspaceSlug === workspaceSlug &&
|
||||
filter.viewId === viewId &&
|
||||
filter.userId === userId
|
||||
);
|
||||
|
||||
if (currentFilterIndex < 0)
|
||||
storageFilters.push({
|
||||
key: currentView,
|
||||
workspaceSlug: workspaceSlug,
|
||||
viewId: viewId,
|
||||
userId: userId,
|
||||
filters: filters,
|
||||
});
|
||||
else
|
||||
storageFilters[currentFilterIndex] = {
|
||||
...storageFilters[currentFilterIndex],
|
||||
filters: {
|
||||
...storageFilters[currentFilterIndex].filters,
|
||||
[filterType]: filters[filterType as keyof IIssueFiltersResponse],
|
||||
},
|
||||
};
|
||||
// All group_by "filters" are stored in a single array, will cause inconsistency in case of duplicated values
|
||||
storage.set("issue_local_filters", JSON.stringify(storageFilters));
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* This Method returns true if the display properties changed requires a server side update
|
||||
* @param displayFilters
|
||||
* @returns
|
||||
*/
|
||||
getShouldReFetchIssues = (displayFilters: IIssueDisplayFilterOptions) => {
|
||||
const NON_SERVER_DISPLAY_FILTERS = ["order_by", "sub_issue", "type"];
|
||||
const displayFilterKeys = Object.keys(displayFilters);
|
||||
|
||||
return NON_SERVER_DISPLAY_FILTERS.some((serverDisplayfilter: string) =>
|
||||
displayFilterKeys.includes(serverDisplayfilter)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This Method returns true if the display properties changed requires a server side update
|
||||
* @param displayFilters
|
||||
* @returns
|
||||
*/
|
||||
getShouldClearIssues = (displayFilters: IIssueDisplayFilterOptions) => {
|
||||
const NON_SERVER_DISPLAY_FILTERS = ["layout"];
|
||||
const displayFilterKeys = Object.keys(displayFilters);
|
||||
|
||||
return NON_SERVER_DISPLAY_FILTERS.some((serverDisplayfilter: string) =>
|
||||
displayFilterKeys.includes(serverDisplayfilter)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This Method is used to construct the url params along with paginated values
|
||||
* @param filterParams params generated from filters
|
||||
* @param options pagination options
|
||||
* @param cursor cursor if exists
|
||||
* @param groupId groupId if to fetch By group
|
||||
* @param subGroupId groupId if to fetch By sub group
|
||||
* @returns
|
||||
*/
|
||||
getPaginationParams(
|
||||
filterParams: Partial<Record<TIssueParams, string | boolean>> | undefined,
|
||||
options: IssuePaginationOptions,
|
||||
cursor: string | undefined,
|
||||
groupId?: string,
|
||||
subGroupId?: string
|
||||
) {
|
||||
// if cursor exists, use the cursor. If it doesn't exist construct the cursor based on per page count
|
||||
const pageCursor = cursor ? cursor : groupId ? `${options.perPageCount}:1:0` : `${options.perPageCount}:0:0`;
|
||||
|
||||
// pagination params
|
||||
const paginationParams: Partial<Record<TIssueParams, string | boolean>> = {
|
||||
...filterParams,
|
||||
cursor: pageCursor,
|
||||
per_page: options.perPageCount.toString(),
|
||||
};
|
||||
|
||||
// If group by is specifically sent through options, like that for calendar layout, use that to group
|
||||
if (options.groupedBy) {
|
||||
paginationParams.group_by = options.groupedBy;
|
||||
}
|
||||
|
||||
// If before and after dates are sent from option to filter by then, add them to filter the options
|
||||
if (options.after && options.before) {
|
||||
paginationParams["target_date"] = `${options.after};after,${options.before};before`;
|
||||
}
|
||||
|
||||
// If groupId is passed down, add a filter param for that group Id
|
||||
if (groupId) {
|
||||
const groupBy = paginationParams["group_by"] as EIssueGroupByToServerOptions | undefined;
|
||||
delete paginationParams["group_by"];
|
||||
|
||||
if (groupBy) {
|
||||
const groupByFilterOption = EServerGroupByToFilterOptions[groupBy];
|
||||
paginationParams[groupByFilterOption] = groupId;
|
||||
}
|
||||
}
|
||||
|
||||
// If subGroupId is passed down, add a filter param for that subGroup Id
|
||||
if (subGroupId) {
|
||||
const subGroupBy = paginationParams["sub_group_by"] as EIssueGroupByToServerOptions | undefined;
|
||||
delete paginationParams["sub_group_by"];
|
||||
|
||||
if (subGroupBy) {
|
||||
const subGroupByFilterOption = EServerGroupByToFilterOptions[subGroupBy];
|
||||
paginationParams[subGroupByFilterOption] = subGroupId;
|
||||
}
|
||||
}
|
||||
|
||||
return paginationParams;
|
||||
}
|
||||
}
|
||||
202
apps/web/core/store/issue/issue-details/attachment.store.ts
Normal file
202
apps/web/core/store/issue/issue-details/attachment.store.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import { uniq, pull, set, debounce, update, concat } from "lodash-es";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
// types
|
||||
import type { TIssueAttachment, TIssueAttachmentMap, TIssueAttachmentIdMap, TIssueServiceType } from "@plane/types";
|
||||
// services
|
||||
import { IssueAttachmentService } from "@/services/issue";
|
||||
import type { IIssueRootStore } from "../root.store";
|
||||
import type { IIssueDetail } from "./root.store";
|
||||
|
||||
export type TAttachmentUploadStatus = {
|
||||
id: string;
|
||||
name: string;
|
||||
progress: number;
|
||||
size: number;
|
||||
type: string;
|
||||
};
|
||||
|
||||
export interface IIssueAttachmentStoreActions {
|
||||
// actions
|
||||
addAttachments: (issueId: string, attachments: TIssueAttachment[]) => void;
|
||||
fetchAttachments: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssueAttachment[]>;
|
||||
createAttachment: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
file: File
|
||||
) => Promise<TIssueAttachment>;
|
||||
removeAttachment: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
attachmentId: string
|
||||
) => Promise<TIssueAttachment>;
|
||||
}
|
||||
|
||||
export interface IIssueAttachmentStore extends IIssueAttachmentStoreActions {
|
||||
// observables
|
||||
attachments: TIssueAttachmentIdMap;
|
||||
attachmentMap: TIssueAttachmentMap;
|
||||
attachmentsUploadStatusMap: Record<string, Record<string, TAttachmentUploadStatus>>;
|
||||
// computed
|
||||
issueAttachments: string[] | undefined;
|
||||
// helper methods
|
||||
getAttachmentsUploadStatusByIssueId: (issueId: string) => TAttachmentUploadStatus[] | undefined;
|
||||
getAttachmentsByIssueId: (issueId: string) => string[] | undefined;
|
||||
getAttachmentById: (attachmentId: string) => TIssueAttachment | undefined;
|
||||
getAttachmentsCountByIssueId: (issueId: string) => number;
|
||||
}
|
||||
|
||||
export class IssueAttachmentStore implements IIssueAttachmentStore {
|
||||
// observables
|
||||
attachments: TIssueAttachmentIdMap = {};
|
||||
attachmentMap: TIssueAttachmentMap = {};
|
||||
attachmentsUploadStatusMap: Record<string, Record<string, TAttachmentUploadStatus>> = {};
|
||||
// root store
|
||||
rootIssueStore: IIssueRootStore;
|
||||
rootIssueDetailStore: IIssueDetail;
|
||||
// services
|
||||
issueAttachmentService;
|
||||
|
||||
constructor(rootStore: IIssueRootStore, serviceType: TIssueServiceType) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
attachments: observable,
|
||||
attachmentMap: observable,
|
||||
attachmentsUploadStatusMap: observable,
|
||||
// computed
|
||||
issueAttachments: computed,
|
||||
// actions
|
||||
addAttachments: action.bound,
|
||||
fetchAttachments: action,
|
||||
createAttachment: action,
|
||||
removeAttachment: action,
|
||||
});
|
||||
// root store
|
||||
this.rootIssueStore = rootStore;
|
||||
this.rootIssueDetailStore = rootStore.issueDetail;
|
||||
// services
|
||||
this.issueAttachmentService = new IssueAttachmentService(serviceType);
|
||||
}
|
||||
|
||||
// computed
|
||||
get issueAttachments() {
|
||||
const issueId = this.rootIssueDetailStore.peekIssue?.issueId;
|
||||
if (!issueId) return undefined;
|
||||
return this.attachments[issueId] ?? undefined;
|
||||
}
|
||||
|
||||
// helper methods
|
||||
getAttachmentsUploadStatusByIssueId = computedFn((issueId: string) => {
|
||||
if (!issueId) return undefined;
|
||||
const attachmentsUploadStatus = Object.values(this.attachmentsUploadStatusMap[issueId] ?? {});
|
||||
return attachmentsUploadStatus ?? undefined;
|
||||
});
|
||||
|
||||
getAttachmentsByIssueId = (issueId: string) => {
|
||||
if (!issueId) return undefined;
|
||||
return this.attachments[issueId] ?? undefined;
|
||||
};
|
||||
|
||||
getAttachmentById = (attachmentId: string) => {
|
||||
if (!attachmentId) return undefined;
|
||||
return this.attachmentMap[attachmentId] ?? undefined;
|
||||
};
|
||||
|
||||
getAttachmentsCountByIssueId = (issueId: string) => {
|
||||
const attachments = this.getAttachmentsByIssueId(issueId);
|
||||
return attachments?.length ?? 0;
|
||||
};
|
||||
|
||||
// actions
|
||||
addAttachments = (issueId: string, attachments: TIssueAttachment[]) => {
|
||||
if (attachments && attachments.length > 0) {
|
||||
const newAttachmentIds = attachments.map((attachment) => attachment.id);
|
||||
runInAction(() => {
|
||||
update(this.attachments, [issueId], (attachmentIds = []) => uniq(concat(attachmentIds, newAttachmentIds)));
|
||||
attachments.forEach((attachment) => set(this.attachmentMap, attachment.id, attachment));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
fetchAttachments = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
const response = await this.issueAttachmentService.getIssueAttachments(workspaceSlug, projectId, issueId);
|
||||
this.addAttachments(issueId, response);
|
||||
return response;
|
||||
};
|
||||
|
||||
private debouncedUpdateProgress = debounce((issueId: string, tempId: string, progress: number) => {
|
||||
runInAction(() => {
|
||||
set(this.attachmentsUploadStatusMap, [issueId, tempId, "progress"], progress);
|
||||
});
|
||||
}, 16);
|
||||
|
||||
createAttachment = async (workspaceSlug: string, projectId: string, issueId: string, file: File) => {
|
||||
const tempId = uuidv4();
|
||||
try {
|
||||
// update attachment upload status
|
||||
runInAction(() => {
|
||||
set(this.attachmentsUploadStatusMap, [issueId, tempId], {
|
||||
id: tempId,
|
||||
name: file.name,
|
||||
progress: 0,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
});
|
||||
});
|
||||
const response = await this.issueAttachmentService.uploadIssueAttachment(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
file,
|
||||
(progressEvent) => {
|
||||
const progressPercentage = Math.round((progressEvent.progress ?? 0) * 100);
|
||||
this.debouncedUpdateProgress(issueId, tempId, progressPercentage);
|
||||
}
|
||||
);
|
||||
|
||||
if (response && response.id) {
|
||||
runInAction(() => {
|
||||
update(this.attachments, [issueId], (attachmentIds = []) => uniq(concat(attachmentIds, [response.id])));
|
||||
set(this.attachmentMap, response.id, response);
|
||||
this.rootIssueStore.issues.updateIssue(issueId, {
|
||||
attachment_count: this.getAttachmentsCountByIssueId(issueId),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error in uploading issue attachment:", error);
|
||||
throw error;
|
||||
} finally {
|
||||
runInAction(() => {
|
||||
delete this.attachmentsUploadStatusMap[issueId][tempId];
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
removeAttachment = async (workspaceSlug: string, projectId: string, issueId: string, attachmentId: string) => {
|
||||
const response = await this.issueAttachmentService.deleteIssueAttachment(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
attachmentId
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
update(this.attachments, [issueId], (attachmentIds = []) => {
|
||||
if (attachmentIds.includes(attachmentId)) pull(attachmentIds, attachmentId);
|
||||
return attachmentIds;
|
||||
});
|
||||
delete this.attachmentMap[attachmentId];
|
||||
this.rootIssueStore.issues.updateIssue(issueId, {
|
||||
attachment_count: this.getAttachmentsCountByIssueId(issueId),
|
||||
});
|
||||
});
|
||||
|
||||
return response;
|
||||
};
|
||||
}
|
||||
177
apps/web/core/store/issue/issue-details/comment.store.ts
Normal file
177
apps/web/core/store/issue/issue-details/comment.store.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { pull, concat, update, uniq, set } from "lodash-es";
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
// Plane Imports
|
||||
import type { TIssueComment, TIssueCommentMap, TIssueCommentIdMap, TIssueServiceType } from "@plane/types";
|
||||
// services
|
||||
import { IssueCommentService } from "@/services/issue";
|
||||
// types
|
||||
import type { IIssueDetail } from "./root.store";
|
||||
|
||||
export type TCommentLoader = "fetch" | "create" | "update" | "delete" | "mutate" | undefined;
|
||||
|
||||
export interface IIssueCommentStoreActions {
|
||||
fetchComments: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
loaderType?: TCommentLoader
|
||||
) => Promise<TIssueComment[]>;
|
||||
createComment: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
data: Partial<TIssueComment>
|
||||
) => Promise<any>;
|
||||
updateComment: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
commentId: string,
|
||||
data: Partial<TIssueComment>
|
||||
) => Promise<any>;
|
||||
removeComment: (workspaceSlug: string, projectId: string, issueId: string, commentId: string) => Promise<any>;
|
||||
}
|
||||
|
||||
export interface IIssueCommentStore extends IIssueCommentStoreActions {
|
||||
// observables
|
||||
loader: TCommentLoader;
|
||||
comments: TIssueCommentIdMap;
|
||||
commentMap: TIssueCommentMap;
|
||||
// helper methods
|
||||
getCommentsByIssueId: (issueId: string) => string[] | undefined;
|
||||
getCommentById: (activityId: string) => TIssueComment | undefined;
|
||||
}
|
||||
|
||||
export class IssueCommentStore implements IIssueCommentStore {
|
||||
// observables
|
||||
loader: TCommentLoader = "fetch";
|
||||
comments: TIssueCommentIdMap = {};
|
||||
commentMap: TIssueCommentMap = {};
|
||||
serviceType;
|
||||
// root store
|
||||
rootIssueDetail: IIssueDetail;
|
||||
// services
|
||||
issueCommentService;
|
||||
|
||||
constructor(rootStore: IIssueDetail, serviceType: TIssueServiceType) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
loader: observable.ref,
|
||||
comments: observable,
|
||||
commentMap: observable,
|
||||
// actions
|
||||
fetchComments: action,
|
||||
createComment: action,
|
||||
updateComment: action,
|
||||
removeComment: action,
|
||||
});
|
||||
// root store
|
||||
this.serviceType = serviceType;
|
||||
this.rootIssueDetail = rootStore;
|
||||
// services
|
||||
this.issueCommentService = new IssueCommentService(serviceType);
|
||||
}
|
||||
|
||||
// helper methods
|
||||
getCommentsByIssueId = (issueId: string) => {
|
||||
if (!issueId) return undefined;
|
||||
return this.comments[issueId] ?? undefined;
|
||||
};
|
||||
|
||||
getCommentById = (commentId: string) => {
|
||||
if (!commentId) return undefined;
|
||||
return this.commentMap[commentId] ?? undefined;
|
||||
};
|
||||
|
||||
fetchComments = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
loaderType: TCommentLoader = "fetch"
|
||||
) => {
|
||||
this.loader = loaderType;
|
||||
|
||||
let props = {};
|
||||
const _commentIds = this.getCommentsByIssueId(issueId);
|
||||
if (_commentIds && _commentIds.length > 0) {
|
||||
const _comment = this.getCommentById(_commentIds[_commentIds.length - 1]);
|
||||
if (_comment) props = { created_at__gt: _comment.created_at };
|
||||
}
|
||||
|
||||
const comments = await this.issueCommentService.getIssueComments(workspaceSlug, projectId, issueId, props);
|
||||
|
||||
const commentIds = comments.map((comment) => comment.id);
|
||||
runInAction(() => {
|
||||
update(this.comments, issueId, (_commentIds) => {
|
||||
if (!_commentIds) return commentIds;
|
||||
return uniq(concat(_commentIds, commentIds));
|
||||
});
|
||||
comments.forEach((comment) => {
|
||||
this.rootIssueDetail.commentReaction.applyCommentReactions(comment.id, comment?.comment_reactions || []);
|
||||
set(this.commentMap, comment.id, comment);
|
||||
});
|
||||
this.loader = undefined;
|
||||
});
|
||||
|
||||
return comments;
|
||||
};
|
||||
|
||||
createComment = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssueComment>) => {
|
||||
const response = await this.issueCommentService.createIssueComment(workspaceSlug, projectId, issueId, data);
|
||||
|
||||
runInAction(() => {
|
||||
update(this.comments, issueId, (_commentIds) => {
|
||||
if (!_commentIds) return [response.id];
|
||||
return uniq(concat(_commentIds, [response.id]));
|
||||
});
|
||||
set(this.commentMap, response.id, response);
|
||||
});
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
updateComment = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
commentId: string,
|
||||
data: Partial<TIssueComment>
|
||||
) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
Object.keys(data).forEach((key) => {
|
||||
set(this.commentMap, [commentId, key], data[key as keyof TIssueComment]);
|
||||
});
|
||||
});
|
||||
|
||||
const response = await this.issueCommentService.patchIssueComment(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
commentId,
|
||||
data
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
set(this.commentMap, [commentId, "updated_at"], response.updated_at);
|
||||
set(this.commentMap, [commentId, "edited_at"], response.edited_at);
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
this.rootIssueDetail.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
removeComment = async (workspaceSlug: string, projectId: string, issueId: string, commentId: string) => {
|
||||
const response = await this.issueCommentService.deleteIssueComment(workspaceSlug, projectId, issueId, commentId);
|
||||
|
||||
runInAction(() => {
|
||||
pull(this.comments[issueId], commentId);
|
||||
delete this.commentMap[commentId];
|
||||
});
|
||||
|
||||
return response;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import { pull, find, concat, update, set } from "lodash-es";
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
// Plane Imports
|
||||
import type { TIssueCommentReaction, TIssueCommentReactionIdMap, TIssueCommentReactionMap } from "@plane/types";
|
||||
import { groupReactions } from "@plane/utils";
|
||||
// services
|
||||
import { IssueReactionService } from "@/services/issue";
|
||||
// types
|
||||
import type { IIssueDetail } from "./root.store";
|
||||
|
||||
export interface IIssueCommentReactionStoreActions {
|
||||
// actions
|
||||
fetchCommentReactions: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
commentId: string
|
||||
) => Promise<TIssueCommentReaction[]>;
|
||||
applyCommentReactions: (commentId: string, commentReactions: TIssueCommentReaction[]) => void;
|
||||
createCommentReaction: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
commentId: string,
|
||||
reaction: string
|
||||
) => Promise<any>;
|
||||
removeCommentReaction: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
commentId: string,
|
||||
reaction: string,
|
||||
userId: string
|
||||
) => Promise<any>;
|
||||
}
|
||||
|
||||
export interface IIssueCommentReactionStore extends IIssueCommentReactionStoreActions {
|
||||
// observables
|
||||
commentReactions: TIssueCommentReactionIdMap;
|
||||
commentReactionMap: TIssueCommentReactionMap;
|
||||
// helper methods
|
||||
getCommentReactionsByCommentId: (commentId: string) => { [reaction_id: string]: string[] } | undefined;
|
||||
getCommentReactionById: (reactionId: string) => TIssueCommentReaction | undefined;
|
||||
commentReactionsByUser: (commentId: string, userId: string) => TIssueCommentReaction[];
|
||||
}
|
||||
|
||||
export class IssueCommentReactionStore implements IIssueCommentReactionStore {
|
||||
// observables
|
||||
commentReactions: TIssueCommentReactionIdMap = {};
|
||||
commentReactionMap: TIssueCommentReactionMap = {};
|
||||
// root store
|
||||
rootIssueDetailStore: IIssueDetail;
|
||||
// services
|
||||
issueReactionService;
|
||||
|
||||
constructor(rootStore: IIssueDetail) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
commentReactions: observable,
|
||||
commentReactionMap: observable,
|
||||
// actions
|
||||
fetchCommentReactions: action,
|
||||
applyCommentReactions: action,
|
||||
createCommentReaction: action,
|
||||
removeCommentReaction: action,
|
||||
});
|
||||
// root store
|
||||
this.rootIssueDetailStore = rootStore;
|
||||
// services
|
||||
this.issueReactionService = new IssueReactionService();
|
||||
}
|
||||
|
||||
// helper methods
|
||||
getCommentReactionsByCommentId = (commentId: string) => {
|
||||
if (!commentId) return undefined;
|
||||
return this.commentReactions[commentId] ?? undefined;
|
||||
};
|
||||
|
||||
getCommentReactionById = (reactionId: string) => {
|
||||
if (!reactionId) return undefined;
|
||||
return this.commentReactionMap[reactionId] ?? undefined;
|
||||
};
|
||||
|
||||
commentReactionsByUser = (commentId: string, userId: string) => {
|
||||
if (!commentId || !userId) return [];
|
||||
|
||||
const reactions = this.getCommentReactionsByCommentId(commentId);
|
||||
if (!reactions) return [];
|
||||
|
||||
const _userReactions: TIssueCommentReaction[] = [];
|
||||
Object.keys(reactions).forEach((reaction) => {
|
||||
if (reactions?.[reaction])
|
||||
reactions?.[reaction].map((reactionId) => {
|
||||
const currentReaction = this.getCommentReactionById(reactionId);
|
||||
if (currentReaction && currentReaction.actor === userId) _userReactions.push(currentReaction);
|
||||
});
|
||||
});
|
||||
|
||||
return _userReactions;
|
||||
};
|
||||
|
||||
// actions
|
||||
fetchCommentReactions = async (workspaceSlug: string, projectId: string, commentId: string) => {
|
||||
try {
|
||||
const response = await this.issueReactionService.listIssueCommentReactions(workspaceSlug, projectId, commentId);
|
||||
|
||||
const groupedReactions = groupReactions(response || [], "reaction");
|
||||
|
||||
const commentReactionIdsMap: { [reaction: string]: string[] } = {};
|
||||
|
||||
Object.keys(groupedReactions).map((reactionId) => {
|
||||
const reactionIds = (groupedReactions[reactionId] || []).map((reaction) => reaction.id);
|
||||
commentReactionIdsMap[reactionId] = reactionIds;
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
set(this.commentReactions, commentId, commentReactionIdsMap);
|
||||
response.forEach((reaction) => set(this.commentReactionMap, reaction.id, reaction));
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
applyCommentReactions = (commentId: string, commentReactions: TIssueCommentReaction[]) => {
|
||||
const groupedReactions = groupReactions(commentReactions || [], "reaction");
|
||||
|
||||
const commentReactionIdsMap: { [reaction: string]: string[] } = {};
|
||||
|
||||
Object.keys(groupedReactions).map((reactionId) => {
|
||||
const reactionIds = (groupedReactions[reactionId] || []).map((reaction) => reaction.id);
|
||||
commentReactionIdsMap[reactionId] = reactionIds;
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
set(this.commentReactions, commentId, commentReactionIdsMap);
|
||||
commentReactions.forEach((reaction) => set(this.commentReactionMap, reaction.id, reaction));
|
||||
});
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
createCommentReaction = async (workspaceSlug: string, projectId: string, commentId: string, reaction: string) => {
|
||||
try {
|
||||
const response = await this.issueReactionService.createIssueCommentReaction(workspaceSlug, projectId, commentId, {
|
||||
reaction,
|
||||
});
|
||||
|
||||
if (!this.commentReactions[commentId]) this.commentReactions[commentId] = {};
|
||||
runInAction(() => {
|
||||
update(this.commentReactions, `${commentId}.${reaction}`, (reactionId) => {
|
||||
if (!reactionId) return [response.id];
|
||||
return concat(reactionId, response.id);
|
||||
});
|
||||
set(this.commentReactionMap, response.id, response);
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
removeCommentReaction = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
commentId: string,
|
||||
reaction: string,
|
||||
userId: string
|
||||
) => {
|
||||
try {
|
||||
const userReactions = this.commentReactionsByUser(commentId, userId);
|
||||
const currentReaction = find(userReactions, { actor: userId, reaction: reaction });
|
||||
|
||||
if (currentReaction && currentReaction.id) {
|
||||
runInAction(() => {
|
||||
pull(this.commentReactions[commentId][reaction], currentReaction.id);
|
||||
delete this.commentReactionMap[reaction];
|
||||
});
|
||||
}
|
||||
|
||||
const response = await this.issueReactionService.deleteIssueCommentReaction(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
commentId,
|
||||
reaction
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
this.fetchCommentReactions(workspaceSlug, projectId, commentId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
352
apps/web/core/store/issue/issue-details/issue.store.ts
Normal file
352
apps/web/core/store/issue/issue-details/issue.store.ts
Normal file
@@ -0,0 +1,352 @@
|
||||
import { makeObservable, observable } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// types
|
||||
import type { TIssue, TIssueServiceType } from "@plane/types";
|
||||
import { EIssueServiceType } from "@plane/types";
|
||||
// local
|
||||
import { persistence } from "@/local-db/storage.sqlite";
|
||||
// services
|
||||
import { IssueArchiveService, WorkspaceDraftService, IssueService } from "@/services/issue";
|
||||
// types
|
||||
import type { IIssueDetail } from "./root.store";
|
||||
|
||||
export interface IIssueStoreActions {
|
||||
// actions
|
||||
fetchIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssue>;
|
||||
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
addCycleToIssue: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
|
||||
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
|
||||
removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
|
||||
changeModulesInIssue: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
addModuleIds: string[],
|
||||
removeModuleIds: string[]
|
||||
) => Promise<void>;
|
||||
removeIssueFromModule: (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => Promise<void>;
|
||||
fetchIssueWithIdentifier: (workspaceSlug: string, project_identifier: string, sequence_id: string) => Promise<TIssue>;
|
||||
}
|
||||
|
||||
export interface IIssueStore extends IIssueStoreActions {
|
||||
getIsFetchingIssueDetails: (issueId: string | undefined) => boolean;
|
||||
getIsLocalDBIssueDescription: (issueId: string | undefined) => boolean;
|
||||
// helper methods
|
||||
getIssueById: (issueId: string) => TIssue | undefined;
|
||||
getIssueIdByIdentifier: (issueIdentifier: string) => string | undefined;
|
||||
}
|
||||
|
||||
export class IssueStore implements IIssueStore {
|
||||
fetchingIssueDetails: string | undefined = undefined;
|
||||
localDBIssueDescription: string | undefined = undefined;
|
||||
// root store
|
||||
rootIssueDetailStore: IIssueDetail;
|
||||
// services
|
||||
serviceType;
|
||||
issueService;
|
||||
epicService;
|
||||
issueArchiveService;
|
||||
draftWorkItemService;
|
||||
|
||||
constructor(rootStore: IIssueDetail, serviceType: TIssueServiceType) {
|
||||
makeObservable(this, {
|
||||
fetchingIssueDetails: observable.ref,
|
||||
localDBIssueDescription: observable.ref,
|
||||
});
|
||||
// root store
|
||||
this.rootIssueDetailStore = rootStore;
|
||||
// services
|
||||
this.serviceType = serviceType;
|
||||
this.issueService = new IssueService(serviceType);
|
||||
this.epicService = new IssueService(EIssueServiceType.EPICS);
|
||||
this.issueArchiveService = new IssueArchiveService(serviceType);
|
||||
this.draftWorkItemService = new WorkspaceDraftService();
|
||||
}
|
||||
|
||||
getIsFetchingIssueDetails = computedFn((issueId: string | undefined) => {
|
||||
if (!issueId) return false;
|
||||
|
||||
return this.fetchingIssueDetails === issueId;
|
||||
});
|
||||
|
||||
getIsLocalDBIssueDescription = computedFn((issueId: string | undefined) => {
|
||||
if (!issueId) return false;
|
||||
|
||||
return this.localDBIssueDescription === issueId;
|
||||
});
|
||||
|
||||
// helper methods
|
||||
getIssueById = computedFn((issueId: string) => {
|
||||
if (!issueId) return undefined;
|
||||
return this.rootIssueDetailStore.rootIssueStore.issues.getIssueById(issueId) ?? undefined;
|
||||
});
|
||||
|
||||
getIssueIdByIdentifier = computedFn((issueIdentifier: string) => {
|
||||
if (!issueIdentifier) return undefined;
|
||||
return this.rootIssueDetailStore.rootIssueStore.issues.getIssueIdByIdentifier(issueIdentifier) ?? undefined;
|
||||
});
|
||||
|
||||
// actions
|
||||
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
const query = {
|
||||
expand: "issue_reactions,issue_attachments,issue_link,parent",
|
||||
};
|
||||
|
||||
let issue: TIssue | undefined;
|
||||
|
||||
// fetch issue from local db
|
||||
if (this.serviceType === EIssueServiceType.ISSUES) {
|
||||
issue = await persistence.getIssue(issueId);
|
||||
}
|
||||
|
||||
this.fetchingIssueDetails = issueId;
|
||||
|
||||
if (issue) {
|
||||
this.addIssueToStore(issue);
|
||||
this.localDBIssueDescription = issueId;
|
||||
}
|
||||
|
||||
issue = await this.issueService.retrieve(workspaceSlug, projectId, issueId, query);
|
||||
|
||||
if (!issue) throw new Error("Work item not found");
|
||||
|
||||
const issuePayload = this.addIssueToStore(issue);
|
||||
this.localDBIssueDescription = undefined;
|
||||
|
||||
this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issuePayload]);
|
||||
|
||||
// store handlers from issue detail
|
||||
// parent
|
||||
if (issue && issue?.parent && issue?.parent?.id && issue?.parent?.project_id) {
|
||||
this.issueService.retrieve(workspaceSlug, issue.parent.project_id, issue?.parent?.id).then((res) => {
|
||||
this.rootIssueDetailStore.rootIssueStore.issues.addIssue([res]);
|
||||
});
|
||||
}
|
||||
// assignees
|
||||
// labels
|
||||
// state
|
||||
|
||||
// issue reactions
|
||||
if (issue.issue_reactions) this.rootIssueDetailStore.addReactions(issueId, issue.issue_reactions);
|
||||
|
||||
// fetch issue links
|
||||
if (issue.issue_link) this.rootIssueDetailStore.addLinks(issueId, issue.issue_link);
|
||||
|
||||
// fetch issue attachments
|
||||
if (issue.issue_attachments) this.rootIssueDetailStore.addAttachments(issueId, issue.issue_attachments);
|
||||
|
||||
this.rootIssueDetailStore.addSubscription(issueId, issue.is_subscribed);
|
||||
|
||||
// fetch issue activity
|
||||
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||
|
||||
// fetch issue comments
|
||||
this.rootIssueDetailStore.comment.fetchComments(workspaceSlug, projectId, issueId);
|
||||
|
||||
// fetch sub issues
|
||||
this.rootIssueDetailStore.subIssues.fetchSubIssues(workspaceSlug, projectId, issueId);
|
||||
|
||||
// fetch issue relations
|
||||
this.rootIssueDetailStore.relation.fetchRelations(workspaceSlug, projectId, issueId);
|
||||
|
||||
// fetching states
|
||||
// TODO: check if this function is required
|
||||
this.rootIssueDetailStore.rootIssueStore.rootStore.state.fetchProjectStates(workspaceSlug, projectId);
|
||||
|
||||
return issue;
|
||||
};
|
||||
|
||||
addIssueToStore = (issue: TIssue) => {
|
||||
const issuePayload: TIssue = {
|
||||
id: issue?.id,
|
||||
sequence_id: issue?.sequence_id,
|
||||
name: issue?.name,
|
||||
description_html: issue?.description_html,
|
||||
sort_order: issue?.sort_order,
|
||||
state_id: issue?.state_id,
|
||||
priority: issue?.priority,
|
||||
label_ids: issue?.label_ids,
|
||||
assignee_ids: issue?.assignee_ids,
|
||||
estimate_point: issue?.estimate_point,
|
||||
sub_issues_count: issue?.sub_issues_count,
|
||||
attachment_count: issue?.attachment_count,
|
||||
link_count: issue?.link_count,
|
||||
project_id: issue?.project_id,
|
||||
parent_id: issue?.parent_id,
|
||||
cycle_id: issue?.cycle_id,
|
||||
module_ids: issue?.module_ids,
|
||||
type_id: issue?.type_id,
|
||||
created_at: issue?.created_at,
|
||||
updated_at: issue?.updated_at,
|
||||
start_date: issue?.start_date,
|
||||
target_date: issue?.target_date,
|
||||
completed_at: issue?.completed_at,
|
||||
archived_at: issue?.archived_at,
|
||||
created_by: issue?.created_by,
|
||||
updated_by: issue?.updated_by,
|
||||
is_draft: issue?.is_draft,
|
||||
is_subscribed: issue?.is_subscribed,
|
||||
is_epic: issue?.is_epic,
|
||||
};
|
||||
|
||||
this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issuePayload]);
|
||||
this.fetchingIssueDetails = undefined;
|
||||
|
||||
return issuePayload;
|
||||
};
|
||||
|
||||
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
|
||||
const currentStore =
|
||||
this.serviceType === EIssueServiceType.EPICS
|
||||
? this.rootIssueDetailStore.rootIssueStore.projectEpics
|
||||
: this.rootIssueDetailStore.rootIssueStore.projectIssues;
|
||||
|
||||
await Promise.all([
|
||||
currentStore.updateIssue(workspaceSlug, projectId, issueId, data),
|
||||
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId),
|
||||
]);
|
||||
};
|
||||
|
||||
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
const currentStore =
|
||||
this.serviceType === EIssueServiceType.EPICS
|
||||
? this.rootIssueDetailStore.rootIssueStore.projectEpics
|
||||
: this.rootIssueDetailStore.rootIssueStore.projectIssues;
|
||||
currentStore.removeIssue(workspaceSlug, projectId, issueId);
|
||||
};
|
||||
|
||||
archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
const currentStore =
|
||||
this.serviceType === EIssueServiceType.EPICS
|
||||
? this.rootIssueDetailStore.rootIssueStore.projectEpics
|
||||
: this.rootIssueDetailStore.rootIssueStore.projectIssues;
|
||||
currentStore.archiveIssue(workspaceSlug, projectId, issueId);
|
||||
};
|
||||
|
||||
addCycleToIssue = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => {
|
||||
await this.rootIssueDetailStore.rootIssueStore.cycleIssues.addCycleToIssue(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
cycleId,
|
||||
issueId
|
||||
);
|
||||
await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||
};
|
||||
|
||||
addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
|
||||
await this.rootIssueDetailStore.rootIssueStore.cycleIssues.addIssueToCycle(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
cycleId,
|
||||
issueIds,
|
||||
false
|
||||
);
|
||||
if (issueIds && issueIds.length > 0)
|
||||
await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueIds[0]);
|
||||
};
|
||||
|
||||
removeIssueFromCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => {
|
||||
const cycle = await this.rootIssueDetailStore.rootIssueStore.cycleIssues.removeIssueFromCycle(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
cycleId,
|
||||
issueId
|
||||
);
|
||||
await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||
return cycle;
|
||||
};
|
||||
|
||||
changeModulesInIssue = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
addModuleIds: string[],
|
||||
removeModuleIds: string[]
|
||||
) => {
|
||||
await this.rootIssueDetailStore.rootIssueStore.moduleIssues.changeModulesInIssue(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
addModuleIds,
|
||||
removeModuleIds
|
||||
);
|
||||
await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||
};
|
||||
|
||||
removeIssueFromModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => {
|
||||
const currentModule = await this.rootIssueDetailStore.rootIssueStore.moduleIssues.removeIssuesFromModule(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
moduleId,
|
||||
[issueId]
|
||||
);
|
||||
await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||
return currentModule;
|
||||
};
|
||||
|
||||
fetchIssueWithIdentifier = async (workspaceSlug: string, project_identifier: string, sequence_id: string) => {
|
||||
const query = {
|
||||
expand: "issue_reactions,issue_attachments,issue_link,parent",
|
||||
};
|
||||
const issue = await this.issueService.retrieveWithIdentifier(workspaceSlug, project_identifier, sequence_id, query);
|
||||
const issueIdentifier = `${project_identifier}-${sequence_id}`;
|
||||
const issueId = issue?.id;
|
||||
const projectId = issue?.project_id;
|
||||
const rootWorkItemDetailStore = issue?.is_epic
|
||||
? this.rootIssueDetailStore.rootIssueStore.epicDetail
|
||||
: this.rootIssueDetailStore.rootIssueStore.issueDetail;
|
||||
|
||||
if (!issue || !projectId || !issueId) throw new Error("Issue not found");
|
||||
|
||||
const issuePayload = this.addIssueToStore(issue);
|
||||
this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issuePayload]);
|
||||
|
||||
// handle parent issue if exists
|
||||
if (issue?.parent && issue?.parent?.id && issue?.parent?.project_id) {
|
||||
this.issueService.retrieve(workspaceSlug, issue.parent.project_id, issue.parent.id).then((res) => {
|
||||
this.rootIssueDetailStore.rootIssueStore.issues.addIssue([res]);
|
||||
});
|
||||
}
|
||||
|
||||
// add identifiers to map
|
||||
rootWorkItemDetailStore.rootIssueStore.issues.addIssueIdentifier(issueIdentifier, issueId);
|
||||
|
||||
// add related data
|
||||
if (issue.issue_reactions) rootWorkItemDetailStore.addReactions(issue.id, issue.issue_reactions);
|
||||
if (issue.issue_link) rootWorkItemDetailStore.addLinks(issue.id, issue.issue_link);
|
||||
if (issue.issue_attachments) rootWorkItemDetailStore.addAttachments(issue.id, issue.issue_attachments);
|
||||
rootWorkItemDetailStore.addSubscription(issue.id, issue.is_subscribed);
|
||||
|
||||
// fetch related data
|
||||
// issue reactions
|
||||
if (issue.issue_reactions) rootWorkItemDetailStore.addReactions(issueId, issue.issue_reactions);
|
||||
|
||||
// fetch issue links
|
||||
if (issue.issue_link) rootWorkItemDetailStore.addLinks(issueId, issue.issue_link);
|
||||
|
||||
// fetch issue attachments
|
||||
if (issue.issue_attachments) rootWorkItemDetailStore.addAttachments(issueId, issue.issue_attachments);
|
||||
|
||||
rootWorkItemDetailStore.addSubscription(issueId, issue.is_subscribed);
|
||||
|
||||
// fetch issue activity
|
||||
rootWorkItemDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||
|
||||
// fetch issue comments
|
||||
rootWorkItemDetailStore.comment.fetchComments(workspaceSlug, projectId, issueId);
|
||||
|
||||
// fetch sub issues
|
||||
rootWorkItemDetailStore.subIssues.fetchSubIssues(workspaceSlug, projectId, issueId);
|
||||
|
||||
// fetch issue relations
|
||||
rootWorkItemDetailStore.relation.fetchRelations(workspaceSlug, projectId, issueId);
|
||||
|
||||
// fetching states
|
||||
// TODO: check if this function is required
|
||||
rootWorkItemDetailStore.rootIssueStore.rootStore.state.fetchProjectStates(workspaceSlug, projectId);
|
||||
|
||||
return issue;
|
||||
};
|
||||
}
|
||||
165
apps/web/core/store/issue/issue-details/link.store.ts
Normal file
165
apps/web/core/store/issue/issue-details/link.store.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { set } from "lodash-es";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
// services
|
||||
import type { TIssueLink, TIssueLinkMap, TIssueLinkIdMap, TIssueServiceType } from "@plane/types";
|
||||
import { IssueService } from "@/services/issue";
|
||||
// types
|
||||
import type { IIssueDetail } from "./root.store";
|
||||
|
||||
export interface IIssueLinkStoreActions {
|
||||
addLinks: (issueId: string, links: TIssueLink[]) => void;
|
||||
fetchLinks: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssueLink[]>;
|
||||
createLink: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
data: Partial<TIssueLink>
|
||||
) => Promise<TIssueLink>;
|
||||
updateLink: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
linkId: string,
|
||||
data: Partial<TIssueLink>
|
||||
) => Promise<TIssueLink>;
|
||||
removeLink: (workspaceSlug: string, projectId: string, issueId: string, linkId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface IIssueLinkStore extends IIssueLinkStoreActions {
|
||||
// observables
|
||||
links: TIssueLinkIdMap;
|
||||
linkMap: TIssueLinkMap;
|
||||
// computed
|
||||
issueLinks: string[] | undefined;
|
||||
// helper methods
|
||||
getLinksByIssueId: (issueId: string) => string[] | undefined;
|
||||
getLinkById: (linkId: string) => TIssueLink | undefined;
|
||||
}
|
||||
|
||||
export class IssueLinkStore implements IIssueLinkStore {
|
||||
// observables
|
||||
links: TIssueLinkIdMap = {};
|
||||
linkMap: TIssueLinkMap = {};
|
||||
// root store
|
||||
rootIssueDetailStore: IIssueDetail;
|
||||
// services
|
||||
issueService;
|
||||
serviceType;
|
||||
|
||||
constructor(rootStore: IIssueDetail, serviceType: TIssueServiceType) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
links: observable,
|
||||
linkMap: observable,
|
||||
// computed
|
||||
issueLinks: computed,
|
||||
// actions
|
||||
addLinks: action.bound,
|
||||
fetchLinks: action,
|
||||
createLink: action,
|
||||
updateLink: action,
|
||||
removeLink: action,
|
||||
});
|
||||
this.serviceType = serviceType;
|
||||
// root store
|
||||
this.rootIssueDetailStore = rootStore;
|
||||
// services
|
||||
this.issueService = new IssueService(serviceType);
|
||||
}
|
||||
|
||||
// computed
|
||||
get issueLinks() {
|
||||
const issueId = this.rootIssueDetailStore.peekIssue?.issueId;
|
||||
if (!issueId) return undefined;
|
||||
return this.links[issueId] ?? undefined;
|
||||
}
|
||||
|
||||
// helper methods
|
||||
getLinksByIssueId = (issueId: string) => {
|
||||
if (!issueId) return undefined;
|
||||
return this.links[issueId] ?? undefined;
|
||||
};
|
||||
|
||||
getLinkById = (linkId: string) => {
|
||||
if (!linkId) return undefined;
|
||||
return this.linkMap[linkId] ?? undefined;
|
||||
};
|
||||
|
||||
// actions
|
||||
addLinks = (issueId: string, links: TIssueLink[]) => {
|
||||
runInAction(() => {
|
||||
this.links[issueId] = links.map((link) => link.id);
|
||||
links.forEach((link) => set(this.linkMap, link.id, link));
|
||||
});
|
||||
};
|
||||
|
||||
fetchLinks = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
const response = await this.issueService.fetchIssueLinks(workspaceSlug, projectId, issueId);
|
||||
this.addLinks(issueId, response);
|
||||
return response;
|
||||
};
|
||||
|
||||
createLink = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssueLink>) => {
|
||||
const response = await this.issueService.createIssueLink(workspaceSlug, projectId, issueId, data);
|
||||
const issueLinkCount = this.getLinksByIssueId(issueId)?.length ?? 0;
|
||||
runInAction(() => {
|
||||
this.links[issueId].push(response.id);
|
||||
set(this.linkMap, response.id, response);
|
||||
this.rootIssueDetailStore.rootIssueStore.issues.updateIssue(issueId, {
|
||||
link_count: issueLinkCount + 1, // increment link count
|
||||
});
|
||||
});
|
||||
// fetching activity
|
||||
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||
return response;
|
||||
};
|
||||
|
||||
updateLink = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
linkId: string,
|
||||
data: Partial<TIssueLink>
|
||||
) => {
|
||||
const initialData = { ...this.linkMap[linkId] };
|
||||
try {
|
||||
runInAction(() => {
|
||||
Object.keys(data).forEach((key) => {
|
||||
set(this.linkMap, [linkId, key], data[key as keyof TIssueLink]);
|
||||
});
|
||||
});
|
||||
|
||||
const response = await this.issueService.updateIssueLink(workspaceSlug, projectId, issueId, linkId, data);
|
||||
|
||||
// fetching activity
|
||||
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("error", error);
|
||||
runInAction(() => {
|
||||
Object.keys(initialData).forEach((key) => {
|
||||
set(this.linkMap, [linkId, key], initialData[key as keyof TIssueLink]);
|
||||
});
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
removeLink = async (workspaceSlug: string, projectId: string, issueId: string, linkId: string) => {
|
||||
const issueLinkCount = this.getLinksByIssueId(issueId)?.length ?? 0;
|
||||
await this.issueService.deleteIssueLink(workspaceSlug, projectId, issueId, linkId);
|
||||
|
||||
const linkIndex = this.links[issueId].findIndex((_comment) => _comment === linkId);
|
||||
if (linkIndex >= 0)
|
||||
runInAction(() => {
|
||||
this.links[issueId].splice(linkIndex, 1);
|
||||
delete this.linkMap[linkId];
|
||||
this.rootIssueDetailStore.rootIssueStore.issues.updateIssue(issueId, {
|
||||
link_count: issueLinkCount - 1, // decrement link count
|
||||
});
|
||||
});
|
||||
|
||||
// fetching activity
|
||||
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||
};
|
||||
}
|
||||
156
apps/web/core/store/issue/issue-details/reaction.store.ts
Normal file
156
apps/web/core/store/issue/issue-details/reaction.store.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { pull, find, concat, set, update } from "lodash-es";
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
// Plane Imports
|
||||
import type { TIssueReaction, TIssueReactionMap, TIssueReactionIdMap, TIssueServiceType } from "@plane/types";
|
||||
import { groupReactions } from "@plane/utils";
|
||||
// services
|
||||
import { IssueReactionService } from "@/services/issue";
|
||||
// types
|
||||
import type { IIssueDetail } from "./root.store";
|
||||
|
||||
export interface IIssueReactionStoreActions {
|
||||
// actions
|
||||
addReactions: (issueId: string, reactions: TIssueReaction[]) => void;
|
||||
fetchReactions: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssueReaction[]>;
|
||||
createReaction: (workspaceSlug: string, projectId: string, issueId: string, reaction: string) => Promise<any>;
|
||||
removeReaction: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
reaction: string,
|
||||
userId: string
|
||||
) => Promise<any>;
|
||||
}
|
||||
|
||||
export interface IIssueReactionStore extends IIssueReactionStoreActions {
|
||||
// observables
|
||||
reactions: TIssueReactionIdMap;
|
||||
reactionMap: TIssueReactionMap;
|
||||
// helper methods
|
||||
getReactionsByIssueId: (issueId: string) => { [reaction_id: string]: string[] } | undefined;
|
||||
getReactionById: (reactionId: string) => TIssueReaction | undefined;
|
||||
reactionsByUser: (issueId: string, userId: string) => TIssueReaction[];
|
||||
}
|
||||
|
||||
export class IssueReactionStore implements IIssueReactionStore {
|
||||
// observables
|
||||
reactions: TIssueReactionIdMap = {};
|
||||
reactionMap: TIssueReactionMap = {};
|
||||
// root store
|
||||
rootIssueDetailStore: IIssueDetail;
|
||||
// services
|
||||
issueReactionService;
|
||||
serviceType;
|
||||
|
||||
constructor(rootStore: IIssueDetail, serviceType: TIssueServiceType) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
reactions: observable,
|
||||
reactionMap: observable,
|
||||
// actions
|
||||
addReactions: action.bound,
|
||||
fetchReactions: action,
|
||||
createReaction: action,
|
||||
removeReaction: action,
|
||||
});
|
||||
this.serviceType = serviceType;
|
||||
// root store
|
||||
this.rootIssueDetailStore = rootStore;
|
||||
// services
|
||||
this.issueReactionService = new IssueReactionService(serviceType);
|
||||
}
|
||||
|
||||
// helper methods
|
||||
getReactionsByIssueId = (issueId: string) => {
|
||||
if (!issueId) return undefined;
|
||||
return this.reactions[issueId] ?? undefined;
|
||||
};
|
||||
|
||||
getReactionById = (reactionId: string) => {
|
||||
if (!reactionId) return undefined;
|
||||
return this.reactionMap[reactionId] ?? undefined;
|
||||
};
|
||||
|
||||
reactionsByUser = (issueId: string, userId: string) => {
|
||||
if (!issueId || !userId) return [];
|
||||
|
||||
const reactions = this.getReactionsByIssueId(issueId);
|
||||
if (!reactions) return [];
|
||||
|
||||
const _userReactions: TIssueReaction[] = [];
|
||||
Object.keys(reactions).forEach((reaction) => {
|
||||
if (reactions?.[reaction])
|
||||
reactions?.[reaction].map((reactionId) => {
|
||||
const currentReaction = this.getReactionById(reactionId);
|
||||
if (currentReaction && currentReaction.actor === userId) _userReactions.push(currentReaction);
|
||||
});
|
||||
});
|
||||
|
||||
return _userReactions;
|
||||
};
|
||||
|
||||
addReactions = (issueId: string, reactions: TIssueReaction[]) => {
|
||||
const groupedReactions = groupReactions(reactions || [], "reaction");
|
||||
|
||||
const issueReactionIdsMap: { [reaction: string]: string[] } = {};
|
||||
|
||||
Object.keys(groupedReactions).map((reactionId) => {
|
||||
const reactionIds = (groupedReactions[reactionId] || []).map((reaction) => reaction.id);
|
||||
issueReactionIdsMap[reactionId] = reactionIds;
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
set(this.reactions, issueId, issueReactionIdsMap);
|
||||
reactions.forEach((reaction) => set(this.reactionMap, reaction.id, reaction));
|
||||
});
|
||||
};
|
||||
|
||||
// actions
|
||||
fetchReactions = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
const response = await this.issueReactionService.listIssueReactions(workspaceSlug, projectId, issueId);
|
||||
this.addReactions(issueId, response);
|
||||
return response;
|
||||
};
|
||||
|
||||
createReaction = async (workspaceSlug: string, projectId: string, issueId: string, reaction: string) => {
|
||||
const response = await this.issueReactionService.createIssueReaction(workspaceSlug, projectId, issueId, {
|
||||
reaction,
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
update(this.reactions, [issueId, reaction], (reactionId) => {
|
||||
if (!reactionId) return [response.id];
|
||||
return concat(reactionId, response.id);
|
||||
});
|
||||
set(this.reactionMap, response.id, response);
|
||||
});
|
||||
|
||||
// fetching activity
|
||||
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||
return response;
|
||||
};
|
||||
|
||||
removeReaction = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
reaction: string,
|
||||
userId: string
|
||||
) => {
|
||||
const userReactions = this.reactionsByUser(issueId, userId);
|
||||
const currentReaction = find(userReactions, { actor: userId, reaction: reaction });
|
||||
|
||||
if (currentReaction && currentReaction.id) {
|
||||
runInAction(() => {
|
||||
pull(this.reactions[issueId][reaction], currentReaction.id);
|
||||
delete this.reactionMap[reaction];
|
||||
});
|
||||
}
|
||||
|
||||
const response = await this.issueReactionService.deleteIssueReaction(workspaceSlug, projectId, issueId, reaction);
|
||||
|
||||
// fetching activity
|
||||
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||
return response;
|
||||
};
|
||||
}
|
||||
306
apps/web/core/store/issue/issue-details/relation.store.ts
Normal file
306
apps/web/core/store/issue/issue-details/relation.store.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
import { uniq, get, set } from "lodash-es";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// plane imports
|
||||
import type { TIssueRelationIdMap, TIssueRelationMap, TIssueRelation, TIssue } from "@plane/types";
|
||||
// components
|
||||
import type { TRelationObject } from "@/components/issues/issue-detail-widgets/relations";
|
||||
// Plane-web
|
||||
import { REVERSE_RELATIONS } from "@/plane-web/constants/gantt-chart";
|
||||
import type { TIssueRelationTypes } from "@/plane-web/types";
|
||||
// services
|
||||
import { IssueRelationService } from "@/services/issue";
|
||||
// types
|
||||
import type { IIssueDetail } from "./root.store";
|
||||
export interface IIssueRelationStoreActions {
|
||||
// actions
|
||||
fetchRelations: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssueRelation>;
|
||||
createRelation: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
relationType: TIssueRelationTypes,
|
||||
issues: string[]
|
||||
) => Promise<TIssue[]>;
|
||||
removeRelation: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
relationType: TIssueRelationTypes,
|
||||
related_issue: string,
|
||||
updateLocally?: boolean
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface IIssueRelationStore extends IIssueRelationStoreActions {
|
||||
// observables
|
||||
relationMap: TIssueRelationMap; // Record defines relationType as key and reactions as value
|
||||
// computed
|
||||
issueRelations: TIssueRelationIdMap | undefined;
|
||||
// helper methods
|
||||
getRelationsByIssueId: (issueId: string) => TIssueRelationIdMap | undefined;
|
||||
getRelationCountByIssueId: (
|
||||
issueId: string,
|
||||
ISSUE_RELATION_OPTIONS: { [key in TIssueRelationTypes]?: TRelationObject }
|
||||
) => number;
|
||||
getRelationByIssueIdRelationType: (issueId: string, relationType: TIssueRelationTypes) => string[] | undefined;
|
||||
extractRelationsFromIssues: (issues: TIssue[]) => void;
|
||||
createCurrentRelation: (issueId: string, relationType: TIssueRelationTypes, relatedIssueId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export class IssueRelationStore implements IIssueRelationStore {
|
||||
// observables
|
||||
relationMap: TIssueRelationMap = {};
|
||||
// root store
|
||||
rootIssueDetailStore: IIssueDetail;
|
||||
// services
|
||||
issueRelationService;
|
||||
|
||||
constructor(rootStore: IIssueDetail) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
relationMap: observable,
|
||||
// computed
|
||||
issueRelations: computed,
|
||||
// actions
|
||||
fetchRelations: action,
|
||||
createRelation: action,
|
||||
createCurrentRelation: action,
|
||||
removeRelation: action,
|
||||
extractRelationsFromIssues: action,
|
||||
});
|
||||
// root store
|
||||
this.rootIssueDetailStore = rootStore;
|
||||
// services
|
||||
this.issueRelationService = new IssueRelationService();
|
||||
}
|
||||
|
||||
// computed
|
||||
get issueRelations() {
|
||||
const issueId = this.rootIssueDetailStore.peekIssue?.issueId;
|
||||
if (!issueId) return undefined;
|
||||
return this.relationMap?.[issueId] ?? undefined;
|
||||
}
|
||||
|
||||
// // helper methods
|
||||
getRelationsByIssueId = (issueId: string) => {
|
||||
if (!issueId) return undefined;
|
||||
return this.relationMap?.[issueId] ?? undefined;
|
||||
};
|
||||
|
||||
getRelationCountByIssueId = computedFn(
|
||||
(issueId: string, ISSUE_RELATION_OPTIONS: { [key in TIssueRelationTypes]?: TRelationObject }) => {
|
||||
const issueRelations = this.getRelationsByIssueId(issueId);
|
||||
|
||||
const issueRelationKeys = (Object.keys(issueRelations ?? {}) as TIssueRelationTypes[]).filter(
|
||||
(relationKey) => !!ISSUE_RELATION_OPTIONS[relationKey]
|
||||
);
|
||||
|
||||
return issueRelationKeys.reduce((acc, curr) => acc + (issueRelations?.[curr]?.length ?? 0), 0);
|
||||
}
|
||||
);
|
||||
|
||||
getRelationByIssueIdRelationType = (issueId: string, relationType: TIssueRelationTypes) => {
|
||||
if (!issueId || !relationType) return undefined;
|
||||
return this.relationMap?.[issueId]?.[relationType] ?? undefined;
|
||||
};
|
||||
|
||||
// actions
|
||||
fetchRelations = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
const response = await this.issueRelationService.listIssueRelations(workspaceSlug, projectId, issueId);
|
||||
|
||||
runInAction(() => {
|
||||
Object.keys(response).forEach((key) => {
|
||||
const relation_key = key as TIssueRelationTypes;
|
||||
const relation_issues = response[relation_key];
|
||||
const issues = relation_issues.flat().map((issue) => issue);
|
||||
if (issues && issues.length > 0) this.rootIssueDetailStore.rootIssueStore.issues.addIssue(issues);
|
||||
set(
|
||||
this.relationMap,
|
||||
[issueId, relation_key],
|
||||
issues && issues.length > 0 ? issues.map((issue) => issue.id) : []
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
createRelation = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
relationType: TIssueRelationTypes,
|
||||
issues: string[]
|
||||
) => {
|
||||
const response = await this.issueRelationService.createIssueRelations(workspaceSlug, projectId, issueId, {
|
||||
relation_type: relationType,
|
||||
issues,
|
||||
});
|
||||
|
||||
const reverseRelatedType = REVERSE_RELATIONS[relationType];
|
||||
|
||||
const issuesOfRelation = get(this.relationMap, [issueId, relationType]) ?? [];
|
||||
|
||||
if (response && response.length > 0)
|
||||
runInAction(() => {
|
||||
response.forEach((issue) => {
|
||||
const issuesOfRelated = get(this.relationMap, [issue.id, reverseRelatedType]);
|
||||
this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issue]);
|
||||
issuesOfRelation.push(issue.id);
|
||||
|
||||
if (!issuesOfRelated) {
|
||||
set(this.relationMap, [issue.id, reverseRelatedType], [issueId]);
|
||||
} else {
|
||||
set(this.relationMap, [issue.id, reverseRelatedType], uniq([...issuesOfRelated, issueId]));
|
||||
}
|
||||
});
|
||||
set(this.relationMap, [issueId, relationType], uniq(issuesOfRelation));
|
||||
});
|
||||
|
||||
// fetching activity
|
||||
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||
return response;
|
||||
};
|
||||
|
||||
/**
|
||||
* create Relation in current project optimistically
|
||||
* @param issueId
|
||||
* @param relationType
|
||||
* @param relatedIssueId
|
||||
* @returns
|
||||
*/
|
||||
createCurrentRelation = async (issueId: string, relationType: TIssueRelationTypes, relatedIssueId: string) => {
|
||||
const workspaceSlug = this.rootIssueDetailStore.rootIssueStore.workspaceSlug;
|
||||
const projectId = this.rootIssueDetailStore.issue.getIssueById(issueId)?.project_id;
|
||||
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const reverseRelatedType = REVERSE_RELATIONS[relationType];
|
||||
|
||||
const issuesOfRelation = get(this.relationMap, [issueId, relationType]);
|
||||
const issuesOfRelated = get(this.relationMap, [relatedIssueId, reverseRelatedType]);
|
||||
|
||||
try {
|
||||
// update relations before API call
|
||||
runInAction(() => {
|
||||
if (!issuesOfRelation) {
|
||||
set(this.relationMap, [issueId, relationType], [relatedIssueId]);
|
||||
} else {
|
||||
set(this.relationMap, [issueId, relationType], uniq([...issuesOfRelation, relatedIssueId]));
|
||||
}
|
||||
|
||||
if (!issuesOfRelated) {
|
||||
set(this.relationMap, [relatedIssueId, reverseRelatedType], [issueId]);
|
||||
} else {
|
||||
set(this.relationMap, [relatedIssueId, reverseRelatedType], uniq([...issuesOfRelated, issueId]));
|
||||
}
|
||||
});
|
||||
|
||||
// perform API call
|
||||
await this.issueRelationService.createIssueRelations(workspaceSlug, projectId, issueId, {
|
||||
relation_type: relationType,
|
||||
issues: [relatedIssueId],
|
||||
});
|
||||
} catch (e) {
|
||||
// Revert back store changes if API fails
|
||||
runInAction(() => {
|
||||
if (issuesOfRelation) {
|
||||
set(this.relationMap, [issueId, relationType], issuesOfRelation);
|
||||
}
|
||||
|
||||
if (issuesOfRelated) {
|
||||
set(this.relationMap, [relatedIssueId, reverseRelatedType], issuesOfRelated);
|
||||
}
|
||||
});
|
||||
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
removeRelation = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
relationType: TIssueRelationTypes,
|
||||
related_issue: string,
|
||||
updateLocally = false
|
||||
) => {
|
||||
try {
|
||||
const relationIndex = this.relationMap[issueId]?.[relationType]?.findIndex(
|
||||
(_issueId) => _issueId === related_issue
|
||||
);
|
||||
if (relationIndex >= 0)
|
||||
runInAction(() => {
|
||||
this.relationMap[issueId]?.[relationType]?.splice(relationIndex, 1);
|
||||
});
|
||||
|
||||
if (!updateLocally) {
|
||||
await this.issueRelationService.deleteIssueRelation(workspaceSlug, projectId, issueId, {
|
||||
relation_type: relationType,
|
||||
related_issue,
|
||||
});
|
||||
}
|
||||
|
||||
// While removing one relation, reverse of the relation should also be removed
|
||||
const reverseRelatedType = REVERSE_RELATIONS[relationType];
|
||||
const relatedIndex = this.relationMap[related_issue]?.[reverseRelatedType]?.findIndex(
|
||||
(_issueId) => _issueId === related_issue
|
||||
);
|
||||
if (relationIndex >= 0)
|
||||
runInAction(() => {
|
||||
this.relationMap[related_issue]?.[reverseRelatedType]?.splice(relatedIndex, 1);
|
||||
});
|
||||
|
||||
// fetching activity
|
||||
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||
} catch (error) {
|
||||
this.fetchRelations(workspaceSlug, projectId, issueId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract Relation from the issue Array objects and store it in this Store
|
||||
* @param issues
|
||||
*/
|
||||
extractRelationsFromIssues = (issues: TIssue[]) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
for (const issue of issues) {
|
||||
const { issue_relation, issue_related, id: issueId } = issue;
|
||||
|
||||
const issueRelations: { [key in TIssueRelationTypes]?: string[] } = {};
|
||||
|
||||
if (issue_relation && Array.isArray(issue_relation) && issue_relation.length) {
|
||||
for (const relation of issue_relation) {
|
||||
const { relation_type, id } = relation;
|
||||
|
||||
if (!relation_type) continue;
|
||||
|
||||
if (issueRelations[relation_type]) issueRelations[relation_type]?.push(id);
|
||||
else issueRelations[relation_type] = [id];
|
||||
}
|
||||
}
|
||||
|
||||
if (issue_related && Array.isArray(issue_related) && issue_related.length) {
|
||||
for (const relation of issue_related) {
|
||||
const { relation_type, id } = relation;
|
||||
|
||||
if (!relation_type) continue;
|
||||
|
||||
const reverseRelatedType = REVERSE_RELATIONS[relation_type as TIssueRelationTypes];
|
||||
|
||||
if (issueRelations[reverseRelatedType]) issueRelations[reverseRelatedType]?.push(id);
|
||||
else issueRelations[reverseRelatedType] = [id];
|
||||
}
|
||||
}
|
||||
|
||||
set(this.relationMap, [issueId], issueRelations);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error while extracting issue relations from issues");
|
||||
}
|
||||
};
|
||||
}
|
||||
414
apps/web/core/store/issue/issue-details/root.store.ts
Normal file
414
apps/web/core/store/issue/issue-details/root.store.ts
Normal file
@@ -0,0 +1,414 @@
|
||||
import { action, computed, makeObservable, observable } from "mobx";
|
||||
// types
|
||||
import type {
|
||||
TIssue,
|
||||
TIssueAttachment,
|
||||
TIssueComment,
|
||||
TIssueCommentReaction,
|
||||
TIssueLink,
|
||||
TIssueReaction,
|
||||
TIssueServiceType,
|
||||
TWorkItemWidgets,
|
||||
} from "@plane/types";
|
||||
// plane web store
|
||||
import { IssueActivityStore } from "@/plane-web/store/issue/issue-details/activity.store";
|
||||
import type {
|
||||
IIssueActivityStore,
|
||||
IIssueActivityStoreActions,
|
||||
TActivityLoader,
|
||||
} from "@/plane-web/store/issue/issue-details/activity.store";
|
||||
import type { RootStore } from "@/plane-web/store/root.store";
|
||||
import type { TIssueRelationTypes } from "@/plane-web/types";
|
||||
import type { IIssueRootStore } from "../root.store";
|
||||
import { IssueAttachmentStore } from "./attachment.store";
|
||||
import type { IIssueAttachmentStore, IIssueAttachmentStoreActions } from "./attachment.store";
|
||||
import { IssueCommentStore } from "./comment.store";
|
||||
import type { IIssueCommentStore, IIssueCommentStoreActions, TCommentLoader } from "./comment.store";
|
||||
import { IssueCommentReactionStore } from "./comment_reaction.store";
|
||||
import type { IIssueCommentReactionStore, IIssueCommentReactionStoreActions } from "./comment_reaction.store";
|
||||
import { IssueStore } from "./issue.store";
|
||||
import type { IIssueStore, IIssueStoreActions } from "./issue.store";
|
||||
import { IssueLinkStore } from "./link.store";
|
||||
import type { IIssueLinkStore, IIssueLinkStoreActions } from "./link.store";
|
||||
import { IssueReactionStore } from "./reaction.store";
|
||||
import type { IIssueReactionStore, IIssueReactionStoreActions } from "./reaction.store";
|
||||
import { IssueRelationStore } from "./relation.store";
|
||||
import type { IIssueRelationStore, IIssueRelationStoreActions } from "./relation.store";
|
||||
import { IssueSubIssuesStore } from "./sub_issues.store";
|
||||
import type { IIssueSubIssuesStore, IIssueSubIssuesStoreActions } from "./sub_issues.store";
|
||||
import { IssueSubscriptionStore } from "./subscription.store";
|
||||
import type { IIssueSubscriptionStore, IIssueSubscriptionStoreActions } from "./subscription.store";
|
||||
|
||||
export type TPeekIssue = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
nestingLevel?: number;
|
||||
isArchived?: boolean;
|
||||
};
|
||||
|
||||
export type TIssueRelationModal = {
|
||||
issueId: string | null;
|
||||
relationType: TIssueRelationTypes | null;
|
||||
};
|
||||
|
||||
export type TIssueCrudState = { toggle: boolean; parentIssueId: string | undefined; issue: TIssue | undefined };
|
||||
|
||||
export type TIssueCrudOperationState = {
|
||||
create: TIssueCrudState;
|
||||
existing: TIssueCrudState;
|
||||
};
|
||||
|
||||
export interface IIssueDetail
|
||||
extends IIssueStoreActions,
|
||||
IIssueReactionStoreActions,
|
||||
IIssueLinkStoreActions,
|
||||
IIssueSubIssuesStoreActions,
|
||||
IIssueSubscriptionStoreActions,
|
||||
IIssueAttachmentStoreActions,
|
||||
IIssueRelationStoreActions,
|
||||
IIssueActivityStoreActions,
|
||||
IIssueCommentStoreActions,
|
||||
IIssueCommentReactionStoreActions {
|
||||
// observables
|
||||
peekIssue: TPeekIssue | undefined;
|
||||
relationKey: TIssueRelationTypes | null;
|
||||
issueLinkData: TIssueLink | null;
|
||||
issueCrudOperationState: TIssueCrudOperationState;
|
||||
openWidgets: TWorkItemWidgets[];
|
||||
lastWidgetAction: TWorkItemWidgets | null;
|
||||
isCreateIssueModalOpen: boolean;
|
||||
isIssueLinkModalOpen: boolean;
|
||||
isParentIssueModalOpen: string | null;
|
||||
isDeleteIssueModalOpen: string | null;
|
||||
isArchiveIssueModalOpen: string | null;
|
||||
isRelationModalOpen: TIssueRelationModal | null;
|
||||
isSubIssuesModalOpen: string | null;
|
||||
attachmentDeleteModalId: string | null;
|
||||
// computed
|
||||
isAnyModalOpen: boolean;
|
||||
isPeekOpen: boolean;
|
||||
// helper actions
|
||||
getIsIssuePeeked: (issueId: string) => boolean;
|
||||
// actions
|
||||
setPeekIssue: (peekIssue: TPeekIssue | undefined) => void;
|
||||
setIssueLinkData: (issueLinkData: TIssueLink | null) => void;
|
||||
toggleCreateIssueModal: (value: boolean) => void;
|
||||
toggleIssueLinkModal: (value: boolean) => void;
|
||||
toggleParentIssueModal: (issueId: string | null) => void;
|
||||
toggleDeleteIssueModal: (issueId: string | null) => void;
|
||||
toggleArchiveIssueModal: (value: string | null) => void;
|
||||
toggleRelationModal: (issueId: string | null, relationType: TIssueRelationTypes | null) => void;
|
||||
toggleSubIssuesModal: (value: string | null) => void;
|
||||
toggleDeleteAttachmentModal: (attachmentId: string | null) => void;
|
||||
setOpenWidgets: (state: TWorkItemWidgets[]) => void;
|
||||
setLastWidgetAction: (action: TWorkItemWidgets) => void;
|
||||
toggleOpenWidget: (state: TWorkItemWidgets) => void;
|
||||
setRelationKey: (relationKey: TIssueRelationTypes | null) => void;
|
||||
setIssueCrudOperationState: (state: TIssueCrudOperationState) => void;
|
||||
// store
|
||||
rootIssueStore: IIssueRootStore;
|
||||
issue: IIssueStore;
|
||||
reaction: IIssueReactionStore;
|
||||
attachment: IIssueAttachmentStore;
|
||||
activity: IIssueActivityStore;
|
||||
comment: IIssueCommentStore;
|
||||
commentReaction: IIssueCommentReactionStore;
|
||||
subIssues: IIssueSubIssuesStore;
|
||||
link: IIssueLinkStore;
|
||||
subscription: IIssueSubscriptionStore;
|
||||
relation: IIssueRelationStore;
|
||||
}
|
||||
|
||||
export abstract class IssueDetail implements IIssueDetail {
|
||||
// observables
|
||||
peekIssue: TPeekIssue | undefined = undefined;
|
||||
relationKey: TIssueRelationTypes | null = null;
|
||||
issueLinkData: TIssueLink | null = null;
|
||||
issueCrudOperationState: TIssueCrudOperationState = {
|
||||
create: {
|
||||
toggle: false,
|
||||
parentIssueId: undefined,
|
||||
issue: undefined,
|
||||
},
|
||||
existing: {
|
||||
toggle: false,
|
||||
parentIssueId: undefined,
|
||||
issue: undefined,
|
||||
},
|
||||
};
|
||||
openWidgets: TWorkItemWidgets[] = ["sub-work-items", "links", "attachments"];
|
||||
lastWidgetAction: TWorkItemWidgets | null = null;
|
||||
isCreateIssueModalOpen: boolean = false;
|
||||
isIssueLinkModalOpen: boolean = false;
|
||||
isParentIssueModalOpen: string | null = null;
|
||||
isDeleteIssueModalOpen: string | null = null;
|
||||
isArchiveIssueModalOpen: string | null = null;
|
||||
isRelationModalOpen: TIssueRelationModal | null = null;
|
||||
isSubIssuesModalOpen: string | null = null;
|
||||
attachmentDeleteModalId: string | null = null;
|
||||
// service type
|
||||
serviceType: TIssueServiceType;
|
||||
// store
|
||||
rootIssueStore: IIssueRootStore;
|
||||
issue: IIssueStore;
|
||||
reaction: IIssueReactionStore;
|
||||
attachment: IIssueAttachmentStore;
|
||||
subIssues: IIssueSubIssuesStore;
|
||||
link: IIssueLinkStore;
|
||||
subscription: IIssueSubscriptionStore;
|
||||
relation: IIssueRelationStore;
|
||||
activity: IIssueActivityStore;
|
||||
comment: IIssueCommentStore;
|
||||
commentReaction: IIssueCommentReactionStore;
|
||||
|
||||
constructor(rootStore: IIssueRootStore, serviceType: TIssueServiceType) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
peekIssue: observable,
|
||||
relationKey: observable,
|
||||
issueLinkData: observable,
|
||||
issueCrudOperationState: observable,
|
||||
isCreateIssueModalOpen: observable,
|
||||
isIssueLinkModalOpen: observable.ref,
|
||||
isParentIssueModalOpen: observable.ref,
|
||||
isDeleteIssueModalOpen: observable.ref,
|
||||
isArchiveIssueModalOpen: observable.ref,
|
||||
isRelationModalOpen: observable.ref,
|
||||
isSubIssuesModalOpen: observable.ref,
|
||||
attachmentDeleteModalId: observable.ref,
|
||||
openWidgets: observable.ref,
|
||||
lastWidgetAction: observable.ref,
|
||||
// computed
|
||||
isAnyModalOpen: computed,
|
||||
isPeekOpen: computed,
|
||||
// action
|
||||
setPeekIssue: action,
|
||||
setIssueLinkData: action,
|
||||
toggleCreateIssueModal: action,
|
||||
toggleIssueLinkModal: action,
|
||||
toggleParentIssueModal: action,
|
||||
toggleDeleteIssueModal: action,
|
||||
toggleArchiveIssueModal: action,
|
||||
toggleRelationModal: action,
|
||||
toggleSubIssuesModal: action,
|
||||
toggleDeleteAttachmentModal: action,
|
||||
setOpenWidgets: action,
|
||||
setLastWidgetAction: action,
|
||||
toggleOpenWidget: action,
|
||||
setRelationKey: action,
|
||||
setIssueCrudOperationState: action,
|
||||
});
|
||||
|
||||
// store
|
||||
this.serviceType = serviceType;
|
||||
this.rootIssueStore = rootStore;
|
||||
this.issue = new IssueStore(this, serviceType);
|
||||
this.reaction = new IssueReactionStore(this, serviceType);
|
||||
this.attachment = new IssueAttachmentStore(rootStore, serviceType);
|
||||
this.activity = new IssueActivityStore(rootStore.rootStore as RootStore, serviceType);
|
||||
this.comment = new IssueCommentStore(this, serviceType);
|
||||
this.commentReaction = new IssueCommentReactionStore(this);
|
||||
this.subIssues = new IssueSubIssuesStore(this, serviceType);
|
||||
this.link = new IssueLinkStore(this, serviceType);
|
||||
this.subscription = new IssueSubscriptionStore(this, serviceType);
|
||||
this.relation = new IssueRelationStore(this);
|
||||
}
|
||||
|
||||
// computed
|
||||
get isAnyModalOpen() {
|
||||
return (
|
||||
this.isCreateIssueModalOpen ||
|
||||
this.isIssueLinkModalOpen ||
|
||||
!!this.isParentIssueModalOpen ||
|
||||
!!this.isDeleteIssueModalOpen ||
|
||||
!!this.isArchiveIssueModalOpen ||
|
||||
!!this.isRelationModalOpen?.issueId ||
|
||||
!!this.isSubIssuesModalOpen ||
|
||||
!!this.attachmentDeleteModalId
|
||||
);
|
||||
}
|
||||
|
||||
get isPeekOpen() {
|
||||
return !!this.peekIssue;
|
||||
}
|
||||
|
||||
// helper actions
|
||||
getIsIssuePeeked = (issueId: string) => this.peekIssue?.issueId === issueId;
|
||||
|
||||
// actions
|
||||
setRelationKey = (relationKey: TIssueRelationTypes | null) => (this.relationKey = relationKey);
|
||||
setIssueCrudOperationState = (state: TIssueCrudOperationState) => (this.issueCrudOperationState = state);
|
||||
setPeekIssue = (peekIssue: TPeekIssue | undefined) => (this.peekIssue = peekIssue);
|
||||
toggleCreateIssueModal = (value: boolean) => (this.isCreateIssueModalOpen = value);
|
||||
toggleIssueLinkModal = (value: boolean) => (this.isIssueLinkModalOpen = value);
|
||||
toggleParentIssueModal = (issueId: string | null) => (this.isParentIssueModalOpen = issueId);
|
||||
toggleDeleteIssueModal = (issueId: string | null) => (this.isDeleteIssueModalOpen = issueId);
|
||||
toggleArchiveIssueModal = (issueId: string | null) => (this.isArchiveIssueModalOpen = issueId);
|
||||
toggleRelationModal = (issueId: string | null, relationType: TIssueRelationTypes | null) =>
|
||||
(this.isRelationModalOpen = { issueId, relationType });
|
||||
toggleSubIssuesModal = (issueId: string | null) => (this.isSubIssuesModalOpen = issueId);
|
||||
toggleDeleteAttachmentModal = (attachmentId: string | null) => (this.attachmentDeleteModalId = attachmentId);
|
||||
setOpenWidgets = (state: TWorkItemWidgets[]) => {
|
||||
this.openWidgets = state;
|
||||
if (this.lastWidgetAction) this.lastWidgetAction = null;
|
||||
};
|
||||
setLastWidgetAction = (action: TWorkItemWidgets) => {
|
||||
this.openWidgets = [action];
|
||||
};
|
||||
toggleOpenWidget = (state: TWorkItemWidgets) => {
|
||||
if (this.openWidgets && this.openWidgets.includes(state))
|
||||
this.openWidgets = this.openWidgets.filter((s) => s !== state);
|
||||
else this.openWidgets = [state, ...this.openWidgets];
|
||||
};
|
||||
setIssueLinkData = (issueLinkData: TIssueLink | null) => (this.issueLinkData = issueLinkData);
|
||||
|
||||
// issue
|
||||
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||
this.issue.fetchIssue(workspaceSlug, projectId, issueId);
|
||||
fetchIssueWithIdentifier = async (workspaceSlug: string, projectIdentifier: string, sequenceId: string) =>
|
||||
this.issue.fetchIssueWithIdentifier(workspaceSlug, projectIdentifier, sequenceId);
|
||||
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) =>
|
||||
this.issue.updateIssue(workspaceSlug, projectId, issueId, data);
|
||||
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||
this.issue.removeIssue(workspaceSlug, projectId, issueId);
|
||||
archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||
this.issue.archiveIssue(workspaceSlug, projectId, issueId);
|
||||
addCycleToIssue = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) =>
|
||||
this.issue.addCycleToIssue(workspaceSlug, projectId, cycleId, issueId);
|
||||
addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) =>
|
||||
this.issue.addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
|
||||
removeIssueFromCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) =>
|
||||
this.issue.removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId);
|
||||
changeModulesInIssue = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
addModuleIds: string[],
|
||||
removeModuleIds: string[]
|
||||
) => this.issue.changeModulesInIssue(workspaceSlug, projectId, issueId, addModuleIds, removeModuleIds);
|
||||
removeIssueFromModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) =>
|
||||
this.issue.removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId);
|
||||
|
||||
// reactions
|
||||
addReactions = (issueId: string, reactions: TIssueReaction[]) => this.reaction.addReactions(issueId, reactions);
|
||||
fetchReactions = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||
this.reaction.fetchReactions(workspaceSlug, projectId, issueId);
|
||||
createReaction = async (workspaceSlug: string, projectId: string, issueId: string, reaction: string) =>
|
||||
this.reaction.createReaction(workspaceSlug, projectId, issueId, reaction);
|
||||
removeReaction = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
reaction: string,
|
||||
userId: string
|
||||
) => this.reaction.removeReaction(workspaceSlug, projectId, issueId, reaction, userId);
|
||||
|
||||
// attachments
|
||||
addAttachments = (issueId: string, attachments: TIssueAttachment[]) =>
|
||||
this.attachment.addAttachments(issueId, attachments);
|
||||
fetchAttachments = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||
this.attachment.fetchAttachments(workspaceSlug, projectId, issueId);
|
||||
createAttachment = async (workspaceSlug: string, projectId: string, issueId: string, file: File) =>
|
||||
this.attachment.createAttachment(workspaceSlug, projectId, issueId, file);
|
||||
removeAttachment = async (workspaceSlug: string, projectId: string, issueId: string, attachmentId: string) =>
|
||||
this.attachment.removeAttachment(workspaceSlug, projectId, issueId, attachmentId);
|
||||
|
||||
// link
|
||||
addLinks = (issueId: string, links: TIssueLink[]) => this.link.addLinks(issueId, links);
|
||||
fetchLinks = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||
this.link.fetchLinks(workspaceSlug, projectId, issueId);
|
||||
createLink = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssueLink>) =>
|
||||
this.link.createLink(workspaceSlug, projectId, issueId, data);
|
||||
updateLink = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
linkId: string,
|
||||
data: Partial<TIssueLink>
|
||||
) => this.link.updateLink(workspaceSlug, projectId, issueId, linkId, data);
|
||||
removeLink = async (workspaceSlug: string, projectId: string, issueId: string, linkId: string) =>
|
||||
this.link.removeLink(workspaceSlug, projectId, issueId, linkId);
|
||||
|
||||
// sub issues
|
||||
fetchSubIssues = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||
this.subIssues.fetchSubIssues(workspaceSlug, projectId, issueId);
|
||||
createSubIssues = async (workspaceSlug: string, projectId: string, parentIssueId: string, data: string[]) =>
|
||||
this.subIssues.createSubIssues(workspaceSlug, projectId, parentIssueId, data);
|
||||
updateSubIssue = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
parentIssueId: string,
|
||||
issueId: string,
|
||||
issueData: Partial<TIssue>,
|
||||
oldIssue?: Partial<TIssue>,
|
||||
fromModal?: boolean
|
||||
) => this.subIssues.updateSubIssue(workspaceSlug, projectId, parentIssueId, issueId, issueData, oldIssue, fromModal);
|
||||
removeSubIssue = async (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) =>
|
||||
this.subIssues.removeSubIssue(workspaceSlug, projectId, parentIssueId, issueId);
|
||||
deleteSubIssue = async (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) =>
|
||||
this.subIssues.deleteSubIssue(workspaceSlug, projectId, parentIssueId, issueId);
|
||||
|
||||
// subscription
|
||||
addSubscription = (issueId: string, isSubscribed: boolean | undefined | null) =>
|
||||
this.subscription.addSubscription(issueId, isSubscribed);
|
||||
fetchSubscriptions = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||
this.subscription.fetchSubscriptions(workspaceSlug, projectId, issueId);
|
||||
createSubscription = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||
this.subscription.createSubscription(workspaceSlug, projectId, issueId);
|
||||
removeSubscription = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||
this.subscription.removeSubscription(workspaceSlug, projectId, issueId);
|
||||
|
||||
// relations
|
||||
fetchRelations = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||
this.relation.fetchRelations(workspaceSlug, projectId, issueId);
|
||||
createRelation = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
relationType: TIssueRelationTypes,
|
||||
issues: string[]
|
||||
) => this.relation.createRelation(workspaceSlug, projectId, issueId, relationType, issues);
|
||||
removeRelation = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
relationType: TIssueRelationTypes,
|
||||
relatedIssue: string,
|
||||
updateLocally?: boolean
|
||||
) => this.relation.removeRelation(workspaceSlug, projectId, issueId, relationType, relatedIssue, updateLocally);
|
||||
|
||||
// activity
|
||||
fetchActivities = async (workspaceSlug: string, projectId: string, issueId: string, loaderType?: TActivityLoader) =>
|
||||
this.activity.fetchActivities(workspaceSlug, projectId, issueId, loaderType);
|
||||
|
||||
// comment
|
||||
fetchComments = async (workspaceSlug: string, projectId: string, issueId: string, loaderType?: TCommentLoader) =>
|
||||
this.comment.fetchComments(workspaceSlug, projectId, issueId, loaderType);
|
||||
createComment = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssueComment>) =>
|
||||
this.comment.createComment(workspaceSlug, projectId, issueId, data);
|
||||
updateComment = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
commentId: string,
|
||||
data: Partial<TIssueComment>
|
||||
) => this.comment.updateComment(workspaceSlug, projectId, issueId, commentId, data);
|
||||
removeComment = async (workspaceSlug: string, projectId: string, issueId: string, commentId: string) =>
|
||||
this.comment.removeComment(workspaceSlug, projectId, issueId, commentId);
|
||||
|
||||
// comment reaction
|
||||
fetchCommentReactions = async (workspaceSlug: string, projectId: string, commentId: string) =>
|
||||
this.commentReaction.fetchCommentReactions(workspaceSlug, projectId, commentId);
|
||||
applyCommentReactions = async (commentId: string, commentReactions: TIssueCommentReaction[]) =>
|
||||
this.commentReaction.applyCommentReactions(commentId, commentReactions);
|
||||
createCommentReaction = async (workspaceSlug: string, projectId: string, commentId: string, reaction: string) =>
|
||||
this.commentReaction.createCommentReaction(workspaceSlug, projectId, commentId, reaction);
|
||||
removeCommentReaction = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
commentId: string,
|
||||
reaction: string,
|
||||
userId: string
|
||||
) => this.commentReaction.removeCommentReaction(workspaceSlug, projectId, commentId, reaction, userId);
|
||||
}
|
||||
369
apps/web/core/store/issue/issue-details/sub_issues.store.ts
Normal file
369
apps/web/core/store/issue/issue-details/sub_issues.store.ts
Normal file
@@ -0,0 +1,369 @@
|
||||
import { pull, concat, uniq, set, update } from "lodash-es";
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// Plane Imports
|
||||
import type {
|
||||
TIssue,
|
||||
TIssueSubIssues,
|
||||
TIssueSubIssuesStateDistributionMap,
|
||||
TIssueSubIssuesIdMap,
|
||||
TSubIssuesStateDistribution,
|
||||
TIssueServiceType,
|
||||
TLoader,
|
||||
} from "@plane/types";
|
||||
import { EIssueServiceType } from "@plane/types";
|
||||
// services
|
||||
import { updatePersistentLayer } from "@/local-db/utils/utils";
|
||||
import { IssueService } from "@/services/issue";
|
||||
// store
|
||||
import type { IIssueDetail } from "./root.store";
|
||||
import type { IWorkItemSubIssueFiltersStore } from "./sub_issues_filter.store";
|
||||
import { WorkItemSubIssueFiltersStore } from "./sub_issues_filter.store";
|
||||
|
||||
export interface IIssueSubIssuesStoreActions {
|
||||
fetchSubIssues: (workspaceSlug: string, projectId: string, parentIssueId: string) => Promise<TIssueSubIssues>;
|
||||
createSubIssues: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
parentIssueId: string,
|
||||
issueIds: string[]
|
||||
) => Promise<void>;
|
||||
updateSubIssue: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
parentIssueId: string,
|
||||
issueId: string,
|
||||
issueData: Partial<TIssue>,
|
||||
oldIssue?: Partial<TIssue>,
|
||||
fromModal?: boolean
|
||||
) => Promise<void>;
|
||||
removeSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => Promise<void>;
|
||||
deleteSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
type TSubIssueHelpersKeys = "issue_visibility" | "preview_loader" | "issue_loader";
|
||||
type TSubIssueHelpers = Record<TSubIssueHelpersKeys, string[]>;
|
||||
export interface IIssueSubIssuesStore extends IIssueSubIssuesStoreActions {
|
||||
// observables
|
||||
subIssuesStateDistribution: TIssueSubIssuesStateDistributionMap;
|
||||
subIssues: TIssueSubIssuesIdMap;
|
||||
subIssueHelpers: Record<string, TSubIssueHelpers>; // parent_issue_id -> TSubIssueHelpers
|
||||
loader: TLoader;
|
||||
filters: IWorkItemSubIssueFiltersStore;
|
||||
// helper methods
|
||||
stateDistributionByIssueId: (issueId: string) => TSubIssuesStateDistribution | undefined;
|
||||
subIssuesByIssueId: (issueId: string) => string[] | undefined;
|
||||
subIssueHelpersByIssueId: (issueId: string) => TSubIssueHelpers;
|
||||
// actions
|
||||
fetchOtherProjectProperties: (workspaceSlug: string, projectIds: string[]) => Promise<void>;
|
||||
setSubIssueHelpers: (parentIssueId: string, key: TSubIssueHelpersKeys, value: string) => void;
|
||||
}
|
||||
|
||||
export class IssueSubIssuesStore implements IIssueSubIssuesStore {
|
||||
// observables
|
||||
subIssuesStateDistribution: TIssueSubIssuesStateDistributionMap = {};
|
||||
subIssues: TIssueSubIssuesIdMap = {};
|
||||
subIssueHelpers: Record<string, TSubIssueHelpers> = {};
|
||||
loader: TLoader = undefined;
|
||||
|
||||
filters: IWorkItemSubIssueFiltersStore;
|
||||
// root store
|
||||
rootIssueDetailStore: IIssueDetail;
|
||||
// services
|
||||
serviceType;
|
||||
issueService;
|
||||
|
||||
constructor(rootStore: IIssueDetail, serviceType: TIssueServiceType) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
subIssuesStateDistribution: observable,
|
||||
subIssues: observable,
|
||||
subIssueHelpers: observable,
|
||||
loader: observable.ref,
|
||||
// actions
|
||||
setSubIssueHelpers: action,
|
||||
fetchSubIssues: action,
|
||||
createSubIssues: action,
|
||||
updateSubIssue: action,
|
||||
removeSubIssue: action,
|
||||
deleteSubIssue: action,
|
||||
fetchOtherProjectProperties: action,
|
||||
});
|
||||
this.filters = new WorkItemSubIssueFiltersStore(this);
|
||||
// root store
|
||||
this.rootIssueDetailStore = rootStore;
|
||||
// services
|
||||
this.serviceType = serviceType;
|
||||
this.issueService = new IssueService(serviceType);
|
||||
}
|
||||
|
||||
// helper methods
|
||||
stateDistributionByIssueId = (issueId: string) => {
|
||||
if (!issueId) return undefined;
|
||||
return this.subIssuesStateDistribution[issueId] ?? undefined;
|
||||
};
|
||||
|
||||
subIssuesByIssueId = computedFn((issueId: string) => this.subIssues[issueId]);
|
||||
|
||||
subIssueHelpersByIssueId = (issueId: string) => ({
|
||||
preview_loader: this.subIssueHelpers?.[issueId]?.preview_loader || [],
|
||||
issue_visibility: this.subIssueHelpers?.[issueId]?.issue_visibility || [],
|
||||
issue_loader: this.subIssueHelpers?.[issueId]?.issue_loader || [],
|
||||
});
|
||||
|
||||
// actions
|
||||
setSubIssueHelpers = (parentIssueId: string, key: TSubIssueHelpersKeys, value: string) => {
|
||||
if (!parentIssueId || !key || !value) return;
|
||||
|
||||
update(this.subIssueHelpers, [parentIssueId, key], (_subIssueHelpers: string[] = []) => {
|
||||
if (_subIssueHelpers.includes(value)) return pull(_subIssueHelpers, value);
|
||||
return concat(_subIssueHelpers, value);
|
||||
});
|
||||
};
|
||||
|
||||
fetchSubIssues = async (workspaceSlug: string, projectId: string, parentIssueId: string) => {
|
||||
this.loader = "init-loader";
|
||||
const response = await this.issueService.subIssues(workspaceSlug, projectId, parentIssueId);
|
||||
|
||||
const subIssuesStateDistribution = response?.state_distribution ?? {};
|
||||
|
||||
const issueList = (response.sub_issues ?? []) as TIssue[];
|
||||
|
||||
this.rootIssueDetailStore.rootIssueStore.issues.addIssue(issueList);
|
||||
|
||||
// fetch other issues states and members when sub-issues are from different project
|
||||
if (issueList && issueList.length > 0) {
|
||||
const otherProjectIds = uniq(
|
||||
issueList.map((issue) => issue.project_id).filter((id) => !!id && id !== projectId)
|
||||
) as string[];
|
||||
this.fetchOtherProjectProperties(workspaceSlug, otherProjectIds);
|
||||
}
|
||||
if (issueList) {
|
||||
this.rootIssueDetailStore.rootIssueStore.issues.updateIssue(parentIssueId, {
|
||||
sub_issues_count: issueList.length,
|
||||
});
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
set(this.subIssuesStateDistribution, parentIssueId, subIssuesStateDistribution);
|
||||
set(
|
||||
this.subIssues,
|
||||
parentIssueId,
|
||||
issueList.map((issue) => issue.id)
|
||||
);
|
||||
});
|
||||
|
||||
this.loader = undefined;
|
||||
return response;
|
||||
};
|
||||
|
||||
createSubIssues = async (workspaceSlug: string, projectId: string, parentIssueId: string, issueIds: string[]) => {
|
||||
const response = await this.issueService.addSubIssues(workspaceSlug, projectId, parentIssueId, {
|
||||
sub_issue_ids: issueIds,
|
||||
});
|
||||
|
||||
const subIssuesStateDistribution = response?.state_distribution;
|
||||
const subIssues = response.sub_issues as TIssue[];
|
||||
|
||||
// fetch other issues states and members when sub-issues are from different project
|
||||
if (subIssues && subIssues.length > 0) {
|
||||
const otherProjectIds = uniq(
|
||||
subIssues.map((issue) => issue.project_id).filter((id) => !!id && id !== projectId)
|
||||
) as string[];
|
||||
this.fetchOtherProjectProperties(workspaceSlug, otherProjectIds);
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
Object.keys(subIssuesStateDistribution).forEach((key) => {
|
||||
const stateGroup = key as keyof TSubIssuesStateDistribution;
|
||||
update(this.subIssuesStateDistribution, [parentIssueId, stateGroup], (stateDistribution) => {
|
||||
if (!stateDistribution) return subIssuesStateDistribution[stateGroup];
|
||||
return concat(stateDistribution, subIssuesStateDistribution[stateGroup]);
|
||||
});
|
||||
});
|
||||
|
||||
const issueIds = subIssues.map((issue) => issue.id);
|
||||
update(this.subIssues, [parentIssueId], (issues) => {
|
||||
if (!issues) return issueIds;
|
||||
return concat(issues, issueIds);
|
||||
});
|
||||
});
|
||||
|
||||
this.rootIssueDetailStore.rootIssueStore.issues.addIssue(subIssues);
|
||||
|
||||
// update sub-issues_count of the parent issue
|
||||
set(
|
||||
this.rootIssueDetailStore.rootIssueStore.issues.issuesMap,
|
||||
[parentIssueId, "sub_issues_count"],
|
||||
this.subIssues[parentIssueId].length
|
||||
);
|
||||
|
||||
if (this.serviceType === EIssueServiceType.ISSUES) {
|
||||
updatePersistentLayer([parentIssueId, ...issueIds]);
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
updateSubIssue = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
parentIssueId: string,
|
||||
issueId: string,
|
||||
issueData: Partial<TIssue>,
|
||||
oldIssue: Partial<TIssue> = {},
|
||||
fromModal: boolean = false
|
||||
) => {
|
||||
if (!fromModal)
|
||||
await this.rootIssueDetailStore.rootIssueStore.projectIssues.updateIssue(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
issueData
|
||||
);
|
||||
|
||||
// parent update
|
||||
if (issueData.hasOwnProperty("parent_id") && issueData.parent_id !== oldIssue.parent_id) {
|
||||
runInAction(() => {
|
||||
if (oldIssue.parent_id) pull(this.subIssues[oldIssue.parent_id], issueId);
|
||||
if (issueData.parent_id)
|
||||
set(this.subIssues, [issueData.parent_id], concat(this.subIssues[issueData.parent_id], issueId));
|
||||
});
|
||||
}
|
||||
|
||||
// state update
|
||||
if (issueData.hasOwnProperty("state_id") && issueData.state_id !== oldIssue.state_id) {
|
||||
let oldIssueStateGroup: string | undefined = undefined;
|
||||
let issueStateGroup: string | undefined = undefined;
|
||||
|
||||
if (oldIssue.state_id) {
|
||||
const state = this.rootIssueDetailStore.rootIssueStore.rootStore.state.getStateById(oldIssue.state_id);
|
||||
if (state?.group) oldIssueStateGroup = state.group;
|
||||
}
|
||||
|
||||
if (issueData.state_id) {
|
||||
const state = this.rootIssueDetailStore.rootIssueStore.rootStore.state.getStateById(issueData.state_id);
|
||||
if (state?.group) issueStateGroup = state.group;
|
||||
}
|
||||
|
||||
if (oldIssueStateGroup && issueStateGroup && issueStateGroup !== oldIssueStateGroup) {
|
||||
runInAction(() => {
|
||||
if (oldIssueStateGroup)
|
||||
update(this.subIssuesStateDistribution, [parentIssueId, oldIssueStateGroup], (stateDistribution) => {
|
||||
if (!stateDistribution) return;
|
||||
return pull(stateDistribution, issueId);
|
||||
});
|
||||
|
||||
if (issueStateGroup)
|
||||
update(this.subIssuesStateDistribution, [parentIssueId, issueStateGroup], (stateDistribution) => {
|
||||
if (!stateDistribution) return [issueId];
|
||||
return concat(stateDistribution, issueId);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
removeSubIssue = async (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => {
|
||||
await this.rootIssueDetailStore.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, {
|
||||
parent_id: null,
|
||||
});
|
||||
|
||||
const issue = this.rootIssueDetailStore.issue.getIssueById(issueId);
|
||||
if (issue && issue.state_id) {
|
||||
let issueStateGroup: string | undefined = undefined;
|
||||
const state = this.rootIssueDetailStore.rootIssueStore.rootStore.state.getStateById(issue.state_id);
|
||||
if (state?.group) issueStateGroup = state.group;
|
||||
|
||||
if (issueStateGroup) {
|
||||
runInAction(() => {
|
||||
if (issueStateGroup)
|
||||
update(this.subIssuesStateDistribution, [parentIssueId, issueStateGroup], (stateDistribution) => {
|
||||
if (!stateDistribution) return;
|
||||
return pull(stateDistribution, issueId);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
pull(this.subIssues[parentIssueId], issueId);
|
||||
// update sub-issues_count of the parent issue
|
||||
set(
|
||||
this.rootIssueDetailStore.rootIssueStore.issues.issuesMap,
|
||||
[parentIssueId, "sub_issues_count"],
|
||||
this.subIssues[parentIssueId]?.length
|
||||
);
|
||||
});
|
||||
|
||||
if (this.serviceType === EIssueServiceType.ISSUES) {
|
||||
updatePersistentLayer([parentIssueId]);
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
deleteSubIssue = async (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => {
|
||||
await this.rootIssueDetailStore.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId);
|
||||
|
||||
const issue = this.rootIssueDetailStore.issue.getIssueById(issueId);
|
||||
if (issue && issue.state_id) {
|
||||
let issueStateGroup: string | undefined = undefined;
|
||||
const state = this.rootIssueDetailStore.rootIssueStore.rootStore.state.getStateById(issue.state_id);
|
||||
if (state?.group) issueStateGroup = state.group;
|
||||
|
||||
if (issueStateGroup) {
|
||||
runInAction(() => {
|
||||
if (issueStateGroup)
|
||||
update(this.subIssuesStateDistribution, [parentIssueId, issueStateGroup], (stateDistribution) => {
|
||||
if (!stateDistribution) return;
|
||||
return pull(stateDistribution, issueId);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
pull(this.subIssues[parentIssueId], issueId);
|
||||
// update sub-issues_count of the parent issue
|
||||
set(
|
||||
this.rootIssueDetailStore.rootIssueStore.issues.issuesMap,
|
||||
[parentIssueId, "sub_issues_count"],
|
||||
this.subIssues[parentIssueId]?.length
|
||||
);
|
||||
});
|
||||
|
||||
if (this.serviceType === EIssueServiceType.ISSUES) {
|
||||
updatePersistentLayer([parentIssueId]);
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
fetchOtherProjectProperties = async (workspaceSlug: string, projectIds: string[]) => {
|
||||
if (projectIds.length > 0) {
|
||||
for (const projectId of projectIds) {
|
||||
// fetching other project states
|
||||
this.rootIssueDetailStore.rootIssueStore.rootStore.state.fetchProjectStates(workspaceSlug, projectId);
|
||||
// fetching other project members
|
||||
this.rootIssueDetailStore.rootIssueStore.rootStore.memberRoot.project.fetchProjectMembers(
|
||||
workspaceSlug,
|
||||
projectId
|
||||
);
|
||||
// fetching other project labels
|
||||
this.rootIssueDetailStore.rootIssueStore.rootStore.label.fetchProjectLabels(workspaceSlug, projectId);
|
||||
// fetching other project cycles
|
||||
this.rootIssueDetailStore.rootIssueStore.rootStore.cycle.fetchAllCycles(workspaceSlug, projectId);
|
||||
// fetching other project modules
|
||||
this.rootIssueDetailStore.rootIssueStore.rootStore.module.fetchModules(workspaceSlug, projectId);
|
||||
// fetching other project estimates
|
||||
this.rootIssueDetailStore.rootIssueStore.rootStore.projectEstimate.getProjectEstimates(
|
||||
workspaceSlug,
|
||||
projectId
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { set } from "lodash-es";
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
import type { EIssueFilterType } from "@plane/constants";
|
||||
import type {
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
ISubWorkItemFilters,
|
||||
TGroupedIssues,
|
||||
TIssue,
|
||||
} from "@plane/types";
|
||||
import { getFilteredWorkItems, getGroupedWorkItemIds, updateSubWorkItemFilters } from "../helpers/base-issues-utils";
|
||||
import type { IssueSubIssuesStore } from "./sub_issues.store";
|
||||
|
||||
export const DEFAULT_DISPLAY_PROPERTIES = {
|
||||
key: true,
|
||||
issue_type: true,
|
||||
assignee: true,
|
||||
start_date: true,
|
||||
due_date: true,
|
||||
labels: true,
|
||||
priority: true,
|
||||
state: true,
|
||||
};
|
||||
export interface IWorkItemSubIssueFiltersStore {
|
||||
subIssueFilters: Record<string, Partial<ISubWorkItemFilters>>;
|
||||
// helpers methods
|
||||
updateSubWorkItemFilters: (
|
||||
filterType: EIssueFilterType,
|
||||
filters: IIssueDisplayFilterOptions | IIssueDisplayProperties | IIssueFilterOptions,
|
||||
workItemId: string
|
||||
) => void;
|
||||
getGroupedSubWorkItems: (workItemId: string) => TGroupedIssues;
|
||||
getFilteredSubWorkItems: (workItemId: string, filters: IIssueFilterOptions) => TIssue[];
|
||||
getSubIssueFilters: (workItemId: string) => Partial<ISubWorkItemFilters>;
|
||||
resetFilters: (workItemId: string) => void;
|
||||
}
|
||||
|
||||
export class WorkItemSubIssueFiltersStore implements IWorkItemSubIssueFiltersStore {
|
||||
// observables
|
||||
subIssueFilters: Record<string, Partial<ISubWorkItemFilters>> = {};
|
||||
|
||||
// root store
|
||||
subIssueStore: IssueSubIssuesStore;
|
||||
|
||||
constructor(subIssueStore: IssueSubIssuesStore) {
|
||||
makeObservable(this, {
|
||||
subIssueFilters: observable,
|
||||
updateSubWorkItemFilters: action,
|
||||
getSubIssueFilters: action,
|
||||
});
|
||||
|
||||
// root store
|
||||
this.subIssueStore = subIssueStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description This method is used to get the sub issue filters
|
||||
* @param workItemId
|
||||
* @returns
|
||||
*/
|
||||
getSubIssueFilters = (workItemId: string) => {
|
||||
if (!this.subIssueFilters[workItemId]) {
|
||||
this.initializeFilters(workItemId);
|
||||
}
|
||||
return this.subIssueFilters[workItemId];
|
||||
};
|
||||
|
||||
/**
|
||||
* @description This method is used to initialize the sub issue filters
|
||||
* @param workItemId
|
||||
*/
|
||||
initializeFilters = (workItemId: string) => {
|
||||
set(this.subIssueFilters, [workItemId, "displayProperties"], DEFAULT_DISPLAY_PROPERTIES);
|
||||
set(this.subIssueFilters, [workItemId, "filters"], {});
|
||||
set(this.subIssueFilters, [workItemId, "displayFilters"], {});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description This method updates filters for sub issues.
|
||||
* @param filterType
|
||||
* @param filters
|
||||
*/
|
||||
updateSubWorkItemFilters = (
|
||||
filterType: EIssueFilterType,
|
||||
filters: IIssueDisplayFilterOptions | IIssueDisplayProperties | IIssueFilterOptions,
|
||||
workItemId: string
|
||||
) => {
|
||||
runInAction(() => {
|
||||
updateSubWorkItemFilters(this.subIssueFilters, filterType, filters, workItemId);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description This method is used to get the grouped sub work items
|
||||
* @param parentWorkItemId
|
||||
* @returns
|
||||
*/
|
||||
getGroupedSubWorkItems = computedFn((parentWorkItemId: string) => {
|
||||
const subIssueFilters = this.getSubIssueFilters(parentWorkItemId);
|
||||
|
||||
const filteredWorkItems = this.getFilteredSubWorkItems(parentWorkItemId, subIssueFilters.filters ?? {});
|
||||
|
||||
// get group by and order by
|
||||
const groupByKey = subIssueFilters.displayFilters?.group_by;
|
||||
const orderByKey = subIssueFilters.displayFilters?.order_by;
|
||||
|
||||
const groupedWorkItemIds = getGroupedWorkItemIds(filteredWorkItems, groupByKey, orderByKey);
|
||||
|
||||
return groupedWorkItemIds;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description This method is used to get the filtered sub work items
|
||||
* @param workItemId
|
||||
* @returns
|
||||
*/
|
||||
getFilteredSubWorkItems = computedFn((workItemId: string, filters: IIssueFilterOptions) => {
|
||||
const subIssueIds = this.subIssueStore.subIssuesByIssueId(workItemId);
|
||||
const workItems = this.subIssueStore.rootIssueDetailStore.rootIssueStore.issues.getIssuesByIds(
|
||||
subIssueIds,
|
||||
"un-archived"
|
||||
);
|
||||
|
||||
const filteredWorkItems = getFilteredWorkItems(workItems, filters);
|
||||
|
||||
return filteredWorkItems;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description This method is used to reset the filters
|
||||
* @param workItemId
|
||||
*/
|
||||
resetFilters = (workItemId: string) => {
|
||||
this.initializeFilters(workItemId);
|
||||
};
|
||||
}
|
||||
104
apps/web/core/store/issue/issue-details/subscription.store.ts
Normal file
104
apps/web/core/store/issue/issue-details/subscription.store.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { set } from "lodash-es";
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
// services
|
||||
import type { EIssueServiceType } from "@plane/types";
|
||||
import { IssueService } from "@/services/issue/issue.service";
|
||||
// types
|
||||
import type { IIssueDetail } from "./root.store";
|
||||
export interface IIssueSubscriptionStoreActions {
|
||||
addSubscription: (issueId: string, isSubscribed: boolean | undefined | null) => void;
|
||||
fetchSubscriptions: (workspaceSlug: string, projectId: string, issueId: string) => Promise<boolean>;
|
||||
createSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
removeSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface IIssueSubscriptionStore extends IIssueSubscriptionStoreActions {
|
||||
// observables
|
||||
subscriptionMap: Record<string, Record<string, boolean>>; // Record defines subscriptionId as key and link as value
|
||||
// helper methods
|
||||
getSubscriptionByIssueId: (issueId: string) => boolean | undefined;
|
||||
}
|
||||
|
||||
export class IssueSubscriptionStore implements IIssueSubscriptionStore {
|
||||
// observables
|
||||
subscriptionMap: Record<string, Record<string, boolean>> = {};
|
||||
// root store
|
||||
rootIssueDetail: IIssueDetail;
|
||||
// services
|
||||
issueService;
|
||||
|
||||
constructor(rootStore: IIssueDetail, serviceType: EIssueServiceType) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
subscriptionMap: observable,
|
||||
// actions
|
||||
addSubscription: action.bound,
|
||||
fetchSubscriptions: action,
|
||||
createSubscription: action,
|
||||
removeSubscription: action,
|
||||
});
|
||||
// root store
|
||||
this.rootIssueDetail = rootStore;
|
||||
// services
|
||||
this.issueService = new IssueService(serviceType);
|
||||
}
|
||||
|
||||
// helper methods
|
||||
getSubscriptionByIssueId = (issueId: string) => {
|
||||
if (!issueId) return undefined;
|
||||
const currentUserId = this.rootIssueDetail.rootIssueStore.currentUserId;
|
||||
if (!currentUserId) return undefined;
|
||||
return this.subscriptionMap[issueId]?.[currentUserId] ?? undefined;
|
||||
};
|
||||
|
||||
addSubscription = (issueId: string, isSubscribed: boolean | undefined | null) => {
|
||||
const currentUserId = this.rootIssueDetail.rootIssueStore.currentUserId;
|
||||
if (!currentUserId) throw new Error("user id not available");
|
||||
|
||||
runInAction(() => {
|
||||
set(this.subscriptionMap, [issueId, currentUserId], isSubscribed ?? false);
|
||||
});
|
||||
};
|
||||
|
||||
fetchSubscriptions = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
const subscription = await this.issueService.getIssueNotificationSubscriptionStatus(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId
|
||||
);
|
||||
this.addSubscription(issueId, subscription?.subscribed);
|
||||
return subscription?.subscribed;
|
||||
};
|
||||
|
||||
createSubscription = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
const currentUserId = this.rootIssueDetail.rootIssueStore.currentUserId;
|
||||
if (!currentUserId) throw new Error("user id not available");
|
||||
|
||||
runInAction(() => {
|
||||
set(this.subscriptionMap, [issueId, currentUserId], true);
|
||||
});
|
||||
|
||||
await this.issueService.subscribeToIssueNotifications(workspaceSlug, projectId, issueId);
|
||||
} catch (error) {
|
||||
this.fetchSubscriptions(workspaceSlug, projectId, issueId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
removeSubscription = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
const currentUserId = this.rootIssueDetail.rootIssueStore.currentUserId;
|
||||
if (!currentUserId) throw new Error("user id not available");
|
||||
|
||||
runInAction(() => {
|
||||
set(this.subscriptionMap, [issueId, currentUserId], false);
|
||||
});
|
||||
|
||||
await this.issueService.unsubscribeFromIssueNotifications(workspaceSlug, projectId, issueId);
|
||||
} catch (error) {
|
||||
this.fetchSubscriptions(workspaceSlug, projectId, issueId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
172
apps/web/core/store/issue/issue.store.ts
Normal file
172
apps/web/core/store/issue/issue.store.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { clone, set, update } from "lodash-es";
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// types
|
||||
import type { TIssue } from "@plane/types";
|
||||
// helpers
|
||||
import { getCurrentDateTimeInISO } from "@plane/utils";
|
||||
import { rootStore } from "@/lib/store-context";
|
||||
// services
|
||||
import { deleteIssueFromLocal } from "@/local-db/utils/load-issues";
|
||||
import { updatePersistentLayer } from "@/local-db/utils/utils";
|
||||
import { IssueService } from "@/services/issue";
|
||||
|
||||
export type IIssueStore = {
|
||||
// observables
|
||||
issuesMap: Record<string, TIssue>; // Record defines issue_id as key and TIssue as value
|
||||
issuesIdentifierMap: Record<string, string>; // Record defines issue_identifier as key and issue_id as value
|
||||
// actions
|
||||
getIssues(workspaceSlug: string, projectId: string, issueIds: string[]): Promise<TIssue[]>;
|
||||
addIssue(issues: TIssue[]): void;
|
||||
addIssueIdentifier(issueIdentifier: string, issueId: string): void;
|
||||
updateIssue(issueId: string, issue: Partial<TIssue>): void;
|
||||
removeIssue(issueId: string): void;
|
||||
// helper methods
|
||||
getIssueById(issueId: string): undefined | TIssue;
|
||||
getIssueIdByIdentifier(issueIdentifier: string): undefined | string;
|
||||
getIssuesByIds(issueIds: string[], type: "archived" | "un-archived"): TIssue[]; // Record defines issue_id as key and TIssue as value
|
||||
};
|
||||
|
||||
export class IssueStore implements IIssueStore {
|
||||
// observables
|
||||
issuesMap: { [issue_id: string]: TIssue } = {};
|
||||
issuesIdentifierMap: { [issue_identifier: string]: string } = {};
|
||||
// service
|
||||
issueService;
|
||||
|
||||
constructor() {
|
||||
makeObservable(this, {
|
||||
// observable
|
||||
issuesMap: observable,
|
||||
issuesIdentifierMap: observable,
|
||||
// actions
|
||||
addIssue: action,
|
||||
addIssueIdentifier: action,
|
||||
updateIssue: action,
|
||||
removeIssue: action,
|
||||
});
|
||||
this.issueService = new IssueService();
|
||||
}
|
||||
|
||||
// actions
|
||||
/**
|
||||
* @description This method will add issues to the issuesMap
|
||||
* @param {TIssue[]} issues
|
||||
* @returns {void}
|
||||
*/
|
||||
addIssue = (issues: TIssue[]) => {
|
||||
if (issues && issues.length <= 0) return;
|
||||
runInAction(() => {
|
||||
issues.forEach((issue) => {
|
||||
// add issue identifier to the issuesIdentifierMap
|
||||
const projectIdentifier = rootStore.projectRoot.project.getProjectIdentifierById(issue?.project_id);
|
||||
const workItemSequenceId = issue?.sequence_id;
|
||||
const issueIdentifier = `${projectIdentifier}-${workItemSequenceId}`;
|
||||
set(this.issuesIdentifierMap, issueIdentifier, issue.id);
|
||||
|
||||
if (!this.issuesMap[issue.id]) set(this.issuesMap, issue.id, issue);
|
||||
else update(this.issuesMap, issue.id, (prevIssue) => ({ ...prevIssue, ...issue }));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description This method will add issue_identifier to the issuesIdentifierMap
|
||||
* @param issueIdentifier
|
||||
* @param issueId
|
||||
* @returns {void}
|
||||
*/
|
||||
addIssueIdentifier = (issueIdentifier: string, issueId: string) => {
|
||||
if (!issueIdentifier || !issueId) return;
|
||||
runInAction(() => {
|
||||
set(this.issuesIdentifierMap, issueIdentifier, issueId);
|
||||
});
|
||||
};
|
||||
|
||||
getIssues = async (workspaceSlug: string, projectId: string, issueIds: string[]) => {
|
||||
const issues = await this.issueService.retrieveIssues(workspaceSlug, projectId, issueIds);
|
||||
|
||||
runInAction(() => {
|
||||
issues.forEach((issue) => {
|
||||
if (!this.issuesMap[issue.id]) set(this.issuesMap, issue.id, issue);
|
||||
});
|
||||
});
|
||||
|
||||
return issues;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description This method will update the issue in the issuesMap
|
||||
* @param {string} issueId
|
||||
* @param {Partial<TIssue>} issue
|
||||
* @returns {void}
|
||||
*/
|
||||
updateIssue = (issueId: string, issue: Partial<TIssue>) => {
|
||||
if (!issue || !issueId || !this.issuesMap[issueId]) return;
|
||||
const issueBeforeUpdate = clone(this.issuesMap[issueId]);
|
||||
runInAction(() => {
|
||||
set(this.issuesMap, [issueId, "updated_at"], getCurrentDateTimeInISO());
|
||||
Object.keys(issue).forEach((key) => {
|
||||
set(this.issuesMap, [issueId, key], issue[key as keyof TIssue]);
|
||||
});
|
||||
});
|
||||
|
||||
if (!issueBeforeUpdate.is_epic) {
|
||||
updatePersistentLayer(issueId);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description This method will remove the issue from the issuesMap
|
||||
* @param {string} issueId
|
||||
* @returns {void}
|
||||
*/
|
||||
removeIssue = (issueId: string) => {
|
||||
if (!issueId || !this.issuesMap[issueId]) return;
|
||||
runInAction(() => {
|
||||
delete this.issuesMap[issueId];
|
||||
});
|
||||
deleteIssueFromLocal(issueId);
|
||||
};
|
||||
|
||||
// helper methods
|
||||
/**
|
||||
* @description This method will return the issue from the issuesMap
|
||||
* @param {string} issueId
|
||||
* @returns {TIssue | undefined}
|
||||
*/
|
||||
getIssueById = computedFn((issueId: string) => {
|
||||
if (!issueId || !this.issuesMap[issueId]) return undefined;
|
||||
return this.issuesMap[issueId];
|
||||
});
|
||||
|
||||
/**
|
||||
* @description This method will return the issue_id from the issuesIdentifierMap
|
||||
* @param {string} issueIdentifier
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
getIssueIdByIdentifier = computedFn((issueIdentifier: string) => {
|
||||
if (!issueIdentifier || !this.issuesIdentifierMap[issueIdentifier]) return undefined;
|
||||
return this.issuesIdentifierMap[issueIdentifier];
|
||||
});
|
||||
|
||||
/**
|
||||
* @description This method will return the issues from the issuesMap
|
||||
* @param {string[]} issueIds
|
||||
* @param {boolean} archivedIssues
|
||||
* @returns {Record<string, TIssue> | undefined}
|
||||
*/
|
||||
getIssuesByIds = computedFn((issueIds: string[], type: "archived" | "un-archived") => {
|
||||
if (!issueIds || issueIds.length <= 0) return [];
|
||||
const filteredIssues: TIssue[] = [];
|
||||
Object.values(issueIds).forEach((issueId) => {
|
||||
// if type is archived then check archived_at is not null
|
||||
// if type is un-archived then check archived_at is null
|
||||
const issue = this.issuesMap[issueId];
|
||||
if (issue && ((type === "archived" && issue.archived_at) || (type === "un-archived" && !issue?.archived_at))) {
|
||||
filteredIssues.push(issue);
|
||||
}
|
||||
});
|
||||
return filteredIssues;
|
||||
});
|
||||
}
|
||||
209
apps/web/core/store/issue/issue_calendar_view.store.ts
Normal file
209
apps/web/core/store/issue/issue_calendar_view.store.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import { observable, action, makeObservable, runInAction, computed, reaction } from "mobx";
|
||||
|
||||
// helpers
|
||||
import { computedFn } from "mobx-utils";
|
||||
import type { ICalendarPayload, ICalendarWeek } from "@plane/types";
|
||||
import { EStartOfTheWeek } from "@plane/types";
|
||||
import { generateCalendarData, getWeekNumberOfDate } from "@plane/utils";
|
||||
// types
|
||||
import type { IIssueRootStore } from "./root.store";
|
||||
|
||||
export interface ICalendarStore {
|
||||
calendarFilters: {
|
||||
activeMonthDate: Date;
|
||||
activeWeekDate: Date;
|
||||
};
|
||||
calendarPayload: ICalendarPayload | null;
|
||||
|
||||
// action
|
||||
updateCalendarFilters: (filters: Partial<{ activeMonthDate: Date; activeWeekDate: Date }>) => void;
|
||||
updateCalendarPayload: (date: Date) => void;
|
||||
regenerateCalendar: () => void;
|
||||
|
||||
// computed
|
||||
allWeeksOfActiveMonth:
|
||||
| {
|
||||
[weekNumber: string]: ICalendarWeek;
|
||||
}
|
||||
| undefined;
|
||||
activeWeekNumber: number;
|
||||
allDaysOfActiveWeek: ICalendarWeek | undefined;
|
||||
getStartAndEndDate: (layout: "week" | "month") => { startDate: string; endDate: string } | undefined;
|
||||
}
|
||||
|
||||
export class CalendarStore implements ICalendarStore {
|
||||
loader: boolean = false;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
error: any | null = null;
|
||||
|
||||
// observables
|
||||
calendarFilters: { activeMonthDate: Date; activeWeekDate: Date } = {
|
||||
activeMonthDate: new Date(),
|
||||
activeWeekDate: new Date(),
|
||||
};
|
||||
calendarPayload: ICalendarPayload | null = null;
|
||||
// root store
|
||||
rootStore;
|
||||
|
||||
constructor(_rootStore: IIssueRootStore) {
|
||||
makeObservable(this, {
|
||||
loader: observable.ref,
|
||||
error: observable.ref,
|
||||
|
||||
// observables
|
||||
calendarFilters: observable.ref,
|
||||
calendarPayload: observable.ref,
|
||||
|
||||
// actions
|
||||
updateCalendarFilters: action,
|
||||
updateCalendarPayload: action,
|
||||
regenerateCalendar: action,
|
||||
|
||||
//computed
|
||||
allWeeksOfActiveMonth: computed,
|
||||
activeWeekNumber: computed,
|
||||
allDaysOfActiveWeek: computed,
|
||||
});
|
||||
|
||||
this.rootStore = _rootStore;
|
||||
this.initCalendar();
|
||||
|
||||
// Watch for changes in startOfWeek preference and regenerate calendar
|
||||
reaction(
|
||||
() => this.rootStore.rootStore.user.userProfile.data?.start_of_the_week,
|
||||
() => {
|
||||
// Regenerate calendar when startOfWeek preference changes
|
||||
this.regenerateCalendar();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
get allWeeksOfActiveMonth() {
|
||||
if (!this.calendarPayload) return undefined;
|
||||
|
||||
const { activeMonthDate } = this.calendarFilters;
|
||||
|
||||
const year = activeMonthDate.getFullYear();
|
||||
const month = activeMonthDate.getMonth();
|
||||
|
||||
// Get the weeks for the current month
|
||||
const weeks = this.calendarPayload[`y-${year}`][`m-${month}`];
|
||||
|
||||
// If no weeks exist, return undefined
|
||||
if (!weeks) return undefined;
|
||||
|
||||
// Create a new object to store the reordered weeks
|
||||
const reorderedWeeks: { [weekNumber: string]: ICalendarWeek } = {};
|
||||
|
||||
// Get all week numbers and sort them
|
||||
const weekNumbers = Object.keys(weeks).map((key) => parseInt(key.replace("w-", "")));
|
||||
weekNumbers.sort((a, b) => a - b);
|
||||
|
||||
// Reorder weeks based on start_of_week
|
||||
weekNumbers.forEach((weekNumber) => {
|
||||
const weekKey = `w-${weekNumber}`;
|
||||
reorderedWeeks[weekKey] = weeks[weekKey];
|
||||
});
|
||||
|
||||
return reorderedWeeks;
|
||||
}
|
||||
|
||||
get activeWeekNumber() {
|
||||
return getWeekNumberOfDate(this.calendarFilters.activeWeekDate);
|
||||
}
|
||||
|
||||
get allDaysOfActiveWeek() {
|
||||
if (!this.calendarPayload) return undefined;
|
||||
|
||||
const { activeWeekDate } = this.calendarFilters;
|
||||
const year = activeWeekDate.getFullYear();
|
||||
const month = activeWeekDate.getMonth();
|
||||
const dayOfMonth = activeWeekDate.getDate();
|
||||
|
||||
// Check if calendar data exists for this year and month
|
||||
const yearData = this.calendarPayload[`y-${year}`];
|
||||
if (!yearData) return undefined;
|
||||
|
||||
const monthData = yearData[`m-${month}`];
|
||||
if (!monthData) return undefined;
|
||||
|
||||
// Calculate firstDayOfMonth offset (same logic as calendar generation)
|
||||
const startOfWeek = this.rootStore?.rootStore?.user?.userProfile?.data?.start_of_the_week ?? EStartOfTheWeek.SUNDAY;
|
||||
const firstDayOfMonthRaw = new Date(year, month, 1).getDay();
|
||||
const firstDayOfMonth = (firstDayOfMonthRaw - startOfWeek + 7) % 7;
|
||||
|
||||
// Calculate which sequential week this date falls into
|
||||
const weekIndex = Math.floor((dayOfMonth - 1 + firstDayOfMonth) / 7);
|
||||
|
||||
const weekKey = `w-${weekIndex}`;
|
||||
if (!(weekKey in monthData)) {
|
||||
return undefined;
|
||||
}
|
||||
return monthData[weekKey];
|
||||
}
|
||||
|
||||
getStartAndEndDate = computedFn((layout: "week" | "month") => {
|
||||
switch (layout) {
|
||||
case "week": {
|
||||
if (!this.allDaysOfActiveWeek) return;
|
||||
const dates = Object.keys(this.allDaysOfActiveWeek);
|
||||
return { startDate: dates[0], endDate: dates[dates.length - 1] };
|
||||
}
|
||||
case "month": {
|
||||
if (!this.allWeeksOfActiveMonth) return;
|
||||
const weeks = Object.keys(this.allWeeksOfActiveMonth);
|
||||
const firstWeekDates = Object.keys(this.allWeeksOfActiveMonth[weeks[0]]);
|
||||
const lastWeekDates = Object.keys(this.allWeeksOfActiveMonth[weeks[weeks.length - 1]]);
|
||||
|
||||
return { startDate: firstWeekDates[0], endDate: lastWeekDates[lastWeekDates.length - 1] };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
updateCalendarFilters = (filters: Partial<{ activeMonthDate: Date; activeWeekDate: Date }>) => {
|
||||
this.updateCalendarPayload(filters.activeMonthDate || filters.activeWeekDate || new Date());
|
||||
|
||||
runInAction(() => {
|
||||
this.calendarFilters = {
|
||||
...this.calendarFilters,
|
||||
...filters,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
updateCalendarPayload = (date: Date) => {
|
||||
if (!this.calendarPayload) return null;
|
||||
|
||||
const nextDate = new Date(date);
|
||||
const startOfWeek = this.rootStore.rootStore.user.userProfile.data?.start_of_the_week ?? EStartOfTheWeek.SUNDAY;
|
||||
|
||||
runInAction(() => {
|
||||
this.calendarPayload = generateCalendarData(this.calendarPayload, nextDate, startOfWeek);
|
||||
});
|
||||
};
|
||||
|
||||
initCalendar = () => {
|
||||
const startOfWeek = this.rootStore.rootStore.user.userProfile.data?.start_of_the_week ?? EStartOfTheWeek.SUNDAY;
|
||||
const newCalendarPayload = generateCalendarData(null, new Date(), startOfWeek);
|
||||
|
||||
runInAction(() => {
|
||||
this.calendarPayload = newCalendarPayload;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Force complete regeneration of calendar data
|
||||
* This should be called when startOfWeek preference changes
|
||||
*/
|
||||
regenerateCalendar = () => {
|
||||
const startOfWeek = this.rootStore.rootStore.user.userProfile.data?.start_of_the_week ?? EStartOfTheWeek.SUNDAY;
|
||||
const { activeMonthDate } = this.calendarFilters;
|
||||
|
||||
// Force complete regeneration by passing null to clear all cached data
|
||||
const newCalendarPayload = generateCalendarData(null, activeMonthDate, startOfWeek);
|
||||
|
||||
runInAction(() => {
|
||||
this.calendarPayload = newCalendarPayload;
|
||||
});
|
||||
};
|
||||
}
|
||||
95
apps/web/core/store/issue/issue_gantt_view.store.ts
Normal file
95
apps/web/core/store/issue/issue_gantt_view.store.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// helpers
|
||||
import type { ChartDataType, TGanttViews } from "@plane/types";
|
||||
import { currentViewDataWithView } from "@/components/gantt-chart/data";
|
||||
// types
|
||||
|
||||
export interface IGanttStore {
|
||||
// observables
|
||||
currentView: TGanttViews;
|
||||
currentViewData: ChartDataType | undefined;
|
||||
activeBlockId: string | null;
|
||||
renderView: any;
|
||||
// computed functions
|
||||
isBlockActive: (blockId: string) => boolean;
|
||||
// actions
|
||||
updateCurrentView: (view: TGanttViews) => void;
|
||||
updateCurrentViewData: (data: ChartDataType | undefined) => void;
|
||||
updateActiveBlockId: (blockId: string | null) => void;
|
||||
updateRenderView: (data: any[]) => void;
|
||||
}
|
||||
|
||||
export class GanttStore implements IGanttStore {
|
||||
// observables
|
||||
currentView: TGanttViews = "month";
|
||||
currentViewData: ChartDataType | undefined = undefined;
|
||||
activeBlockId: string | null = null;
|
||||
renderView: any[] = [];
|
||||
|
||||
constructor() {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
currentView: observable.ref,
|
||||
currentViewData: observable,
|
||||
activeBlockId: observable.ref,
|
||||
renderView: observable,
|
||||
// actions
|
||||
updateCurrentView: action.bound,
|
||||
updateCurrentViewData: action.bound,
|
||||
updateActiveBlockId: action.bound,
|
||||
updateRenderView: action.bound,
|
||||
});
|
||||
|
||||
this.initGantt();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description check if block is active
|
||||
* @param {string} blockId
|
||||
*/
|
||||
isBlockActive = computedFn((blockId: string): boolean => this.activeBlockId === blockId);
|
||||
|
||||
/**
|
||||
* @description update current view
|
||||
* @param {TGanttViews} view
|
||||
*/
|
||||
updateCurrentView = (view: TGanttViews) => {
|
||||
this.currentView = view;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description update current view data
|
||||
* @param {ChartDataType | undefined} data
|
||||
*/
|
||||
updateCurrentViewData = (data: ChartDataType | undefined) => {
|
||||
this.currentViewData = data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description update active block
|
||||
* @param {string | null} block
|
||||
*/
|
||||
updateActiveBlockId = (blockId: string | null) => {
|
||||
this.activeBlockId = blockId;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description update render view
|
||||
* @param {any[]} data
|
||||
*/
|
||||
updateRenderView = (data: any[]) => {
|
||||
this.renderView = data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description initialize gantt chart with month view
|
||||
*/
|
||||
initGantt = () => {
|
||||
const newCurrentViewData = currentViewDataWithView(this.currentView);
|
||||
|
||||
runInAction(() => {
|
||||
this.currentViewData = newCurrentViewData;
|
||||
});
|
||||
};
|
||||
}
|
||||
83
apps/web/core/store/issue/issue_kanban_view.store.ts
Normal file
83
apps/web/core/store/issue/issue_kanban_view.store.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { action, computed, makeObservable, observable } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
import { DRAG_ALLOWED_GROUPS } from "@plane/constants";
|
||||
// types
|
||||
import type { TIssueGroupByOptions } from "@plane/types";
|
||||
// constants
|
||||
// store
|
||||
import type { IssueRootStore } from "./root.store";
|
||||
|
||||
export interface IIssueKanBanViewStore {
|
||||
kanBanToggle: {
|
||||
groupByHeaderMinMax: string[];
|
||||
subgroupByIssuesVisibility: string[];
|
||||
};
|
||||
isDragging: boolean;
|
||||
// computed
|
||||
getCanUserDragDrop: (
|
||||
group_by: TIssueGroupByOptions | undefined,
|
||||
sub_group_by: TIssueGroupByOptions | undefined
|
||||
) => boolean;
|
||||
canUserDragDropVertically: boolean;
|
||||
canUserDragDropHorizontally: boolean;
|
||||
// actions
|
||||
handleKanBanToggle: (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => void;
|
||||
setIsDragging: (isDragging: boolean) => void;
|
||||
}
|
||||
|
||||
export class IssueKanBanViewStore implements IIssueKanBanViewStore {
|
||||
kanBanToggle: {
|
||||
groupByHeaderMinMax: string[];
|
||||
subgroupByIssuesVisibility: string[];
|
||||
} = { groupByHeaderMinMax: [], subgroupByIssuesVisibility: [] };
|
||||
isDragging = false;
|
||||
// root store
|
||||
rootStore;
|
||||
|
||||
constructor(_rootStore: IssueRootStore) {
|
||||
makeObservable(this, {
|
||||
kanBanToggle: observable,
|
||||
isDragging: observable.ref,
|
||||
// computed
|
||||
canUserDragDropVertically: computed,
|
||||
canUserDragDropHorizontally: computed,
|
||||
|
||||
// actions
|
||||
handleKanBanToggle: action,
|
||||
setIsDragging: action.bound,
|
||||
});
|
||||
|
||||
this.rootStore = _rootStore;
|
||||
}
|
||||
|
||||
setIsDragging = (isDragging: boolean) => {
|
||||
this.isDragging = isDragging;
|
||||
};
|
||||
|
||||
getCanUserDragDrop = computedFn(
|
||||
(group_by: TIssueGroupByOptions | undefined, sub_group_by: TIssueGroupByOptions | undefined) => {
|
||||
if (group_by && DRAG_ALLOWED_GROUPS.includes(group_by)) {
|
||||
if (!sub_group_by) return true;
|
||||
if (sub_group_by && DRAG_ALLOWED_GROUPS.includes(sub_group_by)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
get canUserDragDropVertically() {
|
||||
return false;
|
||||
}
|
||||
|
||||
get canUserDragDropHorizontally() {
|
||||
return false;
|
||||
}
|
||||
|
||||
handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
|
||||
this.kanBanToggle = {
|
||||
...this.kanBanToggle,
|
||||
[toggle]: this.kanBanToggle[toggle].includes(value)
|
||||
? this.kanBanToggle[toggle].filter((v) => v !== value)
|
||||
: [...this.kanBanToggle[toggle], value],
|
||||
};
|
||||
};
|
||||
}
|
||||
319
apps/web/core/store/issue/module/filter.store.ts
Normal file
319
apps/web/core/store/issue/module/filter.store.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
import { isEmpty, set } from "lodash-es";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
// base class
|
||||
import { computedFn } from "mobx-utils";
|
||||
import type { TSupportedFilterTypeForUpdate } from "@plane/constants";
|
||||
import { EIssueFilterType } from "@plane/constants";
|
||||
import type {
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
TIssueKanbanFilters,
|
||||
IIssueFilters,
|
||||
TIssueParams,
|
||||
IssuePaginationOptions,
|
||||
TWorkItemFilterExpression,
|
||||
TSupportedFilterForUpdate,
|
||||
} from "@plane/types";
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
import { handleIssueQueryParamsByLayout } from "@plane/utils";
|
||||
import { IssueFiltersService } from "@/services/issue_filter.service";
|
||||
import type { IBaseIssueFilterStore } from "../helpers/issue-filter-helper.store";
|
||||
import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store";
|
||||
// helpers
|
||||
// types
|
||||
import type { IIssueRootStore } from "../root.store";
|
||||
// constants
|
||||
// services
|
||||
|
||||
export interface IModuleIssuesFilter extends IBaseIssueFilterStore {
|
||||
//helper actions
|
||||
getFilterParams: (
|
||||
options: IssuePaginationOptions,
|
||||
moduleId: string,
|
||||
cursor: string | undefined,
|
||||
groupId: string | undefined,
|
||||
subGroupId: string | undefined
|
||||
) => Partial<Record<TIssueParams, string | boolean>>;
|
||||
getIssueFilters(moduleId: string): IIssueFilters | undefined;
|
||||
// action
|
||||
fetchFilters: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<void>;
|
||||
updateFilterExpression: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
moduleId: string,
|
||||
filters: TWorkItemFilterExpression
|
||||
) => Promise<void>;
|
||||
updateFilters: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
filterType: TSupportedFilterTypeForUpdate,
|
||||
filters: TSupportedFilterForUpdate,
|
||||
moduleId: string
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export class ModuleIssuesFilter extends IssueFilterHelperStore implements IModuleIssuesFilter {
|
||||
// observables
|
||||
filters: { [moduleId: string]: IIssueFilters } = {};
|
||||
// root store
|
||||
rootIssueStore: IIssueRootStore;
|
||||
// services
|
||||
issueFilterService;
|
||||
|
||||
constructor(_rootStore: IIssueRootStore) {
|
||||
super();
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
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 moduleId = this.rootIssueStore.moduleId;
|
||||
if (!moduleId) return undefined;
|
||||
|
||||
return this.getIssueFilters(moduleId);
|
||||
}
|
||||
|
||||
get appliedFilters() {
|
||||
const moduleId = this.rootIssueStore.moduleId;
|
||||
if (!moduleId) return undefined;
|
||||
|
||||
return this.getAppliedFilters(moduleId);
|
||||
}
|
||||
|
||||
getIssueFilters(moduleId: string) {
|
||||
const displayFilters = this.filters[moduleId] || undefined;
|
||||
if (isEmpty(displayFilters)) return undefined;
|
||||
|
||||
const _filters: IIssueFilters = this.computedIssueFilters(displayFilters);
|
||||
|
||||
return _filters;
|
||||
}
|
||||
|
||||
getAppliedFilters(moduleId: string) {
|
||||
const userFilters = this.getIssueFilters(moduleId);
|
||||
if (!userFilters) return undefined;
|
||||
|
||||
const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "issues");
|
||||
if (!filteredParams) return undefined;
|
||||
|
||||
if (filteredParams.includes("module")) filteredParams.splice(filteredParams.indexOf("module"), 1);
|
||||
|
||||
const filteredRouteParams: Partial<Record<TIssueParams, string | boolean>> = this.computedFilteredParams(
|
||||
userFilters?.richFilters,
|
||||
userFilters?.displayFilters,
|
||||
filteredParams
|
||||
);
|
||||
|
||||
return filteredRouteParams;
|
||||
}
|
||||
|
||||
getFilterParams = computedFn(
|
||||
(
|
||||
options: IssuePaginationOptions,
|
||||
moduleId: string,
|
||||
cursor: string | undefined,
|
||||
groupId: string | undefined,
|
||||
subGroupId: string | undefined
|
||||
) => {
|
||||
let filterParams = this.getAppliedFilters(moduleId);
|
||||
|
||||
if (!filterParams) {
|
||||
filterParams = {};
|
||||
}
|
||||
filterParams["module"] = moduleId;
|
||||
|
||||
const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId);
|
||||
return paginationParams;
|
||||
}
|
||||
);
|
||||
|
||||
fetchFilters = async (workspaceSlug: string, projectId: string, moduleId: string) => {
|
||||
const _filters = await this.issueFilterService.fetchModuleIssueFilters(workspaceSlug, projectId, moduleId);
|
||||
|
||||
const richFilters: TWorkItemFilterExpression = _filters?.rich_filters;
|
||||
const displayFilters: IIssueDisplayFilterOptions = this.computedDisplayFilters(_filters?.display_filters);
|
||||
const displayProperties: IIssueDisplayProperties = this.computedDisplayProperties(_filters?.display_properties);
|
||||
|
||||
// fetching the kanban toggle helpers in the local storage
|
||||
const kanbanFilters = {
|
||||
group_by: [],
|
||||
sub_group_by: [],
|
||||
};
|
||||
const currentUserId = this.rootIssueStore.currentUserId;
|
||||
if (currentUserId) {
|
||||
const _kanbanFilters = this.handleIssuesLocalFilters.get(
|
||||
EIssuesStoreType.MODULE,
|
||||
workspaceSlug,
|
||||
moduleId,
|
||||
currentUserId
|
||||
);
|
||||
kanbanFilters.group_by = _kanbanFilters?.kanban_filters?.group_by || [];
|
||||
kanbanFilters.sub_group_by = _kanbanFilters?.kanban_filters?.sub_group_by || [];
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
set(this.filters, [moduleId, "richFilters"], richFilters);
|
||||
set(this.filters, [moduleId, "displayFilters"], displayFilters);
|
||||
set(this.filters, [moduleId, "displayProperties"], displayProperties);
|
||||
set(this.filters, [moduleId, "kanbanFilters"], kanbanFilters);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* NOTE: This method is designed as a fallback function for the work item filter store.
|
||||
* Only use this method directly when initializing filter instances.
|
||||
* For regular filter updates, use this method as a fallback function for the work item filter store methods instead.
|
||||
*/
|
||||
updateFilterExpression: IModuleIssuesFilter["updateFilterExpression"] = async (
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
moduleId,
|
||||
filters
|
||||
) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
set(this.filters, [moduleId, "richFilters"], filters);
|
||||
});
|
||||
|
||||
this.rootIssueStore.moduleIssues.fetchIssuesWithExistingPagination(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
"mutation",
|
||||
moduleId
|
||||
);
|
||||
await this.issueFilterService.patchModuleIssueFilters(workspaceSlug, projectId, moduleId, {
|
||||
rich_filters: filters,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("error while updating rich filters", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
updateFilters: IModuleIssuesFilter["updateFilters"] = async (workspaceSlug, projectId, type, filters, moduleId) => {
|
||||
try {
|
||||
if (isEmpty(this.filters) || isEmpty(this.filters[moduleId])) return;
|
||||
|
||||
const _filters = {
|
||||
richFilters: this.filters[moduleId].richFilters as TWorkItemFilterExpression,
|
||||
displayFilters: this.filters[moduleId].displayFilters as IIssueDisplayFilterOptions,
|
||||
displayProperties: this.filters[moduleId].displayProperties as IIssueDisplayProperties,
|
||||
kanbanFilters: this.filters[moduleId].kanbanFilters as TIssueKanbanFilters,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
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 state 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 = "state";
|
||||
updatedDisplayFilters.group_by = "state";
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
Object.keys(updatedDisplayFilters).forEach((_key) => {
|
||||
set(
|
||||
this.filters,
|
||||
[moduleId, "displayFilters", _key],
|
||||
updatedDisplayFilters[_key as keyof IIssueDisplayFilterOptions]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
if (this.getShouldClearIssues(updatedDisplayFilters)) {
|
||||
this.rootIssueStore.moduleIssues.clear(true, true); // clear issues for local store when some filters like layout changes
|
||||
}
|
||||
|
||||
if (this.getShouldReFetchIssues(updatedDisplayFilters)) {
|
||||
this.rootIssueStore.moduleIssues.fetchIssuesWithExistingPagination(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
"mutation",
|
||||
moduleId
|
||||
);
|
||||
}
|
||||
|
||||
await this.issueFilterService.patchModuleIssueFilters(workspaceSlug, projectId, moduleId, {
|
||||
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,
|
||||
[moduleId, "displayProperties", _key],
|
||||
updatedDisplayProperties[_key as keyof IIssueDisplayProperties]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
await this.issueFilterService.patchModuleIssueFilters(workspaceSlug, projectId, moduleId, {
|
||||
display_properties: _filters.displayProperties,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case EIssueFilterType.KANBAN_FILTERS: {
|
||||
const updatedKanbanFilters = filters as TIssueKanbanFilters;
|
||||
_filters.kanbanFilters = { ..._filters.kanbanFilters, ...updatedKanbanFilters };
|
||||
|
||||
const currentUserId = this.rootIssueStore.currentUserId;
|
||||
if (currentUserId)
|
||||
this.handleIssuesLocalFilters.set(EIssuesStoreType.MODULE, type, workspaceSlug, moduleId, currentUserId, {
|
||||
kanban_filters: _filters.kanbanFilters,
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
Object.keys(updatedKanbanFilters).forEach((_key) => {
|
||||
set(
|
||||
this.filters,
|
||||
[moduleId, "kanbanFilters", _key],
|
||||
updatedKanbanFilters[_key as keyof TIssueKanbanFilters]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
if (moduleId) this.fetchFilters(workspaceSlug, projectId, moduleId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
2
apps/web/core/store/issue/module/index.ts
Normal file
2
apps/web/core/store/issue/module/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./filter.store";
|
||||
export * from "./issue.store";
|
||||
284
apps/web/core/store/issue/module/issue.store.ts
Normal file
284
apps/web/core/store/issue/module/issue.store.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import { action, makeObservable, runInAction } from "mobx";
|
||||
// base class
|
||||
import type {
|
||||
TIssue,
|
||||
TLoader,
|
||||
ViewFlags,
|
||||
IssuePaginationOptions,
|
||||
TIssuesResponse,
|
||||
TBulkOperationsPayload,
|
||||
} from "@plane/types";
|
||||
// helpers
|
||||
import { getDistributionPathsPostUpdate } from "@plane/utils";
|
||||
import type { IBaseIssuesStore } from "../helpers/base-issues.store";
|
||||
import { BaseIssuesStore } from "../helpers/base-issues.store";
|
||||
//
|
||||
import type { IIssueRootStore } from "../root.store";
|
||||
import type { IModuleIssuesFilter } from "./filter.store";
|
||||
|
||||
export interface IModuleIssues extends IBaseIssuesStore {
|
||||
viewFlags: ViewFlags;
|
||||
// actions
|
||||
getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined;
|
||||
fetchIssues: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
loadType: TLoader,
|
||||
options: IssuePaginationOptions,
|
||||
moduleId: string
|
||||
) => Promise<TIssuesResponse | undefined>;
|
||||
fetchIssuesWithExistingPagination: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
loadType: TLoader,
|
||||
moduleId: string
|
||||
) => Promise<TIssuesResponse | undefined>;
|
||||
fetchNextIssues: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
moduleId: string,
|
||||
groupId?: string,
|
||||
subGroupId?: string
|
||||
) => Promise<TIssuesResponse | undefined>;
|
||||
|
||||
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>, moduleId: string) => Promise<TIssue>;
|
||||
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||
archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
quickAddIssue: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: TIssue,
|
||||
moduleId: string
|
||||
) => Promise<TIssue | undefined>;
|
||||
removeBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise<void>;
|
||||
archiveBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise<void>;
|
||||
bulkUpdateProperties: (workspaceSlug: string, projectId: string, data: TBulkOperationsPayload) => Promise<void>;
|
||||
}
|
||||
|
||||
export class ModuleIssues extends BaseIssuesStore implements IModuleIssues {
|
||||
viewFlags = {
|
||||
enableQuickAdd: true,
|
||||
enableIssueCreation: true,
|
||||
enableInlineEditing: true,
|
||||
};
|
||||
// filter store
|
||||
issueFilterStore: IModuleIssuesFilter;
|
||||
|
||||
constructor(_rootStore: IIssueRootStore, issueFilterStore: IModuleIssuesFilter) {
|
||||
super(_rootStore, issueFilterStore);
|
||||
|
||||
makeObservable(this, {
|
||||
// action
|
||||
fetchIssues: action,
|
||||
fetchNextIssues: action,
|
||||
fetchIssuesWithExistingPagination: action,
|
||||
|
||||
quickAddIssue: action,
|
||||
});
|
||||
// filter store
|
||||
this.issueFilterStore = issueFilterStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the module details
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param id is the module Id
|
||||
*/
|
||||
fetchParentStats = (workspaceSlug: string, projectId?: string | undefined, id?: string | undefined) => {
|
||||
const moduleId = id ?? this.moduleId;
|
||||
projectId &&
|
||||
moduleId &&
|
||||
this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update Parent stats before fetching from server
|
||||
* @param prevIssueState
|
||||
* @param nextIssueState
|
||||
* @param id
|
||||
*/
|
||||
updateParentStats = (prevIssueState?: TIssue, nextIssueState?: TIssue, id?: string | undefined) => {
|
||||
try {
|
||||
// get distribution updates
|
||||
const distributionUpdates = getDistributionPathsPostUpdate(
|
||||
prevIssueState,
|
||||
nextIssueState,
|
||||
this.rootIssueStore.rootStore.state.stateMap,
|
||||
this.rootIssueStore.rootStore.projectEstimate?.currentActiveEstimate?.estimatePointById
|
||||
);
|
||||
|
||||
const moduleId = id ?? this.moduleId;
|
||||
|
||||
moduleId && this.rootIssueStore.rootStore.module.updateModuleDistribution(distributionUpdates, moduleId);
|
||||
} catch (e) {
|
||||
console.warn("could not update module statistics");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This method is called to fetch the first issues of pagination
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param loadType
|
||||
* @param options
|
||||
* @param moduleId
|
||||
* @returns
|
||||
*/
|
||||
fetchIssues = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
loadType: TLoader,
|
||||
options: IssuePaginationOptions,
|
||||
moduleId: string,
|
||||
isExistingPaginationOptions: boolean = false
|
||||
) => {
|
||||
try {
|
||||
// set loader and clear store
|
||||
runInAction(() => {
|
||||
this.setLoader(loadType);
|
||||
this.clear(!isExistingPaginationOptions, false); // clear while fetching from server.
|
||||
if (!this.groupBy) this.clear(!isExistingPaginationOptions, true); // clear while using local to have the no load effect.
|
||||
});
|
||||
|
||||
// get params from pagination options
|
||||
const params = this.issueFilterStore?.getFilterParams(options, moduleId, undefined, undefined, undefined);
|
||||
// call the fetch issues API with the params
|
||||
const response = await this.issueService.getIssues(workspaceSlug, projectId, params, {
|
||||
signal: this.controller.signal,
|
||||
});
|
||||
|
||||
// after fetching issues, call the base method to process the response further
|
||||
this.onfetchIssues(response, options, workspaceSlug, projectId, moduleId, !isExistingPaginationOptions);
|
||||
return response;
|
||||
} catch (error) {
|
||||
// set loader to undefined once errored out
|
||||
this.setLoader(undefined);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This method is called subsequent pages of pagination
|
||||
* if groupId/subgroupId is provided, only that specific group's next page is fetched
|
||||
* else all the groups' next page is fetched
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param moduleId
|
||||
* @param groupId
|
||||
* @param subGroupId
|
||||
* @returns
|
||||
*/
|
||||
fetchNextIssues = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
moduleId: string,
|
||||
groupId?: string,
|
||||
subGroupId?: string
|
||||
) => {
|
||||
const cursorObject = this.getPaginationData(groupId, subGroupId);
|
||||
// if there are no pagination options and the next page results do not exist the return
|
||||
if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return;
|
||||
try {
|
||||
// set Loader
|
||||
this.setLoader("pagination", groupId, subGroupId);
|
||||
|
||||
// get params from stored pagination options
|
||||
const params = this.issueFilterStore?.getFilterParams(
|
||||
this.paginationOptions,
|
||||
moduleId,
|
||||
this.getNextCursor(groupId, subGroupId),
|
||||
groupId,
|
||||
subGroupId
|
||||
);
|
||||
// call the fetch issues API with the params for next page in issues
|
||||
const response = await this.issueService.getIssues(workspaceSlug, projectId, params);
|
||||
|
||||
// after the next page of issues are fetched, call the base method to process the response
|
||||
this.onfetchNexIssues(response, groupId, subGroupId);
|
||||
return response;
|
||||
} catch (error) {
|
||||
// set Loader as undefined if errored out
|
||||
this.setLoader(undefined, groupId, subGroupId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This Method exists to fetch the first page of the issues with the existing stored pagination
|
||||
* This is useful for refetching when filters, groupBy, orderBy etc changes
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param loadType
|
||||
* @param moduleId
|
||||
* @returns
|
||||
*/
|
||||
fetchIssuesWithExistingPagination = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
loadType: TLoader,
|
||||
moduleId: string
|
||||
) => {
|
||||
if (!this.paginationOptions) return;
|
||||
return await this.fetchIssues(workspaceSlug, projectId, loadType, this.paginationOptions, moduleId, true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Override inherited create issue, to also add issue to module
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param data
|
||||
* @param moduleId
|
||||
* @returns
|
||||
*/
|
||||
override createIssue = async (workspaceSlug: string, projectId: string, data: Partial<TIssue>, moduleId: string) => {
|
||||
try {
|
||||
const response = await super.createIssue(workspaceSlug, projectId, data, moduleId, false);
|
||||
const moduleIds = data.module_ids && data.module_ids.length > 1 ? data.module_ids : [moduleId];
|
||||
await this.addModulesToIssue(workspaceSlug, projectId, response.id, moduleIds);
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This Method overrides the base quickAdd issue
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param data
|
||||
* @param moduleId
|
||||
* @returns
|
||||
*/
|
||||
quickAddIssue = async (workspaceSlug: string, projectId: string, data: TIssue, moduleId: string) => {
|
||||
try {
|
||||
// add temporary issue to store list
|
||||
this.addIssue(data);
|
||||
|
||||
// call overridden create issue
|
||||
const response = await this.createIssue(workspaceSlug, projectId, data, moduleId);
|
||||
|
||||
// remove temp Issue from store list
|
||||
runInAction(() => {
|
||||
this.removeIssueFromList(data.id);
|
||||
this.rootIssueStore.issues.removeIssue(data.id);
|
||||
});
|
||||
|
||||
const currentCycleId = data.cycle_id !== "" && data.cycle_id === "None" ? undefined : data.cycle_id;
|
||||
|
||||
if (currentCycleId) {
|
||||
await this.addCycleToIssue(workspaceSlug, projectId, currentCycleId, response.id);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Using aliased names as they cannot be overridden in other stores
|
||||
archiveBulkIssues = this.bulkArchiveIssues;
|
||||
updateIssue = this.issueUpdate;
|
||||
archiveIssue = this.issueArchive;
|
||||
}
|
||||
284
apps/web/core/store/issue/profile/filter.store.ts
Normal file
284
apps/web/core/store/issue/profile/filter.store.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import { isEmpty, set } from "lodash-es";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
// base class
|
||||
import { computedFn } from "mobx-utils";
|
||||
import type { TSupportedFilterTypeForUpdate } from "@plane/constants";
|
||||
import { EIssueFilterType } from "@plane/constants";
|
||||
import type {
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
TIssueKanbanFilters,
|
||||
IIssueFilters,
|
||||
TIssueParams,
|
||||
IssuePaginationOptions,
|
||||
TWorkItemFilterExpression,
|
||||
TSupportedFilterForUpdate,
|
||||
} from "@plane/types";
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
import { handleIssueQueryParamsByLayout } from "@plane/utils";
|
||||
import { IssueFiltersService } from "@/services/issue_filter.service";
|
||||
import type { IBaseIssueFilterStore } from "../helpers/issue-filter-helper.store";
|
||||
import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store";
|
||||
// helpers
|
||||
// types
|
||||
import type { IIssueRootStore } from "../root.store";
|
||||
// constants
|
||||
// services
|
||||
|
||||
export interface IProfileIssuesFilter extends IBaseIssueFilterStore {
|
||||
// observables
|
||||
userId: 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, userId: string) => Promise<void>;
|
||||
updateFilterExpression: (workspaceSlug: string, userId: string, filters: TWorkItemFilterExpression) => Promise<void>;
|
||||
updateFilters: (
|
||||
workspaceSlug: string,
|
||||
projectId: string | undefined,
|
||||
filterType: TSupportedFilterTypeForUpdate,
|
||||
filters: TSupportedFilterForUpdate,
|
||||
userId: string
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export class ProfileIssuesFilter extends IssueFilterHelperStore implements IProfileIssuesFilter {
|
||||
// observables
|
||||
userId: string = "";
|
||||
filters: { [userId: string]: IIssueFilters } = {};
|
||||
// root store
|
||||
rootIssueStore: IIssueRootStore;
|
||||
// services
|
||||
issueFilterService;
|
||||
|
||||
constructor(_rootStore: IIssueRootStore) {
|
||||
super();
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
userId: 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 userId = this.rootIssueStore.userId;
|
||||
if (!userId) return undefined;
|
||||
|
||||
return this.getIssueFilters(userId);
|
||||
}
|
||||
|
||||
get appliedFilters() {
|
||||
const userId = this.rootIssueStore.userId;
|
||||
if (!userId) return undefined;
|
||||
|
||||
return this.getAppliedFilters(userId);
|
||||
}
|
||||
|
||||
getIssueFilters(userId: string) {
|
||||
const displayFilters = this.filters[userId] || undefined;
|
||||
if (isEmpty(displayFilters)) return undefined;
|
||||
|
||||
const _filters: IIssueFilters = this.computedIssueFilters(displayFilters);
|
||||
|
||||
return _filters;
|
||||
}
|
||||
|
||||
getAppliedFilters(userId: string) {
|
||||
const userFilters = this.getIssueFilters(userId);
|
||||
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?.richFilters,
|
||||
userFilters?.displayFilters,
|
||||
filteredParams
|
||||
);
|
||||
|
||||
return filteredRouteParams;
|
||||
}
|
||||
|
||||
getFilterParams = computedFn(
|
||||
(
|
||||
options: IssuePaginationOptions,
|
||||
userId: string,
|
||||
cursor: string | undefined,
|
||||
groupId: string | undefined,
|
||||
subGroupId: string | undefined
|
||||
) => {
|
||||
const filterParams = this.getAppliedFilters(userId);
|
||||
|
||||
const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId);
|
||||
return paginationParams;
|
||||
}
|
||||
);
|
||||
|
||||
fetchFilters = async (workspaceSlug: string, userId: string) => {
|
||||
this.userId = userId;
|
||||
const _filters = this.handleIssuesLocalFilters.get(EIssuesStoreType.PROFILE, workspaceSlug, userId, undefined);
|
||||
|
||||
const richFilters: TWorkItemFilterExpression = _filters?.rich_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, [userId, "richFilters"], richFilters);
|
||||
set(this.filters, [userId, "displayFilters"], displayFilters);
|
||||
set(this.filters, [userId, "displayProperties"], displayProperties);
|
||||
set(this.filters, [userId, "kanbanFilters"], kanbanFilters);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* NOTE: This method is designed as a fallback function for the work item filter store.
|
||||
* Only use this method directly when initializing filter instances.
|
||||
* For regular filter updates, use this method as a fallback function for the work item filter store methods instead.
|
||||
*/
|
||||
updateFilterExpression: IProfileIssuesFilter["updateFilterExpression"] = async (workspaceSlug, userId, filters) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
set(this.filters, [userId, "richFilters"], filters);
|
||||
});
|
||||
|
||||
this.rootIssueStore.profileIssues.fetchIssuesWithExistingPagination(workspaceSlug, userId, "mutation");
|
||||
this.handleIssuesLocalFilters.set(
|
||||
EIssuesStoreType.PROFILE,
|
||||
EIssueFilterType.FILTERS,
|
||||
workspaceSlug,
|
||||
userId,
|
||||
undefined,
|
||||
{
|
||||
rich_filters: filters,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("error while updating rich filters", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
updateFilters: IProfileIssuesFilter["updateFilters"] = async (workspaceSlug, _projectId, type, filters, userId) => {
|
||||
try {
|
||||
if (isEmpty(this.filters) || isEmpty(this.filters[userId])) return;
|
||||
|
||||
const _filters = {
|
||||
richFilters: this.filters[userId].richFilters as TWorkItemFilterExpression,
|
||||
displayFilters: this.filters[userId].displayFilters as IIssueDisplayFilterOptions,
|
||||
displayProperties: this.filters[userId].displayProperties as IIssueDisplayProperties,
|
||||
kanbanFilters: this.filters[userId].kanbanFilters as TIssueKanbanFilters,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
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,
|
||||
[userId, "displayFilters", _key],
|
||||
updatedDisplayFilters[_key as keyof IIssueDisplayFilterOptions]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
this.rootIssueStore.profileIssues.fetchIssuesWithExistingPagination(workspaceSlug, userId, "mutation");
|
||||
|
||||
this.handleIssuesLocalFilters.set(EIssuesStoreType.PROFILE, type, workspaceSlug, userId, 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,
|
||||
[userId, "displayProperties", _key],
|
||||
updatedDisplayProperties[_key as keyof IIssueDisplayProperties]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
this.handleIssuesLocalFilters.set(EIssuesStoreType.PROFILE, type, workspaceSlug, userId, undefined, {
|
||||
display_properties: _filters.displayProperties,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case EIssueFilterType.KANBAN_FILTERS: {
|
||||
const updatedKanbanFilters = filters as TIssueKanbanFilters;
|
||||
_filters.kanbanFilters = { ..._filters.kanbanFilters, ...updatedKanbanFilters };
|
||||
|
||||
const currentUserId = this.rootIssueStore.currentUserId;
|
||||
if (currentUserId)
|
||||
this.handleIssuesLocalFilters.set(EIssuesStoreType.PROFILE, type, workspaceSlug, userId, undefined, {
|
||||
kanban_filters: _filters.kanbanFilters,
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
Object.keys(updatedKanbanFilters).forEach((_key) => {
|
||||
set(
|
||||
this.filters,
|
||||
[userId, "kanbanFilters", _key],
|
||||
updatedKanbanFilters[_key as keyof TIssueKanbanFilters]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
if (userId) this.fetchFilters(workspaceSlug, userId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
2
apps/web/core/store/issue/profile/index.ts
Normal file
2
apps/web/core/store/issue/profile/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./filter.store";
|
||||
export * from "./issue.store";
|
||||
231
apps/web/core/store/issue/profile/issue.store.ts
Normal file
231
apps/web/core/store/issue/profile/issue.store.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import { action, observable, makeObservable, computed, runInAction } from "mobx";
|
||||
// base class
|
||||
import type {
|
||||
TIssue,
|
||||
TLoader,
|
||||
IssuePaginationOptions,
|
||||
TIssuesResponse,
|
||||
ViewFlags,
|
||||
TBulkOperationsPayload,
|
||||
TProfileViews,
|
||||
} from "@plane/types";
|
||||
import { UserService } from "@/services/user.service";
|
||||
|
||||
// services
|
||||
// types
|
||||
import type { IBaseIssuesStore } from "../helpers/base-issues.store";
|
||||
import { BaseIssuesStore } from "../helpers/base-issues.store";
|
||||
import type { IIssueRootStore } from "../root.store";
|
||||
import type { IProfileIssuesFilter } from "./filter.store";
|
||||
|
||||
export interface IProfileIssues extends IBaseIssuesStore {
|
||||
// observable
|
||||
currentView: TProfileViews;
|
||||
viewFlags: ViewFlags;
|
||||
// actions
|
||||
setViewId: (viewId: TProfileViews) => void;
|
||||
// action
|
||||
fetchIssues: (
|
||||
workspaceSlug: string,
|
||||
userId: string,
|
||||
loadType: TLoader,
|
||||
option: IssuePaginationOptions,
|
||||
view: TProfileViews,
|
||||
isExistingPaginationOptions?: boolean
|
||||
) => Promise<TIssuesResponse | undefined>;
|
||||
fetchIssuesWithExistingPagination: (
|
||||
workspaceSlug: string,
|
||||
userId: string,
|
||||
loadType: TLoader
|
||||
) => Promise<TIssuesResponse | undefined>;
|
||||
fetchNextIssues: (
|
||||
workspaceSlug: string,
|
||||
userId: string,
|
||||
groupId?: string,
|
||||
subGroupId?: string
|
||||
) => Promise<TIssuesResponse | undefined>;
|
||||
|
||||
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>;
|
||||
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||
archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
removeBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise<void>;
|
||||
archiveBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise<void>;
|
||||
bulkUpdateProperties: (workspaceSlug: string, projectId: string, data: TBulkOperationsPayload) => Promise<void>;
|
||||
|
||||
quickAddIssue: undefined;
|
||||
}
|
||||
|
||||
export class ProfileIssues extends BaseIssuesStore implements IProfileIssues {
|
||||
currentView: TProfileViews = "assigned";
|
||||
// filter store
|
||||
issueFilterStore: IProfileIssuesFilter;
|
||||
// services
|
||||
userService;
|
||||
|
||||
constructor(_rootStore: IIssueRootStore, issueFilterStore: IProfileIssuesFilter) {
|
||||
super(_rootStore, issueFilterStore);
|
||||
makeObservable(this, {
|
||||
// observable
|
||||
currentView: observable.ref,
|
||||
// computed
|
||||
viewFlags: computed,
|
||||
// action
|
||||
setViewId: action.bound,
|
||||
fetchIssues: action,
|
||||
fetchNextIssues: action,
|
||||
fetchIssuesWithExistingPagination: action,
|
||||
});
|
||||
// filter store
|
||||
this.issueFilterStore = issueFilterStore;
|
||||
// services
|
||||
this.userService = new UserService();
|
||||
}
|
||||
|
||||
get viewFlags() {
|
||||
if (this.currentView === "subscribed")
|
||||
return {
|
||||
enableQuickAdd: false,
|
||||
enableIssueCreation: false,
|
||||
enableInlineEditing: true,
|
||||
};
|
||||
return {
|
||||
enableQuickAdd: false,
|
||||
enableIssueCreation: true,
|
||||
enableInlineEditing: true,
|
||||
};
|
||||
}
|
||||
|
||||
setViewId(viewId: TProfileViews) {
|
||||
this.currentView = viewId;
|
||||
}
|
||||
|
||||
fetchParentStats = () => {};
|
||||
|
||||
/** */
|
||||
updateParentStats = () => {};
|
||||
|
||||
/**
|
||||
* This method is called to fetch the first issues of pagination
|
||||
* @param workspaceSlug
|
||||
* @param userId
|
||||
* @param loadType
|
||||
* @param options
|
||||
* @param view
|
||||
* @returns
|
||||
*/
|
||||
fetchIssues: IProfileIssues["fetchIssues"] = async (
|
||||
workspaceSlug: string,
|
||||
userId: string,
|
||||
loadType: TLoader,
|
||||
options: IssuePaginationOptions,
|
||||
view: TProfileViews,
|
||||
isExistingPaginationOptions: boolean = false
|
||||
) => {
|
||||
try {
|
||||
// set loader and clear store
|
||||
runInAction(() => {
|
||||
this.setLoader(loadType);
|
||||
});
|
||||
this.clear(!isExistingPaginationOptions);
|
||||
|
||||
// set ViewId
|
||||
this.setViewId(view);
|
||||
|
||||
// get params from pagination options
|
||||
let params = this.issueFilterStore?.getFilterParams(options, userId, undefined, undefined, undefined);
|
||||
params = {
|
||||
...params,
|
||||
assignees: undefined,
|
||||
created_by: undefined,
|
||||
subscriber: undefined,
|
||||
};
|
||||
// modify params based on view
|
||||
if (this.currentView === "assigned") params = { ...params, assignees: userId };
|
||||
else if (this.currentView === "created") params = { ...params, created_by: userId };
|
||||
else if (this.currentView === "subscribed") params = { ...params, subscriber: userId };
|
||||
|
||||
// call the fetch issues API with the params
|
||||
const response = await this.userService.getUserProfileIssues(workspaceSlug, userId, params, {
|
||||
signal: this.controller.signal,
|
||||
});
|
||||
|
||||
// after fetching issues, call the base method to process the response further
|
||||
this.onfetchIssues(response, options, workspaceSlug, undefined, undefined, !isExistingPaginationOptions);
|
||||
return response;
|
||||
} catch (error) {
|
||||
// set loader to undefined if errored out
|
||||
this.setLoader(undefined);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This method is called subsequent pages of pagination
|
||||
* if groupId/subgroupId is provided, only that specific group's next page is fetched
|
||||
* else all the groups' next page is fetched
|
||||
* @param workspaceSlug
|
||||
* @param userId
|
||||
* @param groupId
|
||||
* @param subGroupId
|
||||
* @returns
|
||||
*/
|
||||
fetchNextIssues = async (workspaceSlug: string, userId: string, groupId?: string, subGroupId?: string) => {
|
||||
const cursorObject = this.getPaginationData(groupId, subGroupId);
|
||||
// if there are no pagination options and the next page results do not exist the return
|
||||
if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return;
|
||||
try {
|
||||
// set Loader
|
||||
this.setLoader("pagination", groupId, subGroupId);
|
||||
|
||||
// get params from stored pagination options
|
||||
let params = this.issueFilterStore?.getFilterParams(
|
||||
this.paginationOptions,
|
||||
userId,
|
||||
this.getNextCursor(groupId, subGroupId),
|
||||
groupId,
|
||||
subGroupId
|
||||
);
|
||||
params = {
|
||||
...params,
|
||||
assignees: undefined,
|
||||
created_by: undefined,
|
||||
subscriber: undefined,
|
||||
};
|
||||
if (this.currentView === "assigned") params = { ...params, assignees: userId };
|
||||
else if (this.currentView === "created") params = { ...params, created_by: userId };
|
||||
else if (this.currentView === "subscribed") params = { ...params, subscriber: userId };
|
||||
|
||||
// call the fetch issues API with the params for next page in issues
|
||||
const response = await this.userService.getUserProfileIssues(workspaceSlug, userId, params);
|
||||
|
||||
// after the next page of issues are fetched, call the base method to process the response
|
||||
this.onfetchNexIssues(response, groupId, subGroupId);
|
||||
return response;
|
||||
} catch (error) {
|
||||
// set Loader as undefined if errored out
|
||||
this.setLoader(undefined, groupId, subGroupId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This Method exists to fetch the first page of the issues with the existing stored pagination
|
||||
* This is useful for refetching when filters, groupBy, orderBy etc changes
|
||||
* @param workspaceSlug
|
||||
* @param userId
|
||||
* @param loadType
|
||||
* @returns
|
||||
*/
|
||||
fetchIssuesWithExistingPagination = async (workspaceSlug: string, userId: string, loadType: TLoader) => {
|
||||
if (!this.paginationOptions || !this.currentView) return;
|
||||
return await this.fetchIssues(workspaceSlug, userId, loadType, this.paginationOptions, this.currentView, true);
|
||||
};
|
||||
|
||||
// Using aliased names as they cannot be overridden in other stores
|
||||
archiveBulkIssues = this.bulkArchiveIssues;
|
||||
updateIssue = this.issueUpdate;
|
||||
archiveIssue = this.issueArchive;
|
||||
|
||||
// Setting them as undefined as they can not performed on profile issues
|
||||
quickAddIssue = undefined;
|
||||
}
|
||||
338
apps/web/core/store/issue/project-views/filter.store.ts
Normal file
338
apps/web/core/store/issue/project-views/filter.store.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
import { isEmpty, set } from "lodash-es";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
// base class
|
||||
import { computedFn } from "mobx-utils";
|
||||
import type { TSupportedFilterTypeForUpdate } from "@plane/constants";
|
||||
import { EIssueFilterType } from "@plane/constants";
|
||||
import type {
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
TIssueKanbanFilters,
|
||||
IIssueFilters,
|
||||
TIssueParams,
|
||||
IssuePaginationOptions,
|
||||
IProjectView,
|
||||
TWorkItemFilterExpression,
|
||||
TSupportedFilterForUpdate,
|
||||
} from "@plane/types";
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
import { handleIssueQueryParamsByLayout } from "@plane/utils";
|
||||
// services
|
||||
import { ViewService } from "@/plane-web/services";
|
||||
import type { IBaseIssueFilterStore } from "../helpers/issue-filter-helper.store";
|
||||
import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store";
|
||||
// helpers
|
||||
// types
|
||||
import type { IIssueRootStore } from "../root.store";
|
||||
// constants
|
||||
|
||||
export interface IProjectViewIssuesFilter extends IBaseIssueFilterStore {
|
||||
//helper actions
|
||||
getFilterParams: (
|
||||
options: IssuePaginationOptions,
|
||||
viewId: string,
|
||||
cursor: string | undefined,
|
||||
groupId: string | undefined,
|
||||
subGroupId: string | undefined
|
||||
) => Partial<Record<TIssueParams, string | boolean>>;
|
||||
getIssueFilters(viewId: string): IIssueFilters | undefined;
|
||||
// helper actions
|
||||
mutateFilters: (workspaceSlug: string, viewId: string, viewDetails: IProjectView) => void;
|
||||
// action
|
||||
fetchFilters: (workspaceSlug: string, projectId: string, viewId: string) => Promise<void>;
|
||||
updateFilterExpression: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
viewId: string,
|
||||
filters: TWorkItemFilterExpression
|
||||
) => Promise<void>;
|
||||
updateFilters: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
filterType: TSupportedFilterTypeForUpdate,
|
||||
filters: TSupportedFilterForUpdate,
|
||||
viewId: string
|
||||
) => Promise<void>;
|
||||
resetFilters: (workspaceSlug: string, viewId: string) => void;
|
||||
}
|
||||
|
||||
export class ProjectViewIssuesFilter extends IssueFilterHelperStore implements IProjectViewIssuesFilter {
|
||||
// observables
|
||||
filters: { [viewId: string]: IIssueFilters } = {};
|
||||
// root store
|
||||
rootIssueStore;
|
||||
// services
|
||||
issueFilterService;
|
||||
|
||||
constructor(_rootStore: IIssueRootStore) {
|
||||
super();
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
filters: observable,
|
||||
// computed
|
||||
issueFilters: computed,
|
||||
appliedFilters: computed,
|
||||
// actions
|
||||
fetchFilters: action,
|
||||
updateFilters: action,
|
||||
resetFilters: action,
|
||||
});
|
||||
// root store
|
||||
this.rootIssueStore = _rootStore;
|
||||
// services
|
||||
this.issueFilterService = new ViewService();
|
||||
}
|
||||
|
||||
get issueFilters() {
|
||||
const viewId = this.rootIssueStore.viewId;
|
||||
if (!viewId) return undefined;
|
||||
|
||||
return this.getIssueFilters(viewId);
|
||||
}
|
||||
|
||||
get appliedFilters() {
|
||||
const viewId = this.rootIssueStore.viewId;
|
||||
if (!viewId) return undefined;
|
||||
|
||||
return this.getAppliedFilters(viewId);
|
||||
}
|
||||
|
||||
getIssueFilters(viewId: string) {
|
||||
const displayFilters = this.filters[viewId] || undefined;
|
||||
if (isEmpty(displayFilters)) return undefined;
|
||||
|
||||
const _filters: IIssueFilters = this.computedIssueFilters(displayFilters);
|
||||
|
||||
return _filters;
|
||||
}
|
||||
|
||||
getAppliedFilters(viewId: string) {
|
||||
const userFilters = this.getIssueFilters(viewId);
|
||||
if (!userFilters) return undefined;
|
||||
|
||||
const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "issues");
|
||||
if (!filteredParams) return undefined;
|
||||
|
||||
const filteredRouteParams: Partial<Record<TIssueParams, string | boolean>> = this.computedFilteredParams(
|
||||
userFilters?.richFilters,
|
||||
userFilters?.displayFilters,
|
||||
filteredParams
|
||||
);
|
||||
|
||||
return filteredRouteParams;
|
||||
}
|
||||
|
||||
getFilterParams = computedFn(
|
||||
(
|
||||
options: IssuePaginationOptions,
|
||||
viewId: string,
|
||||
cursor: string | undefined,
|
||||
groupId: string | undefined,
|
||||
subGroupId: string | undefined
|
||||
) => {
|
||||
const filterParams = this.getAppliedFilters(viewId);
|
||||
|
||||
const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId);
|
||||
return paginationParams;
|
||||
}
|
||||
);
|
||||
|
||||
mutateFilters: IProjectViewIssuesFilter["mutateFilters"] = action((workspaceSlug, viewId, viewDetails) => {
|
||||
const richFilters: TWorkItemFilterExpression = viewDetails?.rich_filters;
|
||||
const displayFilters: IIssueDisplayFilterOptions = this.computedDisplayFilters(viewDetails?.display_filters);
|
||||
const displayProperties: IIssueDisplayProperties = this.computedDisplayProperties(viewDetails?.display_properties);
|
||||
|
||||
// fetching the kanban toggle helpers in the local storage
|
||||
const kanbanFilters = {
|
||||
group_by: [],
|
||||
sub_group_by: [],
|
||||
};
|
||||
const currentUserId = this.rootIssueStore.currentUserId;
|
||||
if (currentUserId) {
|
||||
const _kanbanFilters = this.handleIssuesLocalFilters.get(
|
||||
EIssuesStoreType.PROJECT_VIEW,
|
||||
workspaceSlug,
|
||||
viewId,
|
||||
currentUserId
|
||||
);
|
||||
kanbanFilters.group_by = _kanbanFilters?.kanban_filters?.group_by || [];
|
||||
kanbanFilters.sub_group_by = _kanbanFilters?.kanban_filters?.sub_group_by || [];
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
set(this.filters, [viewId, "richFilters"], richFilters);
|
||||
set(this.filters, [viewId, "displayFilters"], displayFilters);
|
||||
set(this.filters, [viewId, "displayProperties"], displayProperties);
|
||||
set(this.filters, [viewId, "kanbanFilters"], kanbanFilters);
|
||||
});
|
||||
});
|
||||
|
||||
fetchFilters = async (workspaceSlug: string, projectId: string, viewId: string) => {
|
||||
try {
|
||||
const viewDetails = await this.issueFilterService.getViewDetails(workspaceSlug, projectId, viewId);
|
||||
this.mutateFilters(workspaceSlug, viewId, viewDetails);
|
||||
} catch (error) {
|
||||
console.log("error while fetching project view filters", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* NOTE: This method is designed as a fallback function for the work item filter store.
|
||||
* Only use this method directly when initializing filter instances.
|
||||
* For regular filter updates, use this method as a fallback function for the work item filter store methods instead.
|
||||
*/
|
||||
updateFilterExpression: IProjectViewIssuesFilter["updateFilterExpression"] = async (
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
viewId,
|
||||
filters
|
||||
) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
set(this.filters, [viewId, "richFilters"], filters);
|
||||
});
|
||||
|
||||
this.rootIssueStore.projectViewIssues.fetchIssuesWithExistingPagination(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
viewId,
|
||||
"mutation"
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("error while updating rich filters", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
updateFilters: IProjectViewIssuesFilter["updateFilters"] = async (
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
type,
|
||||
filters,
|
||||
viewId
|
||||
) => {
|
||||
try {
|
||||
if (isEmpty(this.filters) || isEmpty(this.filters[viewId])) return;
|
||||
|
||||
const _filters = {
|
||||
richFilters: this.filters[viewId].richFilters as TWorkItemFilterExpression,
|
||||
displayFilters: this.filters[viewId].displayFilters as IIssueDisplayFilterOptions,
|
||||
displayProperties: this.filters[viewId].displayProperties as IIssueDisplayProperties,
|
||||
kanbanFilters: this.filters[viewId].kanbanFilters as TIssueKanbanFilters,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
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 state 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 = "state";
|
||||
updatedDisplayFilters.group_by = "state";
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
Object.keys(updatedDisplayFilters).forEach((_key) => {
|
||||
set(
|
||||
this.filters,
|
||||
[viewId, "displayFilters", _key],
|
||||
updatedDisplayFilters[_key as keyof IIssueDisplayFilterOptions]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
if (this.getShouldClearIssues(updatedDisplayFilters)) {
|
||||
this.rootIssueStore.projectIssues.clear(true, true); // clear issues for local store when some filters like layout changes
|
||||
}
|
||||
|
||||
if (this.getShouldReFetchIssues(updatedDisplayFilters)) {
|
||||
this.rootIssueStore.projectViewIssues.fetchIssuesWithExistingPagination(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
viewId,
|
||||
"mutation"
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case EIssueFilterType.DISPLAY_PROPERTIES: {
|
||||
const updatedDisplayProperties = filters as IIssueDisplayProperties;
|
||||
_filters.displayProperties = { ..._filters.displayProperties, ...updatedDisplayProperties };
|
||||
|
||||
runInAction(() => {
|
||||
Object.keys(updatedDisplayProperties).forEach((_key) => {
|
||||
set(
|
||||
this.filters,
|
||||
[viewId, "displayProperties", _key],
|
||||
updatedDisplayProperties[_key as keyof IIssueDisplayProperties]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case EIssueFilterType.KANBAN_FILTERS: {
|
||||
const updatedKanbanFilters = filters as TIssueKanbanFilters;
|
||||
_filters.kanbanFilters = { ..._filters.kanbanFilters, ...updatedKanbanFilters };
|
||||
|
||||
const currentUserId = this.rootIssueStore.currentUserId;
|
||||
if (currentUserId)
|
||||
this.handleIssuesLocalFilters.set(
|
||||
EIssuesStoreType.PROJECT_VIEW,
|
||||
type,
|
||||
workspaceSlug,
|
||||
viewId,
|
||||
currentUserId,
|
||||
{
|
||||
kanban_filters: _filters.kanbanFilters,
|
||||
}
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
Object.keys(updatedKanbanFilters).forEach((_key) => {
|
||||
set(
|
||||
this.filters,
|
||||
[viewId, "kanbanFilters", _key],
|
||||
updatedKanbanFilters[_key as keyof TIssueKanbanFilters]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
if (viewId) this.fetchFilters(workspaceSlug, projectId, viewId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description resets the filters for a project view
|
||||
* @param workspaceSlug
|
||||
* @param viewId
|
||||
*/
|
||||
resetFilters: IProjectViewIssuesFilter["resetFilters"] = action((workspaceSlug, viewId) => {
|
||||
const viewDetails = this.rootIssueStore.rootStore.projectView.getViewById(viewId);
|
||||
if (!viewDetails) return;
|
||||
this.mutateFilters(workspaceSlug, viewId, viewDetails);
|
||||
});
|
||||
}
|
||||
2
apps/web/core/store/issue/project-views/index.ts
Normal file
2
apps/web/core/store/issue/project-views/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./filter.store";
|
||||
export * from "./issue.store";
|
||||
186
apps/web/core/store/issue/project-views/issue.store.ts
Normal file
186
apps/web/core/store/issue/project-views/issue.store.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { action, makeObservable, runInAction } from "mobx";
|
||||
// base class
|
||||
import type {
|
||||
TIssue,
|
||||
TLoader,
|
||||
ViewFlags,
|
||||
IssuePaginationOptions,
|
||||
TIssuesResponse,
|
||||
TBulkOperationsPayload,
|
||||
} from "@plane/types";
|
||||
// services
|
||||
// types
|
||||
import type { IBaseIssuesStore } from "../helpers/base-issues.store";
|
||||
import { BaseIssuesStore } from "../helpers/base-issues.store";
|
||||
import type { IIssueRootStore } from "../root.store";
|
||||
import type { IProjectViewIssuesFilter } from "./filter.store";
|
||||
|
||||
export interface IProjectViewIssues extends IBaseIssuesStore {
|
||||
viewFlags: ViewFlags;
|
||||
// actions
|
||||
fetchIssues: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
viewId: string,
|
||||
loadType: TLoader,
|
||||
options: IssuePaginationOptions
|
||||
) => Promise<TIssuesResponse | undefined>;
|
||||
fetchIssuesWithExistingPagination: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
viewId: string,
|
||||
loadType: TLoader
|
||||
) => Promise<TIssuesResponse | undefined>;
|
||||
fetchNextIssues: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
viewId: string,
|
||||
groupId?: string,
|
||||
subGroupId?: string
|
||||
) => Promise<TIssuesResponse | undefined>;
|
||||
|
||||
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>;
|
||||
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||
archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
quickAddIssue: (workspaceSlug: string, projectId: string, data: TIssue) => Promise<TIssue | undefined>;
|
||||
removeBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise<void>;
|
||||
archiveBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise<void>;
|
||||
bulkUpdateProperties: (workspaceSlug: string, projectId: string, data: TBulkOperationsPayload) => Promise<void>;
|
||||
}
|
||||
|
||||
export class ProjectViewIssues extends BaseIssuesStore implements IProjectViewIssues {
|
||||
viewFlags = {
|
||||
enableQuickAdd: true,
|
||||
enableIssueCreation: true,
|
||||
enableInlineEditing: true,
|
||||
};
|
||||
//filter store
|
||||
issueFilterStore: IProjectViewIssuesFilter;
|
||||
|
||||
constructor(_rootStore: IIssueRootStore, issueFilterStore: IProjectViewIssuesFilter) {
|
||||
super(_rootStore, issueFilterStore);
|
||||
makeObservable(this, {
|
||||
// action
|
||||
fetchIssues: action,
|
||||
fetchNextIssues: action,
|
||||
fetchIssuesWithExistingPagination: action,
|
||||
});
|
||||
//filter store
|
||||
this.issueFilterStore = issueFilterStore;
|
||||
}
|
||||
|
||||
fetchParentStats = async () => {};
|
||||
|
||||
/** */
|
||||
updateParentStats = () => {};
|
||||
|
||||
/**
|
||||
* This method is called to fetch the first issues of pagination
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param loadType
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
fetchIssues = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
viewId: string,
|
||||
loadType: TLoader,
|
||||
options: IssuePaginationOptions,
|
||||
isExistingPaginationOptions: boolean = false
|
||||
) => {
|
||||
try {
|
||||
// set loader and clear store
|
||||
runInAction(() => {
|
||||
this.setLoader(loadType);
|
||||
this.clear(!isExistingPaginationOptions, false); // clear while fetching from server.
|
||||
if (!this.groupBy) this.clear(!isExistingPaginationOptions, true); // clear while using local to have the no load effect.
|
||||
});
|
||||
|
||||
// get params from pagination options
|
||||
const params = this.issueFilterStore?.getFilterParams(options, viewId, undefined, undefined, undefined);
|
||||
// call the fetch issues API with the params
|
||||
const response = await this.issueService.getIssues(workspaceSlug, projectId, params, {
|
||||
signal: this.controller.signal,
|
||||
});
|
||||
|
||||
// after fetching issues, call the base method to process the response further
|
||||
this.onfetchIssues(response, options, workspaceSlug, projectId, viewId, !isExistingPaginationOptions);
|
||||
return response;
|
||||
} catch (error) {
|
||||
// set loader to undefined if errored out
|
||||
this.setLoader(undefined);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This method is called subsequent pages of pagination
|
||||
* if groupId/subgroupId is provided, only that specific group's next page is fetched
|
||||
* else all the groups' next page is fetched
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param groupId
|
||||
* @param subGroupId
|
||||
* @returns
|
||||
*/
|
||||
fetchNextIssues = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
viewId: string,
|
||||
groupId?: string,
|
||||
subGroupId?: string
|
||||
) => {
|
||||
const cursorObject = this.getPaginationData(groupId, subGroupId);
|
||||
// if there are no pagination options and the next page results do not exist the return
|
||||
if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return;
|
||||
try {
|
||||
// set Loader
|
||||
this.setLoader("pagination", groupId, subGroupId);
|
||||
|
||||
// get params from stored pagination options
|
||||
const params = this.issueFilterStore?.getFilterParams(
|
||||
this.paginationOptions,
|
||||
viewId,
|
||||
this.getNextCursor(groupId, subGroupId),
|
||||
groupId,
|
||||
subGroupId
|
||||
);
|
||||
// call the fetch issues API with the params for next page in issues
|
||||
const response = await this.issueService.getIssues(workspaceSlug, projectId, params);
|
||||
|
||||
// after the next page of issues are fetched, call the base method to process the response
|
||||
this.onfetchNexIssues(response, groupId, subGroupId);
|
||||
return response;
|
||||
} catch (error) {
|
||||
// set Loader as undefined if errored out
|
||||
this.setLoader(undefined, groupId, subGroupId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This Method exists to fetch the first page of the issues with the existing stored pagination
|
||||
* This is useful for refetching when filters, groupBy, orderBy etc changes
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param loadType
|
||||
* @returns
|
||||
*/
|
||||
fetchIssuesWithExistingPagination = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
viewId: string,
|
||||
loadType: TLoader
|
||||
) => {
|
||||
if (!this.paginationOptions) return;
|
||||
return await this.fetchIssues(workspaceSlug, projectId, viewId, loadType, this.paginationOptions, true);
|
||||
};
|
||||
|
||||
// Using aliased names as they cannot be overridden in other stores
|
||||
archiveBulkIssues = this.bulkArchiveIssues;
|
||||
quickAddIssue = this.issueQuickAdd;
|
||||
updateIssue = this.issueUpdate;
|
||||
archiveIssue = this.issueArchive;
|
||||
}
|
||||
297
apps/web/core/store/issue/project/filter.store.ts
Normal file
297
apps/web/core/store/issue/project/filter.store.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
import { isEmpty, set } from "lodash-es";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
// base class
|
||||
import { computedFn } from "mobx-utils";
|
||||
import type { TSupportedFilterTypeForUpdate } from "@plane/constants";
|
||||
import { EIssueFilterType } from "@plane/constants";
|
||||
import type {
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
TIssueKanbanFilters,
|
||||
IIssueFilters,
|
||||
TIssueParams,
|
||||
IssuePaginationOptions,
|
||||
TWorkItemFilterExpression,
|
||||
TSupportedFilterForUpdate,
|
||||
} from "@plane/types";
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
import { handleIssueQueryParamsByLayout } from "@plane/utils";
|
||||
import { IssueFiltersService } from "@/services/issue_filter.service";
|
||||
import type { IBaseIssueFilterStore } from "../helpers/issue-filter-helper.store";
|
||||
import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store";
|
||||
// helpers
|
||||
// types
|
||||
import type { IIssueRootStore } from "../root.store";
|
||||
// constants
|
||||
// services
|
||||
|
||||
export interface IProjectIssuesFilter extends IBaseIssueFilterStore {
|
||||
//helper actions
|
||||
getFilterParams: (
|
||||
options: IssuePaginationOptions,
|
||||
projectId: string,
|
||||
cursor: string | undefined,
|
||||
groupId: string | undefined,
|
||||
subGroupId: string | undefined
|
||||
) => Partial<Record<TIssueParams, string | boolean>>;
|
||||
getIssueFilters(projectId: string): IIssueFilters | undefined;
|
||||
// action
|
||||
fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||
updateFilterExpression: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
filters: TWorkItemFilterExpression
|
||||
) => Promise<void>;
|
||||
updateFilters: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
filterType: TSupportedFilterTypeForUpdate,
|
||||
filters: TSupportedFilterForUpdate
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProjectIssuesFilter {
|
||||
// observables
|
||||
filters: { [projectId: string]: IIssueFilters } = {};
|
||||
// root store
|
||||
rootIssueStore: IIssueRootStore;
|
||||
// services
|
||||
issueFilterService;
|
||||
|
||||
constructor(_rootStore: IIssueRootStore) {
|
||||
super();
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
filters: observable,
|
||||
// computed
|
||||
issueFilters: computed,
|
||||
appliedFilters: computed,
|
||||
// actions
|
||||
fetchFilters: action,
|
||||
updateFilterExpression: action,
|
||||
updateFilters: action,
|
||||
});
|
||||
// root store
|
||||
this.rootIssueStore = _rootStore;
|
||||
// services
|
||||
this.issueFilterService = new IssueFiltersService();
|
||||
}
|
||||
|
||||
get issueFilters() {
|
||||
const projectId = this.rootIssueStore.projectId;
|
||||
if (!projectId) return undefined;
|
||||
|
||||
return this.getIssueFilters(projectId);
|
||||
}
|
||||
|
||||
get appliedFilters() {
|
||||
const projectId = this.rootIssueStore.projectId;
|
||||
if (!projectId) return undefined;
|
||||
|
||||
return this.getAppliedFilters(projectId);
|
||||
}
|
||||
|
||||
getIssueFilters(projectId: string) {
|
||||
const displayFilters = this.filters[projectId] || undefined;
|
||||
if (isEmpty(displayFilters)) return undefined;
|
||||
|
||||
return this.computedIssueFilters(displayFilters);
|
||||
}
|
||||
|
||||
getAppliedFilters(projectId: string) {
|
||||
const userFilters = this.getIssueFilters(projectId);
|
||||
if (!userFilters) return undefined;
|
||||
|
||||
const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "issues");
|
||||
if (!filteredParams) return undefined;
|
||||
|
||||
const filteredRouteParams: Partial<Record<TIssueParams, string | boolean>> = this.computedFilteredParams(
|
||||
userFilters?.richFilters,
|
||||
userFilters?.displayFilters,
|
||||
filteredParams
|
||||
);
|
||||
|
||||
return filteredRouteParams;
|
||||
}
|
||||
|
||||
getFilterParams = computedFn(
|
||||
(
|
||||
options: IssuePaginationOptions,
|
||||
projectId: string,
|
||||
cursor: string | undefined,
|
||||
groupId: string | undefined,
|
||||
subGroupId: string | undefined
|
||||
) => {
|
||||
const filterParams = this.getAppliedFilters(projectId);
|
||||
const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId);
|
||||
return paginationParams;
|
||||
}
|
||||
);
|
||||
|
||||
fetchFilters = async (workspaceSlug: string, projectId: string) => {
|
||||
const _filters = await this.issueFilterService.fetchProjectIssueFilters(workspaceSlug, projectId);
|
||||
|
||||
const richFilters = _filters?.rich_filters;
|
||||
const displayFilters = this.computedDisplayFilters(_filters?.display_filters);
|
||||
const displayProperties = this.computedDisplayProperties(_filters?.display_properties);
|
||||
|
||||
// fetching the kanban toggle helpers in the local storage
|
||||
const kanbanFilters = {
|
||||
group_by: [],
|
||||
sub_group_by: [],
|
||||
};
|
||||
const currentUserId = this.rootIssueStore.currentUserId;
|
||||
if (currentUserId) {
|
||||
const _kanbanFilters = this.handleIssuesLocalFilters.get(
|
||||
EIssuesStoreType.PROJECT,
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
currentUserId
|
||||
);
|
||||
kanbanFilters.group_by = _kanbanFilters?.kanban_filters?.group_by || [];
|
||||
kanbanFilters.sub_group_by = _kanbanFilters?.kanban_filters?.sub_group_by || [];
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
set(this.filters, [projectId, "richFilters"], richFilters);
|
||||
set(this.filters, [projectId, "displayFilters"], displayFilters);
|
||||
set(this.filters, [projectId, "displayProperties"], displayProperties);
|
||||
set(this.filters, [projectId, "kanbanFilters"], kanbanFilters);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* NOTE: This method is designed as a fallback function for the work item filter store.
|
||||
* Only use this method directly when initializing filter instances.
|
||||
* For regular filter updates, use this method as a fallback function for the work item filter store methods instead.
|
||||
*/
|
||||
updateFilterExpression: IProjectIssuesFilter["updateFilterExpression"] = async (
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
filters
|
||||
) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
set(this.filters, [projectId, "richFilters"], filters);
|
||||
});
|
||||
|
||||
this.rootIssueStore.projectIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation");
|
||||
await this.issueFilterService.patchProjectIssueFilters(workspaceSlug, projectId, {
|
||||
rich_filters: filters,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("error while updating rich filters", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
updateFilters: IProjectIssuesFilter["updateFilters"] = async (workspaceSlug, projectId, type, filters) => {
|
||||
try {
|
||||
if (isEmpty(this.filters) || isEmpty(this.filters[projectId])) return;
|
||||
|
||||
const _filters = {
|
||||
richFilters: this.filters[projectId].richFilters as TWorkItemFilterExpression,
|
||||
displayFilters: this.filters[projectId].displayFilters as IIssueDisplayFilterOptions,
|
||||
displayProperties: this.filters[projectId].displayProperties as IIssueDisplayProperties,
|
||||
kanbanFilters: this.filters[projectId].kanbanFilters as TIssueKanbanFilters,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
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 state 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 = "state";
|
||||
updatedDisplayFilters.group_by = "state";
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
Object.keys(updatedDisplayFilters).forEach((_key) => {
|
||||
set(
|
||||
this.filters,
|
||||
[projectId, "displayFilters", _key],
|
||||
updatedDisplayFilters[_key as keyof IIssueDisplayFilterOptions]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
if (this.getShouldClearIssues(updatedDisplayFilters)) {
|
||||
this.rootIssueStore.projectIssues.clear(true, true); // clear issues for local store when some filters like layout changes
|
||||
}
|
||||
|
||||
if (this.getShouldReFetchIssues(updatedDisplayFilters)) {
|
||||
this.rootIssueStore.projectIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation");
|
||||
}
|
||||
|
||||
await this.issueFilterService.patchProjectIssueFilters(workspaceSlug, projectId, {
|
||||
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,
|
||||
[projectId, "displayProperties", _key],
|
||||
updatedDisplayProperties[_key as keyof IIssueDisplayProperties]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
await this.issueFilterService.patchProjectIssueFilters(workspaceSlug, projectId, {
|
||||
display_properties: _filters.displayProperties,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case EIssueFilterType.KANBAN_FILTERS: {
|
||||
const updatedKanbanFilters = filters as TIssueKanbanFilters;
|
||||
_filters.kanbanFilters = { ..._filters.kanbanFilters, ...updatedKanbanFilters };
|
||||
|
||||
const currentUserId = this.rootIssueStore.currentUserId;
|
||||
if (currentUserId)
|
||||
this.handleIssuesLocalFilters.set(EIssuesStoreType.PROJECT, type, workspaceSlug, projectId, currentUserId, {
|
||||
kanban_filters: _filters.kanbanFilters,
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
Object.keys(updatedKanbanFilters).forEach((_key) => {
|
||||
set(
|
||||
this.filters,
|
||||
[projectId, "kanbanFilters", _key],
|
||||
updatedKanbanFilters[_key as keyof TIssueKanbanFilters]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
this.fetchFilters(workspaceSlug, projectId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
2
apps/web/core/store/issue/project/index.ts
Normal file
2
apps/web/core/store/issue/project/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./filter.store";
|
||||
export * from "./issue.store";
|
||||
199
apps/web/core/store/issue/project/issue.store.ts
Normal file
199
apps/web/core/store/issue/project/issue.store.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { action, makeObservable, runInAction } from "mobx";
|
||||
// types
|
||||
import type {
|
||||
TIssue,
|
||||
TLoader,
|
||||
ViewFlags,
|
||||
IssuePaginationOptions,
|
||||
TIssuesResponse,
|
||||
TBulkOperationsPayload,
|
||||
} from "@plane/types";
|
||||
// helpers
|
||||
// base class
|
||||
import type { IBaseIssuesStore } from "../helpers/base-issues.store";
|
||||
import { BaseIssuesStore } from "../helpers/base-issues.store";
|
||||
// services
|
||||
import type { IIssueRootStore } from "../root.store";
|
||||
import type { IProjectIssuesFilter } from "./filter.store";
|
||||
|
||||
export interface IProjectIssues extends IBaseIssuesStore {
|
||||
viewFlags: ViewFlags;
|
||||
// action
|
||||
fetchIssues: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
loadType: TLoader,
|
||||
option: IssuePaginationOptions
|
||||
) => Promise<TIssuesResponse | undefined>;
|
||||
fetchIssuesWithExistingPagination: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
loadType: TLoader
|
||||
) => Promise<TIssuesResponse | undefined>;
|
||||
fetchNextIssues: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
groupId?: string,
|
||||
subGroupId?: string
|
||||
) => Promise<TIssuesResponse | undefined>;
|
||||
|
||||
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>;
|
||||
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||
archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
quickAddIssue: (workspaceSlug: string, projectId: string, data: TIssue) => Promise<TIssue | undefined>;
|
||||
removeBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise<void>;
|
||||
archiveBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise<void>;
|
||||
bulkUpdateProperties: (workspaceSlug: string, projectId: string, data: TBulkOperationsPayload) => Promise<void>;
|
||||
}
|
||||
|
||||
export class ProjectIssues extends BaseIssuesStore implements IProjectIssues {
|
||||
viewFlags = {
|
||||
enableQuickAdd: true,
|
||||
enableIssueCreation: true,
|
||||
enableInlineEditing: true,
|
||||
};
|
||||
router;
|
||||
|
||||
// filter store
|
||||
issueFilterStore: IProjectIssuesFilter;
|
||||
|
||||
constructor(_rootStore: IIssueRootStore, issueFilterStore: IProjectIssuesFilter) {
|
||||
super(_rootStore, issueFilterStore);
|
||||
makeObservable(this, {
|
||||
fetchIssues: action,
|
||||
fetchNextIssues: action,
|
||||
fetchIssuesWithExistingPagination: action,
|
||||
|
||||
quickAddIssue: action,
|
||||
});
|
||||
// filter store
|
||||
this.issueFilterStore = issueFilterStore;
|
||||
this.router = _rootStore.rootStore.router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the project details
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
*/
|
||||
fetchParentStats = async (workspaceSlug: string, projectId?: string) => {
|
||||
projectId && this.rootIssueStore.rootStore.projectRoot.project.fetchProjectDetails(workspaceSlug, projectId);
|
||||
};
|
||||
|
||||
/** */
|
||||
updateParentStats = () => {};
|
||||
|
||||
/**
|
||||
* This method is called to fetch the first issues of pagination
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param loadType
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
fetchIssues = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
loadType: TLoader = "init-loader",
|
||||
options: IssuePaginationOptions,
|
||||
isExistingPaginationOptions: boolean = false
|
||||
) => {
|
||||
try {
|
||||
// set loader and clear store
|
||||
runInAction(() => {
|
||||
this.setLoader(loadType);
|
||||
this.clear(!isExistingPaginationOptions, false); // clear while fetching from server.
|
||||
if (!this.groupBy) this.clear(!isExistingPaginationOptions, true); // clear while using local to have the no load effect.
|
||||
});
|
||||
|
||||
// get params from pagination options
|
||||
const params = this.issueFilterStore?.getFilterParams(options, projectId, undefined, undefined, undefined);
|
||||
// call the fetch issues API with the params
|
||||
const response = await this.issueService.getIssues(workspaceSlug, projectId, params, {
|
||||
signal: this.controller.signal,
|
||||
});
|
||||
|
||||
// after fetching issues, call the base method to process the response further
|
||||
this.onfetchIssues(response, options, workspaceSlug, projectId, undefined, !isExistingPaginationOptions);
|
||||
return response;
|
||||
} catch (error) {
|
||||
// set loader to undefined if errored out
|
||||
this.setLoader(undefined);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This method is called subsequent pages of pagination
|
||||
* if groupId/subgroupId is provided, only that specific group's next page is fetched
|
||||
* else all the groups' next page is fetched
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param groupId
|
||||
* @param subGroupId
|
||||
* @returns
|
||||
*/
|
||||
fetchNextIssues = async (workspaceSlug: string, projectId: string, groupId?: string, subGroupId?: string) => {
|
||||
const cursorObject = this.getPaginationData(groupId, subGroupId);
|
||||
// if there are no pagination options and the next page results do not exist the return
|
||||
if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return;
|
||||
try {
|
||||
// set Loader
|
||||
this.setLoader("pagination", groupId, subGroupId);
|
||||
|
||||
// get params from stored pagination options
|
||||
const params = this.issueFilterStore?.getFilterParams(
|
||||
this.paginationOptions,
|
||||
projectId,
|
||||
this.getNextCursor(groupId, subGroupId),
|
||||
groupId,
|
||||
subGroupId
|
||||
);
|
||||
// call the fetch issues API with the params for next page in issues
|
||||
const response = await this.issueService.getIssues(workspaceSlug, projectId, params);
|
||||
|
||||
// after the next page of issues are fetched, call the base method to process the response
|
||||
this.onfetchNexIssues(response, groupId, subGroupId);
|
||||
return response;
|
||||
} catch (error) {
|
||||
// set Loader as undefined if errored out
|
||||
this.setLoader(undefined, groupId, subGroupId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This Method exists to fetch the first page of the issues with the existing stored pagination
|
||||
* This is useful for refetching when filters, groupBy, orderBy etc changes
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param loadType
|
||||
* @returns
|
||||
*/
|
||||
fetchIssuesWithExistingPagination = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
loadType: TLoader = "mutation"
|
||||
) => {
|
||||
if (!this.paginationOptions) return;
|
||||
return await this.fetchIssues(workspaceSlug, projectId, loadType, this.paginationOptions, true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Override inherited create issue, to update list only if user is on current project
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
override createIssue = async (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => {
|
||||
const response = await super.createIssue(workspaceSlug, projectId, data, "", projectId === this.router.projectId);
|
||||
return response;
|
||||
};
|
||||
|
||||
// Using aliased names as they cannot be overridden in other stores
|
||||
archiveBulkIssues = this.bulkArchiveIssues;
|
||||
quickAddIssue = this.issueQuickAdd;
|
||||
updateIssue = this.issueUpdate;
|
||||
archiveIssue = this.issueArchive;
|
||||
}
|
||||
274
apps/web/core/store/issue/root.store.ts
Normal file
274
apps/web/core/store/issue/root.store.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import { isEmpty } from "lodash-es";
|
||||
import { autorun, makeObservable, observable } from "mobx";
|
||||
// types
|
||||
import type { ICycle, IIssueLabel, IModule, IProject, IState, IUserLite, TIssueServiceType } from "@plane/types";
|
||||
import { EIssueServiceType } from "@plane/types";
|
||||
// plane web store
|
||||
import type { IProjectEpics, IProjectEpicsFilter } from "@/plane-web/store/issue/epic";
|
||||
import { ProjectEpics, ProjectEpicsFilter } from "@/plane-web/store/issue/epic";
|
||||
import type { IIssueDetail } from "@/plane-web/store/issue/issue-details/root.store";
|
||||
import { IssueDetail } from "@/plane-web/store/issue/issue-details/root.store";
|
||||
import type { ITeamIssuesFilter, ITeamIssues } from "@/plane-web/store/issue/team";
|
||||
import { TeamIssues, TeamIssuesFilter } from "@/plane-web/store/issue/team";
|
||||
import type { ITeamProjectWorkItemsFilter } from "@/plane-web/store/issue/team-project/filter.store";
|
||||
import { TeamProjectWorkItemsFilter } from "@/plane-web/store/issue/team-project/filter.store";
|
||||
import type { ITeamProjectWorkItems } from "@/plane-web/store/issue/team-project/issue.store";
|
||||
import { TeamProjectWorkItems } from "@/plane-web/store/issue/team-project/issue.store";
|
||||
import type { ITeamViewIssues, ITeamViewIssuesFilter } from "@/plane-web/store/issue/team-views";
|
||||
import { TeamViewIssues, TeamViewIssuesFilter } from "@/plane-web/store/issue/team-views";
|
||||
// root store
|
||||
import type { IWorkspaceIssues } from "@/plane-web/store/issue/workspace/issue.store";
|
||||
import { WorkspaceIssues } from "@/plane-web/store/issue/workspace/issue.store";
|
||||
import type { RootStore } from "@/plane-web/store/root.store";
|
||||
import type { IWorkspaceMembership } from "@/store/member/workspace/workspace-member.store";
|
||||
// issues data store
|
||||
import type { IArchivedIssuesFilter, IArchivedIssues } from "./archived";
|
||||
import { ArchivedIssuesFilter, ArchivedIssues } from "./archived";
|
||||
import type { ICycleIssuesFilter, ICycleIssues } from "./cycle";
|
||||
import { CycleIssuesFilter, CycleIssues } from "./cycle";
|
||||
import type { IIssueStore } from "./issue.store";
|
||||
import { IssueStore } from "./issue.store";
|
||||
import type { ICalendarStore } from "./issue_calendar_view.store";
|
||||
import { CalendarStore } from "./issue_calendar_view.store";
|
||||
import type { IIssueKanBanViewStore } from "./issue_kanban_view.store";
|
||||
import { IssueKanBanViewStore } from "./issue_kanban_view.store";
|
||||
import type { IModuleIssuesFilter, IModuleIssues } from "./module";
|
||||
import { ModuleIssuesFilter, ModuleIssues } from "./module";
|
||||
import type { IProfileIssuesFilter, IProfileIssues } from "./profile";
|
||||
import { ProfileIssuesFilter, ProfileIssues } from "./profile";
|
||||
import type { IProjectIssuesFilter, IProjectIssues } from "./project";
|
||||
import { ProjectIssuesFilter, ProjectIssues } from "./project";
|
||||
import type { IProjectViewIssuesFilter, IProjectViewIssues } from "./project-views";
|
||||
import { ProjectViewIssuesFilter, ProjectViewIssues } from "./project-views";
|
||||
import type { IWorkspaceIssuesFilter } from "./workspace";
|
||||
import { WorkspaceIssuesFilter } from "./workspace";
|
||||
import type { IWorkspaceDraftIssues, IWorkspaceDraftIssuesFilter } from "./workspace-draft";
|
||||
import { WorkspaceDraftIssues, WorkspaceDraftIssuesFilter } from "./workspace-draft";
|
||||
|
||||
export interface IIssueRootStore {
|
||||
currentUserId: string | undefined;
|
||||
workspaceSlug: string | undefined;
|
||||
teamspaceId: string | undefined;
|
||||
projectId: string | undefined;
|
||||
cycleId: string | undefined;
|
||||
moduleId: string | undefined;
|
||||
viewId: string | undefined;
|
||||
globalViewId: string | undefined; // all issues view id
|
||||
userId: string | undefined; // user profile detail Id
|
||||
stateMap: Record<string, IState> | undefined;
|
||||
stateDetails: IState[] | undefined;
|
||||
workspaceStateDetails: IState[] | undefined;
|
||||
labelMap: Record<string, IIssueLabel> | undefined;
|
||||
workSpaceMemberRolesMap: Record<string, IWorkspaceMembership> | undefined;
|
||||
memberMap: Record<string, IUserLite> | undefined;
|
||||
projectMap: Record<string, IProject> | undefined;
|
||||
moduleMap: Record<string, IModule> | undefined;
|
||||
cycleMap: Record<string, ICycle> | undefined;
|
||||
|
||||
rootStore: RootStore;
|
||||
serviceType: TIssueServiceType;
|
||||
|
||||
issues: IIssueStore;
|
||||
|
||||
issueDetail: IIssueDetail;
|
||||
epicDetail: IIssueDetail;
|
||||
|
||||
workspaceIssuesFilter: IWorkspaceIssuesFilter;
|
||||
workspaceIssues: IWorkspaceIssues;
|
||||
|
||||
workspaceDraftIssuesFilter: IWorkspaceDraftIssuesFilter;
|
||||
workspaceDraftIssues: IWorkspaceDraftIssues;
|
||||
|
||||
profileIssuesFilter: IProfileIssuesFilter;
|
||||
profileIssues: IProfileIssues;
|
||||
|
||||
teamIssuesFilter: ITeamIssuesFilter;
|
||||
teamIssues: ITeamIssues;
|
||||
|
||||
projectIssuesFilter: IProjectIssuesFilter;
|
||||
projectIssues: IProjectIssues;
|
||||
|
||||
cycleIssuesFilter: ICycleIssuesFilter;
|
||||
cycleIssues: ICycleIssues;
|
||||
|
||||
moduleIssuesFilter: IModuleIssuesFilter;
|
||||
moduleIssues: IModuleIssues;
|
||||
|
||||
teamViewIssuesFilter: ITeamViewIssuesFilter;
|
||||
teamViewIssues: ITeamViewIssues;
|
||||
|
||||
teamProjectWorkItemsFilter: ITeamProjectWorkItemsFilter;
|
||||
teamProjectWorkItems: ITeamProjectWorkItems;
|
||||
|
||||
projectViewIssuesFilter: IProjectViewIssuesFilter;
|
||||
projectViewIssues: IProjectViewIssues;
|
||||
|
||||
archivedIssuesFilter: IArchivedIssuesFilter;
|
||||
archivedIssues: IArchivedIssues;
|
||||
|
||||
issueKanBanView: IIssueKanBanViewStore;
|
||||
issueCalendarView: ICalendarStore;
|
||||
|
||||
projectEpicsFilter: IProjectEpicsFilter;
|
||||
projectEpics: IProjectEpics;
|
||||
}
|
||||
|
||||
export class IssueRootStore implements IIssueRootStore {
|
||||
currentUserId: string | undefined = undefined;
|
||||
workspaceSlug: string | undefined = undefined;
|
||||
teamspaceId: string | undefined = undefined;
|
||||
projectId: string | undefined = undefined;
|
||||
cycleId: string | undefined = undefined;
|
||||
moduleId: string | undefined = undefined;
|
||||
viewId: string | undefined = undefined;
|
||||
globalViewId: string | undefined = undefined;
|
||||
userId: string | undefined = undefined;
|
||||
stateMap: Record<string, IState> | undefined = undefined;
|
||||
stateDetails: IState[] | undefined = undefined;
|
||||
workspaceStateDetails: IState[] | undefined = undefined;
|
||||
labelMap: Record<string, IIssueLabel> | undefined = undefined;
|
||||
workSpaceMemberRolesMap: Record<string, IWorkspaceMembership> | undefined = undefined;
|
||||
memberMap: Record<string, IUserLite> | undefined = undefined;
|
||||
projectMap: Record<string, IProject> | undefined = undefined;
|
||||
moduleMap: Record<string, IModule> | undefined = undefined;
|
||||
cycleMap: Record<string, ICycle> | undefined = undefined;
|
||||
|
||||
rootStore: RootStore;
|
||||
serviceType: TIssueServiceType;
|
||||
|
||||
issues: IIssueStore;
|
||||
|
||||
issueDetail: IIssueDetail;
|
||||
epicDetail: IIssueDetail;
|
||||
|
||||
workspaceIssuesFilter: IWorkspaceIssuesFilter;
|
||||
workspaceIssues: IWorkspaceIssues;
|
||||
|
||||
workspaceDraftIssuesFilter: IWorkspaceDraftIssuesFilter;
|
||||
workspaceDraftIssues: IWorkspaceDraftIssues;
|
||||
|
||||
profileIssuesFilter: IProfileIssuesFilter;
|
||||
profileIssues: IProfileIssues;
|
||||
|
||||
teamIssuesFilter: ITeamIssuesFilter;
|
||||
teamIssues: ITeamIssues;
|
||||
|
||||
projectIssuesFilter: IProjectIssuesFilter;
|
||||
projectIssues: IProjectIssues;
|
||||
|
||||
cycleIssuesFilter: ICycleIssuesFilter;
|
||||
cycleIssues: ICycleIssues;
|
||||
|
||||
moduleIssuesFilter: IModuleIssuesFilter;
|
||||
moduleIssues: IModuleIssues;
|
||||
|
||||
teamViewIssuesFilter: ITeamViewIssuesFilter;
|
||||
teamViewIssues: ITeamViewIssues;
|
||||
|
||||
projectViewIssuesFilter: IProjectViewIssuesFilter;
|
||||
projectViewIssues: IProjectViewIssues;
|
||||
|
||||
teamProjectWorkItemsFilter: ITeamProjectWorkItemsFilter;
|
||||
teamProjectWorkItems: ITeamProjectWorkItems;
|
||||
|
||||
archivedIssuesFilter: IArchivedIssuesFilter;
|
||||
archivedIssues: IArchivedIssues;
|
||||
|
||||
issueKanBanView: IIssueKanBanViewStore;
|
||||
issueCalendarView: ICalendarStore;
|
||||
|
||||
projectEpicsFilter: IProjectEpicsFilter;
|
||||
projectEpics: IProjectEpics;
|
||||
|
||||
constructor(rootStore: RootStore, serviceType: TIssueServiceType = EIssueServiceType.ISSUES) {
|
||||
makeObservable(this, {
|
||||
workspaceSlug: observable.ref,
|
||||
teamspaceId: observable.ref,
|
||||
projectId: observable.ref,
|
||||
cycleId: observable.ref,
|
||||
moduleId: observable.ref,
|
||||
viewId: observable.ref,
|
||||
userId: observable.ref,
|
||||
globalViewId: observable.ref,
|
||||
stateMap: observable,
|
||||
stateDetails: observable,
|
||||
workspaceStateDetails: observable,
|
||||
labelMap: observable,
|
||||
memberMap: observable,
|
||||
workSpaceMemberRolesMap: observable,
|
||||
projectMap: observable,
|
||||
moduleMap: observable,
|
||||
cycleMap: observable,
|
||||
});
|
||||
|
||||
this.serviceType = serviceType;
|
||||
this.rootStore = rootStore;
|
||||
|
||||
autorun(() => {
|
||||
if (rootStore?.user?.data?.id) this.currentUserId = rootStore?.user?.data?.id;
|
||||
if (this.workspaceSlug !== rootStore.router.workspaceSlug) this.workspaceSlug = rootStore.router.workspaceSlug;
|
||||
if (this.teamspaceId !== rootStore.router.teamspaceId) this.teamspaceId = rootStore.router.teamspaceId;
|
||||
if (this.projectId !== rootStore.router.projectId) this.projectId = rootStore.router.projectId;
|
||||
if (this.cycleId !== rootStore.router.cycleId) this.cycleId = rootStore.router.cycleId;
|
||||
if (this.moduleId !== rootStore.router.moduleId) this.moduleId = rootStore.router.moduleId;
|
||||
if (this.viewId !== rootStore.router.viewId) this.viewId = rootStore.router.viewId;
|
||||
if (this.globalViewId !== rootStore.router.globalViewId) this.globalViewId = rootStore.router.globalViewId;
|
||||
if (this.userId !== rootStore.router.userId) this.userId = rootStore.router.userId;
|
||||
if (!isEmpty(rootStore?.state?.stateMap)) this.stateMap = rootStore?.state?.stateMap;
|
||||
if (!isEmpty(rootStore?.state?.projectStates)) this.stateDetails = rootStore?.state?.projectStates;
|
||||
if (!isEmpty(rootStore?.state?.workspaceStates)) this.workspaceStateDetails = rootStore?.state?.workspaceStates;
|
||||
if (!isEmpty(rootStore?.label?.labelMap)) this.labelMap = rootStore?.label?.labelMap;
|
||||
if (!isEmpty(rootStore?.memberRoot?.workspace?.workspaceMemberMap))
|
||||
this.workSpaceMemberRolesMap = rootStore?.memberRoot?.workspace?.memberMap || undefined;
|
||||
if (!isEmpty(rootStore?.memberRoot?.memberMap)) this.memberMap = rootStore?.memberRoot?.memberMap || undefined;
|
||||
if (!isEmpty(rootStore?.projectRoot?.project?.projectMap))
|
||||
this.projectMap = rootStore?.projectRoot?.project?.projectMap;
|
||||
if (!isEmpty(rootStore?.module?.moduleMap)) this.moduleMap = rootStore?.module?.moduleMap;
|
||||
if (!isEmpty(rootStore?.cycle?.cycleMap)) this.cycleMap = rootStore?.cycle?.cycleMap;
|
||||
});
|
||||
|
||||
this.issues = new IssueStore();
|
||||
|
||||
this.issueDetail = new IssueDetail(this, EIssueServiceType.ISSUES);
|
||||
this.epicDetail = new IssueDetail(this, EIssueServiceType.EPICS);
|
||||
|
||||
this.workspaceIssuesFilter = new WorkspaceIssuesFilter(this);
|
||||
this.workspaceIssues = new WorkspaceIssues(this, this.workspaceIssuesFilter);
|
||||
|
||||
this.profileIssuesFilter = new ProfileIssuesFilter(this);
|
||||
this.profileIssues = new ProfileIssues(this, this.profileIssuesFilter);
|
||||
|
||||
this.workspaceDraftIssuesFilter = new WorkspaceDraftIssuesFilter(this);
|
||||
this.workspaceDraftIssues = new WorkspaceDraftIssues(this);
|
||||
|
||||
this.projectIssuesFilter = new ProjectIssuesFilter(this);
|
||||
this.projectIssues = new ProjectIssues(this, this.projectIssuesFilter);
|
||||
|
||||
this.teamIssuesFilter = new TeamIssuesFilter(this);
|
||||
this.teamIssues = new TeamIssues(this, this.teamIssuesFilter);
|
||||
|
||||
this.cycleIssuesFilter = new CycleIssuesFilter(this);
|
||||
this.cycleIssues = new CycleIssues(this, this.cycleIssuesFilter);
|
||||
|
||||
this.moduleIssuesFilter = new ModuleIssuesFilter(this);
|
||||
this.moduleIssues = new ModuleIssues(this, this.moduleIssuesFilter);
|
||||
|
||||
this.teamViewIssuesFilter = new TeamViewIssuesFilter(this);
|
||||
this.teamViewIssues = new TeamViewIssues(this, this.teamViewIssuesFilter);
|
||||
|
||||
this.projectViewIssuesFilter = new ProjectViewIssuesFilter(this);
|
||||
this.projectViewIssues = new ProjectViewIssues(this, this.projectViewIssuesFilter);
|
||||
|
||||
this.teamProjectWorkItemsFilter = new TeamProjectWorkItemsFilter(this);
|
||||
this.teamProjectWorkItems = new TeamProjectWorkItems(this, this.teamProjectWorkItemsFilter);
|
||||
|
||||
this.archivedIssuesFilter = new ArchivedIssuesFilter(this);
|
||||
this.archivedIssues = new ArchivedIssues(this, this.archivedIssuesFilter);
|
||||
|
||||
this.issueKanBanView = new IssueKanBanViewStore(this);
|
||||
this.issueCalendarView = new CalendarStore(this);
|
||||
|
||||
this.projectEpicsFilter = new ProjectEpicsFilter(this);
|
||||
this.projectEpics = new ProjectEpics(this, this.projectEpicsFilter);
|
||||
}
|
||||
}
|
||||
268
apps/web/core/store/issue/workspace-draft/filter.store.ts
Normal file
268
apps/web/core/store/issue/workspace-draft/filter.store.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import { isEmpty, set } from "lodash-es";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// Plane Imports
|
||||
import type { TSupportedFilterTypeForUpdate } from "@plane/constants";
|
||||
import { EIssueFilterType } from "@plane/constants";
|
||||
import type {
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
TIssueKanbanFilters,
|
||||
IIssueFilters,
|
||||
TIssueParams,
|
||||
IssuePaginationOptions,
|
||||
TWorkItemFilterExpression,
|
||||
TSupportedFilterForUpdate,
|
||||
} from "@plane/types";
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
import { handleIssueQueryParamsByLayout } from "@plane/utils";
|
||||
// services
|
||||
import { IssueFiltersService } from "@/services/issue_filter.service";
|
||||
// helpers
|
||||
import type { IBaseIssueFilterStore } from "../helpers/issue-filter-helper.store";
|
||||
import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store";
|
||||
// types
|
||||
import type { IIssueRootStore } from "../root.store";
|
||||
|
||||
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>;
|
||||
updateFilterExpression: (workspaceSlug: string, userId: string, filters: TWorkItemFilterExpression) => Promise<void>;
|
||||
updateFilters: (
|
||||
workspaceSlug: string,
|
||||
filterType: TSupportedFilterTypeForUpdate,
|
||||
filters: TSupportedFilterForUpdate
|
||||
) => 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?.richFilters,
|
||||
userFilters?.displayFilters,
|
||||
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 richFilters: TWorkItemFilterExpression = _filters?.rich_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, "richFilters"], richFilters);
|
||||
set(this.filters, [workspaceSlug, "displayFilters"], displayFilters);
|
||||
set(this.filters, [workspaceSlug, "displayProperties"], displayProperties);
|
||||
set(this.filters, [workspaceSlug, "kanbanFilters"], kanbanFilters);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* NOTE: This method is designed as a fallback function for the work item filter store.
|
||||
* Only use this method directly when initializing filter instances.
|
||||
* For regular filter updates, use this method as a fallback function for the work item filter store methods instead.
|
||||
*/
|
||||
updateFilterExpression: IWorkspaceDraftIssuesFilter["updateFilterExpression"] = async (
|
||||
workspaceSlug,
|
||||
userId,
|
||||
filters
|
||||
) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
set(this.filters, [workspaceSlug, "richFilters"], filters);
|
||||
});
|
||||
|
||||
this.rootIssueStore.profileIssues.fetchIssuesWithExistingPagination(workspaceSlug, workspaceSlug, "mutation");
|
||||
this.handleIssuesLocalFilters.set(
|
||||
EIssuesStoreType.PROFILE,
|
||||
EIssueFilterType.FILTERS,
|
||||
workspaceSlug,
|
||||
workspaceSlug,
|
||||
undefined,
|
||||
{
|
||||
rich_filters: filters,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("error while updating rich filters", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
updateFilters: IWorkspaceDraftIssuesFilter["updateFilters"] = async (workspaceSlug, type, filters) => {
|
||||
try {
|
||||
if (isEmpty(this.filters) || isEmpty(this.filters[workspaceSlug])) return;
|
||||
|
||||
const _filters = {
|
||||
richFilters: this.filters[workspaceSlug].richFilters as TWorkItemFilterExpression,
|
||||
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.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;
|
||||
}
|
||||
};
|
||||
}
|
||||
2
apps/web/core/store/issue/workspace-draft/index.ts
Normal file
2
apps/web/core/store/issue/workspace-draft/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./issue.store";
|
||||
export * from "./filter.store";
|
||||
420
apps/web/core/store/issue/workspace-draft/issue.store.ts
Normal file
420
apps/web/core/store/issue/workspace-draft/issue.store.ts
Normal file
@@ -0,0 +1,420 @@
|
||||
import { clone, update, unset, orderBy, set } from "lodash-es";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// plane imports
|
||||
import { EDraftIssuePaginationType } from "@plane/constants";
|
||||
import type {
|
||||
TWorkspaceDraftIssue,
|
||||
TWorkspaceDraftPaginationInfo,
|
||||
TWorkspaceDraftIssueLoader,
|
||||
TWorkspaceDraftQueryParams,
|
||||
TPaginationData,
|
||||
TLoader,
|
||||
TGroupedIssues,
|
||||
TSubGroupedIssues,
|
||||
ViewFlags,
|
||||
TIssue,
|
||||
TBulkOperationsPayload,
|
||||
} from "@plane/types";
|
||||
import { getCurrentDateTimeInISO, convertToISODateString } from "@plane/utils";
|
||||
// local-db
|
||||
import { addIssueToPersistanceLayer } from "@/local-db/utils/utils";
|
||||
// services
|
||||
import workspaceDraftService from "@/services/issue/workspace_draft.service";
|
||||
// types
|
||||
import type { IIssueRootStore } from "../root.store";
|
||||
|
||||
export type TDraftIssuePaginationType = EDraftIssuePaginationType;
|
||||
|
||||
export interface IWorkspaceDraftIssues {
|
||||
// observables
|
||||
loader: TWorkspaceDraftIssueLoader;
|
||||
paginationInfo: Omit<TWorkspaceDraftPaginationInfo<TWorkspaceDraftIssue>, "results"> | undefined;
|
||||
issuesMap: Record<string, TWorkspaceDraftIssue>; // issue_id -> issue;
|
||||
issueMapIds: Record<string, string[]>; // workspace_id -> issue_ids;
|
||||
// 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) => Promise<void>;
|
||||
// actions
|
||||
fetchIssues: (
|
||||
workspaceSlug: string,
|
||||
loadType: TWorkspaceDraftIssueLoader,
|
||||
paginationType?: TDraftIssuePaginationType
|
||||
) => Promise<TWorkspaceDraftPaginationInfo<TWorkspaceDraftIssue> | undefined>;
|
||||
createIssue: (
|
||||
workspaceSlug: string,
|
||||
payload: Partial<TWorkspaceDraftIssue | TIssue>
|
||||
) => Promise<TWorkspaceDraftIssue | undefined>;
|
||||
updateIssue: (
|
||||
workspaceSlug: string,
|
||||
issueId: string,
|
||||
payload: Partial<TWorkspaceDraftIssue | TIssue>
|
||||
) => Promise<TWorkspaceDraftIssue | undefined>;
|
||||
deleteIssue: (workspaceSlug: string, issueId: string) => Promise<void>;
|
||||
moveIssue: (workspaceSlug: string, issueId: string, payload: Partial<TWorkspaceDraftIssue>) => Promise<TIssue>;
|
||||
addCycleToIssue: (
|
||||
workspaceSlug: string,
|
||||
issueId: string,
|
||||
cycleId: string
|
||||
) => Promise<TWorkspaceDraftIssue | undefined>;
|
||||
addModulesToIssue: (
|
||||
workspaceSlug: string,
|
||||
issueId: string,
|
||||
moduleIds: string[]
|
||||
) => Promise<TWorkspaceDraftIssue | undefined>;
|
||||
|
||||
// dummies
|
||||
viewFlags: ViewFlags;
|
||||
groupedIssueIds: TGroupedIssues | TSubGroupedIssues | undefined;
|
||||
getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined;
|
||||
getPaginationData(groupId: string | undefined, subGroupId: string | undefined): TPaginationData | undefined;
|
||||
getIssueLoader(groupId?: string, subGroupId?: string): TLoader;
|
||||
getGroupIssueCount: (
|
||||
groupId: string | undefined,
|
||||
subGroupId: string | undefined,
|
||||
isSubGroupCumulative: boolean
|
||||
) => number | undefined;
|
||||
removeCycleFromIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
addIssueToCycle: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
cycleId: string,
|
||||
issueIds: string[],
|
||||
fetchAddedIssues?: boolean
|
||||
) => Promise<void>;
|
||||
removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
|
||||
|
||||
removeIssuesFromModule: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
moduleId: string,
|
||||
issueIds: string[]
|
||||
) => Promise<void>;
|
||||
changeModulesInIssue(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
addModuleIds: string[],
|
||||
removeModuleIds: string[]
|
||||
): Promise<void>;
|
||||
archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
archiveBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise<void>;
|
||||
removeBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise<void>;
|
||||
bulkUpdateProperties: (workspaceSlug: string, projectId: string, data: TBulkOperationsPayload) => Promise<void>;
|
||||
}
|
||||
|
||||
export class WorkspaceDraftIssues implements IWorkspaceDraftIssues {
|
||||
// local constants
|
||||
paginatedCount = 50;
|
||||
// observables
|
||||
loader: TWorkspaceDraftIssueLoader = undefined;
|
||||
paginationInfo: Omit<TWorkspaceDraftPaginationInfo<TWorkspaceDraftIssue>, "results"> | undefined = undefined;
|
||||
issuesMap: Record<string, TWorkspaceDraftIssue> = {};
|
||||
issueMapIds: Record<string, string[]> = {};
|
||||
|
||||
constructor(public issueStore: IIssueRootStore) {
|
||||
makeObservable(this, {
|
||||
loader: observable.ref,
|
||||
paginationInfo: observable,
|
||||
issuesMap: observable,
|
||||
issueMapIds: observable,
|
||||
// computed
|
||||
issueIds: computed,
|
||||
// action
|
||||
fetchIssues: action,
|
||||
createIssue: action,
|
||||
updateIssue: action,
|
||||
deleteIssue: action,
|
||||
moveIssue: action,
|
||||
addCycleToIssue: action,
|
||||
addModulesToIssue: action,
|
||||
});
|
||||
}
|
||||
|
||||
private updateWorkspaceUserDraftIssueCount(workspaceSlug: string, increment: number) {
|
||||
const workspaceUserInfo = this.issueStore.rootStore.user.permission.workspaceUserInfo;
|
||||
const currentCount = workspaceUserInfo[workspaceSlug]?.draft_issue_count ?? 0;
|
||||
|
||||
set(workspaceUserInfo, [workspaceSlug, "draft_issue_count"], currentCount + increment);
|
||||
}
|
||||
|
||||
// computed
|
||||
get issueIds() {
|
||||
const workspaceSlug = this.issueStore.workspaceSlug;
|
||||
if (!workspaceSlug) return [];
|
||||
if (!this.issueMapIds[workspaceSlug]) return [];
|
||||
const issueIds = this.issueMapIds[workspaceSlug];
|
||||
return orderBy(issueIds, (issueId) => convertToISODateString(this.issuesMap[issueId]?.created_at), ["desc"]);
|
||||
}
|
||||
|
||||
// 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 = async (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) {
|
||||
// adding issueIds
|
||||
const issueIds = results.map((issue) => issue.id);
|
||||
const existingIssueIds = this.issueMapIds[workspaceSlug] ?? [];
|
||||
// new issueIds
|
||||
const newIssueIds = issueIds.filter((issueId) => !existingIssueIds.includes(issueId));
|
||||
this.addIssue(results);
|
||||
// issue map update
|
||||
update(this.issueMapIds, [workspaceSlug], (existingIssueIds = []) => [...newIssueIds, ...existingIssueIds]);
|
||||
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 | TIssue>
|
||||
): Promise<TWorkspaceDraftIssue | undefined> => {
|
||||
try {
|
||||
this.loader = "create";
|
||||
|
||||
const response = await workspaceDraftService.createIssue(workspaceSlug, payload);
|
||||
if (response) {
|
||||
runInAction(() => {
|
||||
this.addIssue([response]);
|
||||
update(this.issueMapIds, [workspaceSlug], (existingIssueIds = []) => [response.id, ...existingIssueIds]);
|
||||
// increase the count of issues in the pagination info
|
||||
if (this.paginationInfo?.total_count) {
|
||||
set(this, "paginationInfo", {
|
||||
...this.paginationInfo,
|
||||
total_count: this.paginationInfo.total_count + 1,
|
||||
});
|
||||
}
|
||||
// Update draft issue count in workspaceUserInfo
|
||||
this.updateWorkspaceUserDraftIssueCount(workspaceSlug, 1);
|
||||
});
|
||||
}
|
||||
|
||||
this.loader = undefined;
|
||||
return response;
|
||||
} catch (error) {
|
||||
this.loader = undefined;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
updateIssue = async (workspaceSlug: string, issueId: string, payload: Partial<TWorkspaceDraftIssue | TIssue>) => {
|
||||
const issueBeforeUpdate = clone(this.getIssueById(issueId));
|
||||
try {
|
||||
this.loader = "update";
|
||||
runInAction(() => {
|
||||
set(this.issuesMap, [issueId], {
|
||||
...issueBeforeUpdate,
|
||||
...payload,
|
||||
...{ updated_at: getCurrentDateTimeInISO() },
|
||||
});
|
||||
});
|
||||
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(() => {
|
||||
// Remove the issue from the issueMapIds
|
||||
this.issueMapIds[workspaceSlug] = (this.issueMapIds[workspaceSlug] || []).filter((id) => id !== issueId);
|
||||
// Remove the issue from the issuesMap
|
||||
delete this.issuesMap[issueId];
|
||||
// reduce the count of issues in the pagination info
|
||||
if (this.paginationInfo?.total_count) {
|
||||
set(this, "paginationInfo", {
|
||||
...this.paginationInfo,
|
||||
total_count: this.paginationInfo.total_count - 1,
|
||||
});
|
||||
}
|
||||
// Update draft issue count in workspaceUserInfo
|
||||
this.updateWorkspaceUserDraftIssueCount(workspaceSlug, -1);
|
||||
});
|
||||
|
||||
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(() => {
|
||||
// Remove the issue from the issueMapIds
|
||||
this.issueMapIds[workspaceSlug] = (this.issueMapIds[workspaceSlug] || []).filter((id) => id !== issueId);
|
||||
// Remove the issue from the issuesMap
|
||||
delete this.issuesMap[issueId];
|
||||
// reduce the count of issues in the pagination info
|
||||
if (this.paginationInfo?.total_count) {
|
||||
set(this, "paginationInfo", {
|
||||
...this.paginationInfo,
|
||||
total_count: this.paginationInfo.total_count - 1,
|
||||
});
|
||||
}
|
||||
|
||||
// sync issue to local db
|
||||
addIssueToPersistanceLayer({ ...payload, ...response });
|
||||
|
||||
// Update draft issue count in workspaceUserInfo
|
||||
this.updateWorkspaceUserDraftIssueCount(workspaceSlug, -1);
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
// dummies
|
||||
viewFlags: ViewFlags = { enableQuickAdd: false, enableIssueCreation: false, enableInlineEditing: false };
|
||||
groupedIssueIds: TGroupedIssues | TSubGroupedIssues | undefined = undefined;
|
||||
getIssueIds = (groupId?: string, subGroupId?: string) => undefined;
|
||||
getPaginationData = (groupId: string | undefined, subGroupId: string | undefined) => undefined;
|
||||
getIssueLoader = (groupId?: string, subGroupId?: string) => "loaded" as TLoader;
|
||||
getGroupIssueCount = (groupId: string | undefined, subGroupId: string | undefined, isSubGroupCumulative: boolean) =>
|
||||
undefined;
|
||||
removeCycleFromIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {};
|
||||
addIssueToCycle = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
cycleId: string,
|
||||
issueIds: string[],
|
||||
fetchAddedIssues?: boolean
|
||||
) => {};
|
||||
removeIssueFromCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => {};
|
||||
|
||||
removeIssuesFromModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => {};
|
||||
changeModulesInIssue = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
addModuleIds: string[],
|
||||
removeModuleIds: string[]
|
||||
) => {};
|
||||
archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {};
|
||||
archiveBulkIssues = async (workspaceSlug: string, projectId: string, issueIds: string[]) => {};
|
||||
removeBulkIssues = async (workspaceSlug: string, projectId: string, issueIds: string[]) => {};
|
||||
bulkUpdateProperties = async (workspaceSlug: string, projectId: string, data: TBulkOperationsPayload) => {};
|
||||
}
|
||||
313
apps/web/core/store/issue/workspace/filter.store.ts
Normal file
313
apps/web/core/store/issue/workspace/filter.store.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
import { isEmpty, set } from "lodash-es";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// plane imports
|
||||
import type { TSupportedFilterTypeForUpdate } from "@plane/constants";
|
||||
import { EIssueFilterType } from "@plane/constants";
|
||||
import type {
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
TIssueKanbanFilters,
|
||||
IIssueFilters,
|
||||
TIssueParams,
|
||||
TStaticViewTypes,
|
||||
IssuePaginationOptions,
|
||||
TWorkItemFilterExpression,
|
||||
TSupportedFilterForUpdate,
|
||||
} from "@plane/types";
|
||||
import { EIssuesStoreType, EIssueLayoutTypes, STATIC_VIEW_TYPES } from "@plane/types";
|
||||
import { handleIssueQueryParamsByLayout } from "@plane/utils";
|
||||
// services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// local imports
|
||||
import type { IBaseIssueFilterStore, IIssueFilterHelperStore } from "../helpers/issue-filter-helper.store";
|
||||
import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store";
|
||||
import type { IIssueRootStore } from "../root.store";
|
||||
|
||||
type TWorkspaceFilters = TStaticViewTypes | string;
|
||||
|
||||
export type TBaseFilterStore = IBaseIssueFilterStore & IIssueFilterHelperStore;
|
||||
|
||||
export interface IWorkspaceIssuesFilter extends TBaseFilterStore {
|
||||
// fetch action
|
||||
fetchFilters: (workspaceSlug: string, viewId: string) => Promise<void>;
|
||||
updateFilterExpression: (workspaceSlug: string, viewId: string, filters: TWorkItemFilterExpression) => Promise<void>;
|
||||
updateFilters: (
|
||||
workspaceSlug: string,
|
||||
projectId: string | undefined,
|
||||
filterType: TSupportedFilterTypeForUpdate,
|
||||
filters: TSupportedFilterForUpdate,
|
||||
viewId: string
|
||||
) => Promise<void>;
|
||||
//helper action
|
||||
getIssueFilters: (viewId: string | undefined) => IIssueFilters | undefined;
|
||||
getAppliedFilters: (viewId: string) => Partial<Record<TIssueParams, string | boolean>> | undefined;
|
||||
getFilterParams: (
|
||||
options: IssuePaginationOptions,
|
||||
viewId: string,
|
||||
cursor: string | undefined,
|
||||
groupId: string | undefined,
|
||||
subGroupId: string | undefined
|
||||
) => Partial<Record<TIssueParams, string | boolean>>;
|
||||
}
|
||||
|
||||
export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWorkspaceIssuesFilter {
|
||||
// observables
|
||||
filters: { [viewId: string]: IIssueFilters } = {};
|
||||
// root store
|
||||
rootIssueStore;
|
||||
// services
|
||||
issueFilterService;
|
||||
|
||||
constructor(_rootStore: IIssueRootStore) {
|
||||
super();
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
filters: observable,
|
||||
// computed
|
||||
issueFilters: computed,
|
||||
appliedFilters: computed,
|
||||
// fetch actions
|
||||
fetchFilters: action,
|
||||
updateFilters: action,
|
||||
});
|
||||
// root store
|
||||
this.rootIssueStore = _rootStore;
|
||||
// services
|
||||
this.issueFilterService = new WorkspaceService();
|
||||
}
|
||||
|
||||
getIssueFilters = (viewId: string | undefined) => {
|
||||
if (!viewId) return undefined;
|
||||
|
||||
const displayFilters = this.filters[viewId] || undefined;
|
||||
if (isEmpty(displayFilters)) return undefined;
|
||||
|
||||
const _filters: IIssueFilters = this.computedIssueFilters(displayFilters);
|
||||
|
||||
return _filters;
|
||||
};
|
||||
|
||||
getAppliedFilters = (viewId: string | undefined) => {
|
||||
if (!viewId) return undefined;
|
||||
|
||||
const userFilters = this.getIssueFilters(viewId);
|
||||
if (!userFilters) return undefined;
|
||||
|
||||
const filteredParams = handleIssueQueryParamsByLayout(EIssueLayoutTypes.SPREADSHEET, "my_issues");
|
||||
if (!filteredParams) return undefined;
|
||||
|
||||
const filteredRouteParams: Partial<Record<TIssueParams, string | boolean>> = this.computedFilteredParams(
|
||||
userFilters?.richFilters,
|
||||
userFilters?.displayFilters,
|
||||
filteredParams
|
||||
);
|
||||
|
||||
return filteredRouteParams;
|
||||
};
|
||||
|
||||
get issueFilters() {
|
||||
const viewId = this.rootIssueStore.globalViewId;
|
||||
return this.getIssueFilters(viewId);
|
||||
}
|
||||
|
||||
get appliedFilters() {
|
||||
const viewId = this.rootIssueStore.globalViewId;
|
||||
return this.getAppliedFilters(viewId);
|
||||
}
|
||||
|
||||
getFilterParams = computedFn(
|
||||
(
|
||||
options: IssuePaginationOptions,
|
||||
viewId: string,
|
||||
cursor: string | undefined,
|
||||
groupId: string | undefined,
|
||||
subGroupId: string | undefined
|
||||
) => {
|
||||
let filterParams = this.getAppliedFilters(viewId);
|
||||
|
||||
if (!filterParams) {
|
||||
filterParams = {};
|
||||
}
|
||||
|
||||
if (STATIC_VIEW_TYPES.includes(viewId)) {
|
||||
const currentUserId = this.rootIssueStore.currentUserId;
|
||||
const paramForStaticView = this.getFilterConditionBasedOnViews(currentUserId, viewId as TStaticViewTypes);
|
||||
if (paramForStaticView) {
|
||||
filterParams = { ...filterParams, ...paramForStaticView };
|
||||
}
|
||||
}
|
||||
|
||||
const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId);
|
||||
return paginationParams;
|
||||
}
|
||||
);
|
||||
|
||||
fetchFilters = async (workspaceSlug: string, viewId: TWorkspaceFilters) => {
|
||||
let richFilters: TWorkItemFilterExpression;
|
||||
let displayFilters: IIssueDisplayFilterOptions;
|
||||
let displayProperties: IIssueDisplayProperties;
|
||||
let kanbanFilters: TIssueKanbanFilters = {
|
||||
group_by: [],
|
||||
sub_group_by: [],
|
||||
};
|
||||
|
||||
const _filters = this.handleIssuesLocalFilters.get(EIssuesStoreType.GLOBAL, workspaceSlug, undefined, viewId);
|
||||
displayFilters = this.computedDisplayFilters(_filters?.display_filters, {
|
||||
layout: EIssueLayoutTypes.SPREADSHEET,
|
||||
order_by: "-created_at",
|
||||
});
|
||||
displayProperties = this.computedDisplayProperties(_filters?.display_properties);
|
||||
kanbanFilters = {
|
||||
group_by: _filters?.kanban_filters?.group_by || [],
|
||||
sub_group_by: _filters?.kanban_filters?.sub_group_by || [],
|
||||
};
|
||||
|
||||
// Get the view details if the view is not a static view
|
||||
if (STATIC_VIEW_TYPES.includes(viewId) === false) {
|
||||
const _filters = await this.issueFilterService.getViewDetails(workspaceSlug, viewId);
|
||||
richFilters = _filters?.rich_filters;
|
||||
displayFilters = this.computedDisplayFilters(_filters?.display_filters, {
|
||||
layout: EIssueLayoutTypes.SPREADSHEET,
|
||||
order_by: "-created_at",
|
||||
});
|
||||
displayProperties = this.computedDisplayProperties(_filters?.display_properties);
|
||||
}
|
||||
|
||||
// override existing order by if ordered by manual sort_order
|
||||
if (displayFilters.order_by === "sort_order") {
|
||||
displayFilters.order_by = "-created_at";
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
set(this.filters, [viewId, "richFilters"], richFilters);
|
||||
set(this.filters, [viewId, "displayFilters"], displayFilters);
|
||||
set(this.filters, [viewId, "displayProperties"], displayProperties);
|
||||
set(this.filters, [viewId, "kanbanFilters"], kanbanFilters);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* NOTE: This method is designed as a fallback function for the work item filter store.
|
||||
* Only use this method directly when initializing filter instances.
|
||||
* For regular filter updates, use this method as a fallback function for the work item filter store methods instead.
|
||||
*/
|
||||
updateFilterExpression: IWorkspaceIssuesFilter["updateFilterExpression"] = async (workspaceSlug, viewId, filters) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
set(this.filters, [viewId, "richFilters"], filters);
|
||||
});
|
||||
|
||||
this.rootIssueStore.workspaceIssues.fetchIssuesWithExistingPagination(workspaceSlug, viewId, "mutation");
|
||||
} catch (error) {
|
||||
console.log("error while updating rich filters", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
updateFilters: IWorkspaceIssuesFilter["updateFilters"] = async (workspaceSlug, projectId, type, filters, viewId) => {
|
||||
try {
|
||||
const issueFilters = this.getIssueFilters(viewId);
|
||||
|
||||
if (!issueFilters) return;
|
||||
|
||||
const _filters = {
|
||||
richFilters: issueFilters.richFilters as TWorkItemFilterExpression,
|
||||
displayFilters: issueFilters.displayFilters as IIssueDisplayFilterOptions,
|
||||
displayProperties: issueFilters.displayProperties as IIssueDisplayProperties,
|
||||
kanbanFilters: issueFilters.kanbanFilters as TIssueKanbanFilters,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
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 state 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 = "state";
|
||||
updatedDisplayFilters.group_by = "state";
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
Object.keys(updatedDisplayFilters).forEach((_key) => {
|
||||
set(
|
||||
this.filters,
|
||||
[viewId, "displayFilters", _key],
|
||||
updatedDisplayFilters[_key as keyof IIssueDisplayFilterOptions]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
this.rootIssueStore.workspaceIssues.fetchIssuesWithExistingPagination(workspaceSlug, viewId, "mutation");
|
||||
|
||||
if (["all-issues", "assigned", "created", "subscribed"].includes(viewId))
|
||||
this.handleIssuesLocalFilters.set(EIssuesStoreType.GLOBAL, type, workspaceSlug, undefined, viewId, {
|
||||
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,
|
||||
[viewId, "displayProperties", _key],
|
||||
updatedDisplayProperties[_key as keyof IIssueDisplayProperties]
|
||||
);
|
||||
});
|
||||
if (["all-issues", "assigned", "created", "subscribed"].includes(viewId))
|
||||
this.handleIssuesLocalFilters.set(EIssuesStoreType.GLOBAL, type, workspaceSlug, undefined, viewId, {
|
||||
display_properties: _filters.displayProperties,
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case EIssueFilterType.KANBAN_FILTERS: {
|
||||
const updatedKanbanFilters = filters as TIssueKanbanFilters;
|
||||
_filters.kanbanFilters = { ..._filters.kanbanFilters, ...updatedKanbanFilters };
|
||||
|
||||
const currentUserId = this.rootIssueStore.currentUserId;
|
||||
if (currentUserId)
|
||||
this.handleIssuesLocalFilters.set(EIssuesStoreType.GLOBAL, type, workspaceSlug, undefined, viewId, {
|
||||
kanban_filters: _filters.kanbanFilters,
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
Object.keys(updatedKanbanFilters).forEach((_key) => {
|
||||
set(
|
||||
this.filters,
|
||||
[viewId, "kanbanFilters", _key],
|
||||
updatedKanbanFilters[_key as keyof TIssueKanbanFilters]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
if (viewId) this.fetchFilters(workspaceSlug, viewId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
2
apps/web/core/store/issue/workspace/index.ts
Normal file
2
apps/web/core/store/issue/workspace/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./filter.store";
|
||||
export * from "./issue.store";
|
||||
181
apps/web/core/store/issue/workspace/issue.store.ts
Normal file
181
apps/web/core/store/issue/workspace/issue.store.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { action, makeObservable, runInAction } from "mobx";
|
||||
// base class
|
||||
import type {
|
||||
IssuePaginationOptions,
|
||||
TBulkOperationsPayload,
|
||||
TIssue,
|
||||
TIssuesResponse,
|
||||
TLoader,
|
||||
ViewFlags,
|
||||
} from "@plane/types";
|
||||
// services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// types
|
||||
import type { IBaseIssuesStore } from "../helpers/base-issues.store";
|
||||
import { BaseIssuesStore } from "../helpers/base-issues.store";
|
||||
import type { IIssueRootStore } from "../root.store";
|
||||
import type { IWorkspaceIssuesFilter } from "./filter.store";
|
||||
|
||||
export interface IWorkspaceIssues extends IBaseIssuesStore {
|
||||
// observable
|
||||
viewFlags: ViewFlags;
|
||||
// actions
|
||||
fetchIssues: (
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
loadType: TLoader,
|
||||
options: IssuePaginationOptions
|
||||
) => Promise<TIssuesResponse | undefined>;
|
||||
fetchIssuesWithExistingPagination: (
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
loadType: TLoader
|
||||
) => Promise<TIssuesResponse | undefined>;
|
||||
fetchNextIssues: (
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
groupId?: string,
|
||||
subGroupId?: string
|
||||
) => Promise<TIssuesResponse | undefined>;
|
||||
|
||||
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>;
|
||||
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||
archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
removeBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise<void>;
|
||||
archiveBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise<void>;
|
||||
bulkUpdateProperties: (workspaceSlug: string, projectId: string, data: TBulkOperationsPayload) => Promise<void>;
|
||||
|
||||
quickAddIssue: undefined;
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
export class WorkspaceIssues extends BaseIssuesStore implements IWorkspaceIssues {
|
||||
viewFlags = {
|
||||
enableQuickAdd: true,
|
||||
enableIssueCreation: true,
|
||||
enableInlineEditing: true,
|
||||
};
|
||||
// service
|
||||
workspaceService;
|
||||
// filterStore
|
||||
issueFilterStore;
|
||||
|
||||
constructor(_rootStore: IIssueRootStore, issueFilterStore: IWorkspaceIssuesFilter) {
|
||||
super(_rootStore, issueFilterStore);
|
||||
|
||||
makeObservable(this, {
|
||||
// action
|
||||
fetchIssues: action,
|
||||
fetchNextIssues: action,
|
||||
fetchIssuesWithExistingPagination: action,
|
||||
});
|
||||
// services
|
||||
this.workspaceService = new WorkspaceService();
|
||||
// filter store
|
||||
this.issueFilterStore = issueFilterStore;
|
||||
}
|
||||
|
||||
fetchParentStats = () => {};
|
||||
|
||||
/** */
|
||||
updateParentStats = () => {};
|
||||
|
||||
/**
|
||||
* This method is called to fetch the first issues of pagination
|
||||
* @param workspaceSlug
|
||||
* @param viewId
|
||||
* @param loadType
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
fetchIssues = async (
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
loadType: TLoader,
|
||||
options: IssuePaginationOptions,
|
||||
isExistingPaginationOptions: boolean = false
|
||||
) => {
|
||||
try {
|
||||
// set loader and clear store
|
||||
runInAction(() => {
|
||||
this.setLoader(loadType);
|
||||
});
|
||||
this.clear(!isExistingPaginationOptions);
|
||||
|
||||
// get params from pagination options
|
||||
const params = this.issueFilterStore?.getFilterParams(options, viewId, undefined, undefined, undefined);
|
||||
// call the fetch issues API with the params
|
||||
const response = await this.workspaceService.getViewIssues(workspaceSlug, params, {
|
||||
signal: this.controller.signal,
|
||||
});
|
||||
|
||||
// after fetching issues, call the base method to process the response further
|
||||
this.onfetchIssues(response, options, workspaceSlug, undefined, undefined, !isExistingPaginationOptions);
|
||||
return response;
|
||||
} catch (error) {
|
||||
// set loader to undefined if errored out
|
||||
this.setLoader(undefined);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This method is called subsequent pages of pagination
|
||||
* if groupId/subgroupId is provided, only that specific group's next page is fetched
|
||||
* else all the groups' next page is fetched
|
||||
* @param workspaceSlug
|
||||
* @param viewId
|
||||
* @param groupId
|
||||
* @param subGroupId
|
||||
* @returns
|
||||
*/
|
||||
fetchNextIssues = async (workspaceSlug: string, viewId: string, groupId?: string, subGroupId?: string) => {
|
||||
const cursorObject = this.getPaginationData(groupId, subGroupId);
|
||||
// if there are no pagination options and the next page results do not exist the return
|
||||
if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return;
|
||||
try {
|
||||
// set Loader
|
||||
this.setLoader("pagination", groupId, subGroupId);
|
||||
|
||||
// get params from stored pagination options
|
||||
const params = this.issueFilterStore?.getFilterParams(
|
||||
this.paginationOptions,
|
||||
viewId,
|
||||
this.getNextCursor(groupId, subGroupId),
|
||||
groupId,
|
||||
subGroupId
|
||||
);
|
||||
// call the fetch issues API with the params for next page in issues
|
||||
const response = await this.workspaceService.getViewIssues(workspaceSlug, params);
|
||||
|
||||
// after the next page of issues are fetched, call the base method to process the response
|
||||
this.onfetchNexIssues(response, groupId, subGroupId);
|
||||
return response;
|
||||
} catch (error) {
|
||||
// set Loader as undefined if errored out
|
||||
this.setLoader(undefined, groupId, subGroupId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This Method exists to fetch the first page of the issues with the existing stored pagination
|
||||
* This is useful for refetching when filters, groupBy, orderBy etc changes
|
||||
* @param workspaceSlug
|
||||
* @param viewId
|
||||
* @param loadType
|
||||
* @returns
|
||||
*/
|
||||
fetchIssuesWithExistingPagination = async (workspaceSlug: string, viewId: string, loadType: TLoader) => {
|
||||
if (!this.paginationOptions) return;
|
||||
return await this.fetchIssues(workspaceSlug, viewId, loadType, this.paginationOptions, true);
|
||||
};
|
||||
|
||||
// Using aliased names as they cannot be overridden in other stores
|
||||
archiveBulkIssues = this.bulkArchiveIssues;
|
||||
updateIssue = this.issueUpdate;
|
||||
archiveIssue = this.issueArchive;
|
||||
|
||||
// Setting them as undefined as they can not performed on workspace issues
|
||||
quickAddIssue = undefined;
|
||||
}
|
||||
Reference in New Issue
Block a user