mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
1120 lines
37 KiB
TypeScript
1120 lines
37 KiB
TypeScript
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
|
import update from "lodash/update";
|
|
import uniq from "lodash/uniq";
|
|
import concat from "lodash/concat";
|
|
import pull from "lodash/pull";
|
|
import orderBy from "lodash/orderBy";
|
|
import clone from "lodash/clone";
|
|
import indexOf from "lodash/indexOf";
|
|
import isEmpty from "lodash/isEmpty";
|
|
// types
|
|
import {
|
|
TIssue,
|
|
TIssueGroupByOptions,
|
|
TIssueOrderByOptions,
|
|
TGroupedIssues,
|
|
TSubGroupedIssues,
|
|
TLoader,
|
|
IssuePaginationOptions,
|
|
TIssuesResponse,
|
|
TIssues,
|
|
TIssuePaginationData,
|
|
TGroupedIssueCount,
|
|
TPaginationData,
|
|
} from "@plane/types";
|
|
import { IIssueRootStore } from "../root.store";
|
|
import { IBaseIssueFilterStore } from "./issue-filter-helper.store";
|
|
// constants
|
|
import { ISSUE_PRIORITIES } from "constants/issue";
|
|
import { STATE_GROUPS } from "constants/state";
|
|
// helpers
|
|
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
|
// services
|
|
import { IssueArchiveService, IssueDraftService, IssueService } from "services/issue";
|
|
import set from "lodash/set";
|
|
import { get } from "lodash";
|
|
import { computedFn } from "mobx-utils";
|
|
import isEqual from "date-fns/isEqual";
|
|
|
|
export type TIssueDisplayFilterOptions = Exclude<TIssueGroupByOptions, null> | "target_date";
|
|
|
|
export enum EIssueGroupedAction {
|
|
ADD = "ADD",
|
|
DELETE = "DELETE",
|
|
REORDER = "REORDER",
|
|
}
|
|
|
|
export const ALL_ISSUES = "All Issues";
|
|
|
|
export interface IBaseIssuesStore {
|
|
// observable
|
|
loader: TLoader;
|
|
|
|
groupedIssueIds: TGroupedIssues | TSubGroupedIssues | undefined;
|
|
groupedIssueCount: TGroupedIssueCount;
|
|
issuePaginationData: TIssuePaginationData;
|
|
|
|
//actions
|
|
removeIssue(workspaceSlug: string, projectId: string, issueId: string): Promise<void>;
|
|
// helper methods
|
|
issueDisplayFiltersDefaultData(groupBy: string | null): string[];
|
|
issuesSortWithOrderBy(issueIds: string[], key: Partial<TIssueOrderByOptions>): string[];
|
|
getGroupArray(value: boolean | number | string | string[] | null, isDate?: boolean): string[];
|
|
getPaginationData(groupId: string | undefined, subGroupId: string | undefined): TPaginationData | undefined;
|
|
getGroupIssueCount: (
|
|
groupId: string | undefined,
|
|
subGroupId: string | undefined,
|
|
isSubGroupCumulative: boolean
|
|
) => number | undefined;
|
|
}
|
|
|
|
const ISSUE_FILTER_DEFAULT_DATA: Record<TIssueDisplayFilterOptions, keyof TIssue> = {
|
|
project: "project_id",
|
|
state: "state_id",
|
|
"state_detail.group": "state_id" as keyof TIssue, // state_detail.group is only being used for state_group display,
|
|
priority: "priority",
|
|
labels: "label_ids",
|
|
created_by: "created_by",
|
|
assignees: "assignee_ids",
|
|
target_date: "target_date",
|
|
cycle: "cycle_id",
|
|
module: "module_ids",
|
|
};
|
|
|
|
const ISSUE_ORDERBY_KEY: Record<TIssueOrderByOptions, keyof TIssue> = {
|
|
created_at: "created_at",
|
|
"-created_at": "created_at",
|
|
updated_at: "updated_at",
|
|
"-updated_at": "updated_at",
|
|
priority: "priority",
|
|
"-priority": "priority",
|
|
sort_order: "sort_order",
|
|
state__name: "state_id",
|
|
"-state__name": "state_id",
|
|
assignees__first_name: "assignee_ids",
|
|
"-assignees__first_name": "assignee_ids",
|
|
labels__name: "label_ids",
|
|
"-labels__name": "label_ids",
|
|
issue_module__module__name: "module_ids",
|
|
"-issue_module__module__name": "module_ids",
|
|
issue_cycle__cycle__name: "cycle_id",
|
|
"-issue_cycle__cycle__name": "cycle_id",
|
|
target_date: "target_date",
|
|
"-target_date": "target_date",
|
|
estimate_point: "estimate_point",
|
|
"-estimate_point": "estimate_point",
|
|
start_date: "start_date",
|
|
"-start_date": "start_date",
|
|
link_count: "link_count",
|
|
"-link_count": "link_count",
|
|
attachment_count: "attachment_count",
|
|
"-attachment_count": "attachment_count",
|
|
sub_issues_count: "sub_issues_count",
|
|
"-sub_issues_count": "sub_issues_count",
|
|
};
|
|
|
|
export class BaseIssuesStore implements IBaseIssuesStore {
|
|
loader: TLoader = "init-loader";
|
|
groupedIssueIds: TIssues | undefined = undefined;
|
|
issuePaginationData: TIssuePaginationData = {};
|
|
|
|
groupedIssueCount: TGroupedIssueCount = {};
|
|
|
|
paginationOptions: IssuePaginationOptions | undefined = undefined;
|
|
|
|
isArchived: boolean;
|
|
|
|
// services
|
|
issueService;
|
|
issueArchiveService;
|
|
issueDraftService;
|
|
// root store
|
|
rootIssueStore;
|
|
issueFilterStore;
|
|
|
|
constructor(_rootStore: IIssueRootStore, issueFilterStore: IBaseIssueFilterStore, isArchived = false) {
|
|
makeObservable(this, {
|
|
// observable
|
|
loader: observable.ref,
|
|
groupedIssueIds: observable,
|
|
issuePaginationData: observable,
|
|
groupedIssueCount: observable,
|
|
|
|
paginationOptions: observable,
|
|
// computed
|
|
orderBy: computed,
|
|
groupBy: computed,
|
|
subGroupBy: computed,
|
|
issueGroupKey: computed,
|
|
issueSubGroupKey: computed,
|
|
// action
|
|
storePreviousPaginationValues: action.bound,
|
|
|
|
onfetchIssues: action.bound,
|
|
onfetchNexIssues: action.bound,
|
|
clear: action.bound,
|
|
getPaginationData: action.bound,
|
|
addIssue: action.bound,
|
|
removeIssueFromList: action.bound,
|
|
|
|
createIssue: action,
|
|
updateIssue: action,
|
|
createDraftIssue: action,
|
|
updateDraftIssue: action,
|
|
issueQuickAdd: action.bound,
|
|
removeIssue: action,
|
|
archiveIssue: action,
|
|
removeBulkIssues: action,
|
|
});
|
|
this.rootIssueStore = _rootStore;
|
|
this.issueFilterStore = issueFilterStore;
|
|
|
|
this.isArchived = isArchived;
|
|
|
|
this.issueService = new IssueService();
|
|
this.issueArchiveService = new IssueArchiveService();
|
|
this.issueDraftService = new IssueDraftService();
|
|
}
|
|
|
|
get orderBy() {
|
|
const displayFilters = this.issueFilterStore?.issueFilters?.displayFilters;
|
|
if (!displayFilters) return;
|
|
|
|
return displayFilters?.order_by;
|
|
}
|
|
|
|
get groupBy() {
|
|
const displayFilters = this.issueFilterStore?.issueFilters?.displayFilters;
|
|
if (!displayFilters) return;
|
|
|
|
const layout = displayFilters?.layout;
|
|
|
|
return layout === "calendar"
|
|
? "target_date"
|
|
: ["list", "kanban"]?.includes(layout)
|
|
? displayFilters?.group_by
|
|
: undefined;
|
|
}
|
|
|
|
get subGroupBy() {
|
|
const displayFilters = this.issueFilterStore?.issueFilters?.displayFilters;
|
|
if (!displayFilters || displayFilters.group_by === displayFilters.sub_group_by) return;
|
|
|
|
return displayFilters?.layout === "kanban" ? displayFilters?.sub_group_by : undefined;
|
|
}
|
|
|
|
get orderByKey() {
|
|
const orderBy = this.orderBy;
|
|
if (!orderBy) return;
|
|
|
|
return ISSUE_ORDERBY_KEY[orderBy];
|
|
}
|
|
|
|
get issueGroupKey() {
|
|
const groupBy = this.groupBy;
|
|
|
|
if (!groupBy) return;
|
|
|
|
return ISSUE_FILTER_DEFAULT_DATA[groupBy];
|
|
}
|
|
|
|
get issueSubGroupKey() {
|
|
const subGroupBy = this.subGroupBy;
|
|
|
|
if (!subGroupBy) return;
|
|
|
|
return ISSUE_FILTER_DEFAULT_DATA[subGroupBy];
|
|
}
|
|
|
|
onfetchIssues(issuesResponse: TIssuesResponse, options: IssuePaginationOptions) {
|
|
const { issueList, groupedIssues, groupedIssueCount } = this.processIssueResponse(issuesResponse);
|
|
|
|
runInAction(() => {
|
|
this.groupedIssueIds = groupedIssues;
|
|
this.groupedIssueCount = groupedIssueCount;
|
|
this.loader = undefined;
|
|
});
|
|
|
|
this.rootIssueStore.issues.addIssue(issueList);
|
|
|
|
this.storePreviousPaginationValues(issuesResponse, options);
|
|
}
|
|
|
|
onfetchNexIssues(issuesResponse: TIssuesResponse, groupId?: string, subGroupId?: string) {
|
|
const { issueList, groupedIssues, groupedIssueCount } = this.processIssueResponse(issuesResponse);
|
|
|
|
this.rootIssueStore.issues.addIssue(issueList);
|
|
|
|
runInAction(() => {
|
|
this.updateGroupedIssueIds(groupedIssues, groupedIssueCount, groupId, subGroupId);
|
|
this.loader = undefined;
|
|
});
|
|
|
|
this.storePreviousPaginationValues(issuesResponse, undefined, groupId, subGroupId);
|
|
}
|
|
|
|
updateGroupedIssueIds(
|
|
groupedIssues: TIssues,
|
|
groupedIssueCount: TGroupedIssueCount,
|
|
groupId?: string,
|
|
subGroupId?: string
|
|
) {
|
|
if (groupId && groupedIssues[ALL_ISSUES] && Array.isArray(groupedIssues[ALL_ISSUES])) {
|
|
const issueGroup = groupedIssues[ALL_ISSUES];
|
|
const issueGroupCount = groupedIssueCount[ALL_ISSUES];
|
|
const issuesPath = [groupId];
|
|
|
|
if (subGroupId) issuesPath.push(subGroupId);
|
|
|
|
set(this.groupedIssueCount, [this.getGroupKey(groupId, subGroupId)], issueGroupCount);
|
|
|
|
this.updateIssueGroup(issueGroup, issuesPath);
|
|
return;
|
|
}
|
|
|
|
set(this.groupedIssueCount, [ALL_ISSUES], groupedIssueCount[ALL_ISSUES]);
|
|
|
|
for (const groupId in groupedIssues) {
|
|
const issueGroup = groupedIssues[groupId];
|
|
const issueGroupCount = groupedIssueCount[groupId];
|
|
|
|
set(this.groupedIssueCount, [groupId], issueGroupCount);
|
|
|
|
const shouldContinue = this.updateIssueGroup(issueGroup, [groupId]);
|
|
if (shouldContinue) continue;
|
|
|
|
for (const subGroupId in issueGroup) {
|
|
const issueSubGroup = (issueGroup as TGroupedIssues)[subGroupId];
|
|
const issueSubGroupCount = groupedIssueCount[subGroupId];
|
|
|
|
set(this.groupedIssueCount, [this.getGroupKey(groupId, subGroupId)], issueSubGroupCount);
|
|
this.updateIssueGroup(issueSubGroup, [groupId, subGroupId]);
|
|
}
|
|
}
|
|
}
|
|
|
|
updateIssueGroup(groupedIssueIds: TGroupedIssues | string[], issuePath: string[]): boolean {
|
|
if (!groupedIssueIds) return true;
|
|
|
|
if (groupedIssueIds && Array.isArray(groupedIssueIds)) {
|
|
update(this, ["groupedIssueIds", ...issuePath], (issueIds: string[] = []) => {
|
|
return this.issuesSortWithOrderBy(uniq(concat(issueIds, groupedIssueIds as string[])), this.orderBy);
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async createIssue(
|
|
workspaceSlug: string,
|
|
projectId: string,
|
|
data: Partial<TIssue>,
|
|
id?: string,
|
|
shouldAddStore = true
|
|
) {
|
|
try {
|
|
const response = await this.issueService.createIssue(workspaceSlug, projectId, data);
|
|
|
|
if (shouldAddStore) this.addIssue(response);
|
|
|
|
return response;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async updateIssue(workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) {
|
|
const issueBeforeUpdate = clone(this.rootIssueStore.issues.getIssueById(issueId));
|
|
try {
|
|
this.rootIssueStore.issues.updateIssue(issueId, data);
|
|
this.updateIssueList({ ...issueBeforeUpdate, ...data } as TIssue, issueBeforeUpdate);
|
|
|
|
await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data);
|
|
} catch (error) {
|
|
this.rootIssueStore.issues.updateIssue(issueId, issueBeforeUpdate ?? {});
|
|
this.updateIssueList(issueBeforeUpdate, { ...issueBeforeUpdate, ...data } as TIssue);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async createDraftIssue(workspaceSlug: string, projectId: string, data: Partial<TIssue>) {
|
|
try {
|
|
const response = await this.issueDraftService.createDraftIssue(workspaceSlug, projectId, data);
|
|
|
|
this.addIssue(response);
|
|
|
|
return response;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async updateDraftIssue(workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) {
|
|
const issueBeforeUpdate = clone(this.rootIssueStore.issues.getIssueById(issueId));
|
|
try {
|
|
this.rootIssueStore.issues.updateIssue(issueId, data);
|
|
this.updateIssueList({ ...issueBeforeUpdate, ...data } as TIssue, issueBeforeUpdate);
|
|
|
|
await this.issueDraftService.updateDraftIssue(workspaceSlug, projectId, issueId, data);
|
|
} catch (error) {
|
|
this.rootIssueStore.issues.updateIssue(issueId, issueBeforeUpdate ?? {});
|
|
this.updateIssueList(issueBeforeUpdate, { ...issueBeforeUpdate, ...data } as TIssue);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async removeIssue(workspaceSlug: string, projectId: string, issueId: string) {
|
|
try {
|
|
await this.issueService.deleteIssue(workspaceSlug, projectId, issueId);
|
|
|
|
runInAction(() => {
|
|
this.removeIssueFromList(issueId);
|
|
});
|
|
|
|
this.rootIssueStore.issues.removeIssue(issueId);
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async archiveIssue(workspaceSlug: string, projectId: string, issueId: string) {
|
|
try {
|
|
const response = await this.issueArchiveService.archiveIssue(workspaceSlug, projectId, issueId);
|
|
|
|
runInAction(() => {
|
|
this.rootIssueStore.issues.updateIssue(issueId, {
|
|
archived_at: response.archived_at,
|
|
});
|
|
this.removeIssueFromList(issueId);
|
|
});
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async issueQuickAdd(workspaceSlug: string, projectId: string, data: TIssue) {
|
|
try {
|
|
this.addIssue(data);
|
|
|
|
const response = await this.createIssue(workspaceSlug, projectId, data);
|
|
return response;
|
|
} catch (error) {
|
|
throw error;
|
|
} finally {
|
|
runInAction(() => {
|
|
this.removeIssueFromList(data.id);
|
|
this.rootIssueStore.issues.removeIssue(data.id);
|
|
});
|
|
}
|
|
}
|
|
|
|
async removeBulkIssues(workspaceSlug: string, projectId: string, issueIds: string[]) {
|
|
try {
|
|
const response = await this.issueService.bulkDeleteIssues(workspaceSlug, projectId, { issue_ids: issueIds });
|
|
|
|
runInAction(() => {
|
|
issueIds.forEach((issueId) => {
|
|
this.removeIssueFromList(issueId);
|
|
this.rootIssueStore.issues.removeIssue(issueId);
|
|
});
|
|
});
|
|
return response;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
addIssue(issue: TIssue) {
|
|
runInAction(() => {
|
|
this.rootIssueStore.issues.addIssue([issue]);
|
|
});
|
|
|
|
this.updateIssueList(issue, undefined, EIssueGroupedAction.ADD);
|
|
}
|
|
|
|
clear() {
|
|
runInAction(() => {
|
|
this.groupedIssueIds = undefined;
|
|
this.issuePaginationData = {};
|
|
this.groupedIssueCount = {};
|
|
this.paginationOptions = undefined;
|
|
});
|
|
}
|
|
|
|
addIssueToList(issueId: string) {
|
|
const issue = this.rootIssueStore.issues.getIssueById(issueId);
|
|
this.updateIssueList(issue, undefined, EIssueGroupedAction.ADD);
|
|
}
|
|
|
|
removeIssueFromList(issueId: string) {
|
|
const issue = this.rootIssueStore.issues.getIssueById(issueId);
|
|
this.updateIssueList(issue, undefined, EIssueGroupedAction.DELETE);
|
|
}
|
|
|
|
updateIssueList(issue?: TIssue, issueBeforeUpdate?: TIssue, action?: EIssueGroupedAction) {
|
|
if (!issue) return;
|
|
const issueUpdates = this.getUpdateDetails(issue, issueBeforeUpdate, action);
|
|
runInAction(() => {
|
|
for (const issueUpdate of issueUpdates) {
|
|
//if update is add, add it at a particular path
|
|
if (issueUpdate.action === EIssueGroupedAction.ADD) {
|
|
update(this, ["groupedIssueIds", ...issueUpdate.path], (issueIds: string[] = []) => {
|
|
return this.issuesSortWithOrderBy(uniq(concat(issueIds, issue.id)), this.orderBy);
|
|
});
|
|
this.updateIssueCount(issueUpdate.path, 1);
|
|
}
|
|
|
|
//if update is delete, remove it at a particular path
|
|
if (issueUpdate.action === EIssueGroupedAction.DELETE) {
|
|
update(this, ["groupedIssueIds", ...issueUpdate.path], (issueIds: string[] = []) => {
|
|
return pull(issueIds, issue.id);
|
|
});
|
|
this.updateIssueCount(issueUpdate.path, -1);
|
|
}
|
|
|
|
//if update is reorder, reorder it at a particular path
|
|
if (issueUpdate.action === EIssueGroupedAction.REORDER) {
|
|
update(this, ["groupedIssueIds", ...issueUpdate.path], (issueIds: string[] = []) => {
|
|
return this.issuesSortWithOrderBy(issueIds, this.orderBy);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
updateIssueCount(path: string[], increment: number) {
|
|
const [groupId, subGroupId] = path;
|
|
|
|
if (subGroupId && groupId) {
|
|
const groupKey = this.getGroupKey(groupId, subGroupId);
|
|
const subGroupIssueCount = get(this.groupedIssueCount, groupKey);
|
|
|
|
set(this.groupedIssueCount, groupKey, subGroupIssueCount + increment);
|
|
}
|
|
|
|
if (groupId) {
|
|
const groupIssueCount = get(this.groupedIssueCount, [groupId]);
|
|
|
|
set(this.groupedIssueCount, groupId, groupIssueCount + increment);
|
|
}
|
|
|
|
if (groupId !== ALL_ISSUES) {
|
|
const totalIssueCount = get(this.groupedIssueCount, [ALL_ISSUES]);
|
|
|
|
set(this.groupedIssueCount, ALL_ISSUES, totalIssueCount + increment);
|
|
}
|
|
}
|
|
|
|
getUpdateDetails = (
|
|
issue?: Partial<TIssue>,
|
|
issueBeforeUpdate?: Partial<TIssue>,
|
|
action?: EIssueGroupedAction
|
|
): { path: string[]; action: EIssueGroupedAction }[] => {
|
|
const orderByUpdates = this.getOrderByUpdateDetails(issue, issueBeforeUpdate);
|
|
if (!this.issueGroupKey || !issue)
|
|
return action ? [{ path: [ALL_ISSUES], action }, ...orderByUpdates] : orderByUpdates;
|
|
|
|
const groupActionsArray = this.getDifference(
|
|
this.getArrayStringArray(issue[this.issueGroupKey], this.groupBy),
|
|
this.getArrayStringArray(issueBeforeUpdate?.[this.issueGroupKey], this.groupBy)
|
|
);
|
|
|
|
if (!this.issueSubGroupKey)
|
|
return [
|
|
...this.getGroupIssueKeyActions(
|
|
groupActionsArray[EIssueGroupedAction.ADD],
|
|
groupActionsArray[EIssueGroupedAction.DELETE]
|
|
),
|
|
...orderByUpdates,
|
|
];
|
|
|
|
const subGroupActionsArray = this.getDifference(
|
|
this.getArrayStringArray(issue[this.issueSubGroupKey], this.subGroupBy),
|
|
this.getArrayStringArray(issueBeforeUpdate?.[this.issueSubGroupKey], this.subGroupBy)
|
|
);
|
|
|
|
return [
|
|
...this.getSubGroupIssueKeyActions(
|
|
groupActionsArray,
|
|
subGroupActionsArray,
|
|
this.getArrayStringArray(issueBeforeUpdate?.[this.issueGroupKey] ?? issue[this.issueGroupKey], this.groupBy),
|
|
this.getArrayStringArray(issue[this.issueGroupKey], this.groupBy),
|
|
this.getArrayStringArray(
|
|
issueBeforeUpdate?.[this.issueSubGroupKey] ?? issue[this.issueSubGroupKey],
|
|
this.subGroupBy
|
|
),
|
|
this.getArrayStringArray(issue[this.issueSubGroupKey], this.subGroupBy)
|
|
),
|
|
...orderByUpdates,
|
|
];
|
|
};
|
|
|
|
getOrderByUpdateDetails(issue: Partial<TIssue> | undefined, issueBeforeUpdate: Partial<TIssue> | undefined) {
|
|
if (
|
|
!issue ||
|
|
!issueBeforeUpdate ||
|
|
!this.orderByKey ||
|
|
isEqual(issue[this.orderByKey], issueBeforeUpdate[this.orderByKey])
|
|
)
|
|
return [];
|
|
|
|
if (!this.issueGroupKey) return [{ path: [ALL_ISSUES], action: EIssueGroupedAction.REORDER }];
|
|
|
|
const issueKeyActions = [];
|
|
const groupByValues = this.getArrayStringArray(issue[this.issueGroupKey]);
|
|
|
|
if (!this.issueSubGroupKey) {
|
|
for (const groupKey of groupByValues) {
|
|
issueKeyActions.push({ path: [groupKey], action: EIssueGroupedAction.REORDER });
|
|
}
|
|
|
|
return issueKeyActions;
|
|
}
|
|
|
|
const subGroupByValues = this.getArrayStringArray(issue[this.issueSubGroupKey]);
|
|
|
|
for (const groupKey of groupByValues) {
|
|
for (const subGroupKey of subGroupByValues) {
|
|
issueKeyActions.push({ path: [groupKey, subGroupKey], action: EIssueGroupedAction.REORDER });
|
|
}
|
|
}
|
|
|
|
return issueKeyActions;
|
|
}
|
|
|
|
getArrayStringArray = (
|
|
value: string | string[] | undefined | null,
|
|
groupByKey?: TIssueGroupByOptions | undefined
|
|
): string[] => {
|
|
if (!value) return [];
|
|
if (Array.isArray(value)) return value;
|
|
|
|
if (groupByKey === "state_detail.group") {
|
|
return [this.rootIssueStore.rootStore.state.stateMap?.[value]?.group];
|
|
}
|
|
|
|
return [value];
|
|
};
|
|
|
|
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 = [];
|
|
|
|
for (const addKey of groupActionsArray[EIssueGroupedAction.ADD]) {
|
|
for (const subGroupProperty of currentIssueSubGroupProperties) {
|
|
issueKeyActions.push({ path: [addKey, subGroupProperty], action: EIssueGroupedAction.ADD });
|
|
}
|
|
}
|
|
|
|
for (const deleteKey of groupActionsArray[EIssueGroupedAction.DELETE]) {
|
|
for (const subGroupProperty of previousIssueSubGroupProperties) {
|
|
issueKeyActions.push({
|
|
path: [deleteKey, subGroupProperty],
|
|
action: EIssueGroupedAction.DELETE,
|
|
});
|
|
}
|
|
}
|
|
|
|
for (const addKey of subGroupActionsArray[EIssueGroupedAction.ADD]) {
|
|
for (const groupProperty of currentIssueGroupProperties) {
|
|
issueKeyActions.push({ path: [groupProperty, addKey], action: EIssueGroupedAction.ADD });
|
|
}
|
|
}
|
|
|
|
for (const deleteKey of subGroupActionsArray[EIssueGroupedAction.DELETE]) {
|
|
for (const groupProperty of previousIssueGroupProperties) {
|
|
issueKeyActions.push({
|
|
path: [groupProperty, deleteKey],
|
|
action: EIssueGroupedAction.DELETE,
|
|
});
|
|
}
|
|
}
|
|
|
|
return issueKeyActions;
|
|
};
|
|
|
|
getGroupIssueKeyActions = (
|
|
addArray: string[],
|
|
deleteArray: string[]
|
|
): { path: string[]; action: EIssueGroupedAction }[] => {
|
|
const issueKeyActions = [];
|
|
|
|
for (const addKey of addArray) {
|
|
issueKeyActions.push({ path: [addKey], action: EIssueGroupedAction.ADD });
|
|
}
|
|
|
|
for (const deleteKey of deleteArray) {
|
|
issueKeyActions.push({ path: [deleteKey], action: EIssueGroupedAction.DELETE });
|
|
}
|
|
|
|
return issueKeyActions;
|
|
};
|
|
|
|
getDifference = (
|
|
current: string[],
|
|
previous: string[],
|
|
action?: EIssueGroupedAction.ADD | EIssueGroupedAction.DELETE
|
|
): { [EIssueGroupedAction.ADD]: string[]; [EIssueGroupedAction.DELETE]: string[] } => {
|
|
const ADD = [];
|
|
const DELETE = [];
|
|
for (const currentValue of current) {
|
|
if (previous.includes(currentValue)) continue;
|
|
ADD.push(currentValue);
|
|
}
|
|
|
|
for (const previousValue of previous) {
|
|
if (current.includes(previousValue)) continue;
|
|
DELETE.push(previousValue);
|
|
}
|
|
|
|
if (!action) return { [EIssueGroupedAction.ADD]: ADD, [EIssueGroupedAction.DELETE]: DELETE };
|
|
|
|
if (action === EIssueGroupedAction.ADD)
|
|
return { [EIssueGroupedAction.ADD]: [...ADD, ...DELETE], [EIssueGroupedAction.DELETE]: [] };
|
|
else return { [EIssueGroupedAction.DELETE]: [...ADD, ...DELETE], [EIssueGroupedAction.ADD]: [] };
|
|
};
|
|
|
|
issueDisplayFiltersDefaultData = (groupBy: string | null): string[] => {
|
|
switch (groupBy) {
|
|
case "state":
|
|
return Object.keys(this.rootIssueStore?.stateMap || {});
|
|
case "state_detail.group":
|
|
return Object.keys(STATE_GROUPS);
|
|
case "priority":
|
|
return ISSUE_PRIORITIES.map((i) => i.key);
|
|
case "labels":
|
|
return Object.keys(this.rootIssueStore?.labelMap || {});
|
|
case "created_by":
|
|
return Object.keys(this.rootIssueStore?.workSpaceMemberRolesMap || {});
|
|
case "assignees":
|
|
return Object.keys(this.rootIssueStore?.workSpaceMemberRolesMap || {});
|
|
case "project":
|
|
return Object.keys(this.rootIssueStore?.projectMap || {});
|
|
default:
|
|
return [];
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This Method is used to get data of the issue based on the ids of the data for states, labels adn assignees
|
|
* @param dataType what type of data is being sent
|
|
* @param dataIds id/ids of the data that is to be populated
|
|
* @param order ascending or descending for arrays of data
|
|
* @returns string | string[] of sortable fields to be used for sorting
|
|
*/
|
|
populateIssueDataForSorting(
|
|
dataType: "state_id" | "label_ids" | "assignee_ids" | "module_ids" | "cycle_id",
|
|
dataIds: string | string[] | null | undefined,
|
|
order?: "asc" | "desc"
|
|
) {
|
|
if (!dataIds) return;
|
|
|
|
const dataValues: string[] = [];
|
|
const isDataIdsArray = Array.isArray(dataIds);
|
|
const dataIdsArray = isDataIdsArray ? dataIds : [dataIds];
|
|
|
|
switch (dataType) {
|
|
case "state_id":
|
|
const stateMap = this.rootIssueStore?.stateMap;
|
|
if (!stateMap) break;
|
|
for (const dataId of dataIdsArray) {
|
|
const state = stateMap[dataId];
|
|
if (state && state.name) dataValues.push(state.name.toLocaleLowerCase());
|
|
}
|
|
break;
|
|
case "label_ids":
|
|
const labelMap = this.rootIssueStore?.labelMap;
|
|
if (!labelMap) break;
|
|
for (const dataId of dataIdsArray) {
|
|
const label = labelMap[dataId];
|
|
if (label && label.name) dataValues.push(label.name.toLocaleLowerCase());
|
|
}
|
|
break;
|
|
case "assignee_ids":
|
|
const memberMap = this.rootIssueStore?.memberMap;
|
|
if (!memberMap) break;
|
|
for (const dataId of dataIdsArray) {
|
|
const member = memberMap[dataId];
|
|
if (member && member.first_name) dataValues.push(member.first_name.toLocaleLowerCase());
|
|
}
|
|
break;
|
|
case "module_ids":
|
|
const moduleMap = this.rootIssueStore?.moduleMap;
|
|
if (!moduleMap) break;
|
|
for (const dataId of dataIdsArray) {
|
|
const currentModule = moduleMap[dataId];
|
|
if (currentModule && currentModule.name) dataValues.push(currentModule.name.toLocaleLowerCase());
|
|
}
|
|
break;
|
|
case "cycle_id":
|
|
const cycleMap = this.rootIssueStore?.cycleMap;
|
|
if (!cycleMap) break;
|
|
for (const dataId of dataIdsArray) {
|
|
const cycle = cycleMap[dataId];
|
|
if (cycle && cycle.name) dataValues.push(cycle.name.toLocaleLowerCase());
|
|
}
|
|
break;
|
|
}
|
|
|
|
return isDataIdsArray ? (order ? orderBy(dataValues, undefined, [order]) : dataValues) : dataValues[0];
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
getSortOrderToFilterEmptyValues(key: string, object: any) {
|
|
const value = object?.[key];
|
|
|
|
if (typeof value !== "number" && isEmpty(value)) return 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
getIssueIds(issues: TIssue[]) {
|
|
return issues.map((issue) => issue?.id);
|
|
}
|
|
|
|
issuesSortWithOrderBy = (issueIds: string[], key: TIssueOrderByOptions | undefined): string[] => {
|
|
const issues = this.rootIssueStore.issues.getIssuesByIds(issueIds, this.isArchived ? "archived" : "un-archived");
|
|
const array = orderBy(issues, "created_at", ["desc"]);
|
|
|
|
switch (key) {
|
|
case "sort_order":
|
|
return this.getIssueIds(orderBy(array, "sort_order"));
|
|
case "state__name":
|
|
return this.getIssueIds(
|
|
orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue["state_id"]))
|
|
);
|
|
case "-state__name":
|
|
return this.getIssueIds(
|
|
orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue["state_id"]), ["desc"])
|
|
);
|
|
// dates
|
|
case "created_at":
|
|
return this.getIssueIds(orderBy(array, "created_at"));
|
|
case "-created_at":
|
|
return this.getIssueIds(orderBy(array, "created_at", ["desc"]));
|
|
case "updated_at":
|
|
return this.getIssueIds(orderBy(array, "updated_at"));
|
|
case "-updated_at":
|
|
return this.getIssueIds(orderBy(array, "updated_at", ["desc"]));
|
|
case "start_date":
|
|
return this.getIssueIds(
|
|
orderBy(array, [this.getSortOrderToFilterEmptyValues.bind(null, "start_date"), "start_date"])
|
|
); //preferring sorting based on empty values to always keep the empty values below
|
|
case "-start_date":
|
|
return this.getIssueIds(
|
|
orderBy(
|
|
array,
|
|
[this.getSortOrderToFilterEmptyValues.bind(null, "start_date"), "start_date"], //preferring sorting based on empty values to always keep the empty values below
|
|
["asc", "desc"]
|
|
)
|
|
);
|
|
|
|
case "target_date":
|
|
return this.getIssueIds(
|
|
orderBy(array, [this.getSortOrderToFilterEmptyValues.bind(null, "target_date"), "target_date"])
|
|
); //preferring sorting based on empty values to always keep the empty values below
|
|
case "-target_date":
|
|
return this.getIssueIds(
|
|
orderBy(
|
|
array,
|
|
[this.getSortOrderToFilterEmptyValues.bind(null, "target_date"), "target_date"], //preferring sorting based on empty values to always keep the empty values below
|
|
["asc", "desc"]
|
|
)
|
|
);
|
|
|
|
// custom
|
|
case "priority": {
|
|
const sortArray = ISSUE_PRIORITIES.map((i) => i.key);
|
|
return this.getIssueIds(orderBy(array, (currentIssue: TIssue) => indexOf(sortArray, currentIssue.priority)));
|
|
}
|
|
case "-priority": {
|
|
const sortArray = ISSUE_PRIORITIES.map((i) => i.key);
|
|
return this.getIssueIds(
|
|
orderBy(array, (currentIssue: TIssue) => indexOf(sortArray, currentIssue.priority), ["desc"])
|
|
);
|
|
}
|
|
|
|
// number
|
|
case "attachment_count":
|
|
return this.getIssueIds(orderBy(array, "attachment_count"));
|
|
case "-attachment_count":
|
|
return this.getIssueIds(orderBy(array, "attachment_count", ["desc"]));
|
|
|
|
case "estimate_point":
|
|
return this.getIssueIds(
|
|
orderBy(array, [this.getSortOrderToFilterEmptyValues.bind(null, "estimate_point"), "estimate_point"])
|
|
); //preferring sorting based on empty values to always keep the empty values below
|
|
case "-estimate_point":
|
|
return this.getIssueIds(
|
|
orderBy(
|
|
array,
|
|
[this.getSortOrderToFilterEmptyValues.bind(null, "estimate_point"), "estimate_point"], //preferring sorting based on empty values to always keep the empty values below
|
|
["asc", "desc"]
|
|
)
|
|
);
|
|
|
|
case "link_count":
|
|
return this.getIssueIds(orderBy(array, "link_count"));
|
|
case "-link_count":
|
|
return this.getIssueIds(orderBy(array, "link_count", ["desc"]));
|
|
|
|
case "sub_issues_count":
|
|
return this.getIssueIds(orderBy(array, "sub_issues_count"));
|
|
case "-sub_issues_count":
|
|
return this.getIssueIds(orderBy(array, "sub_issues_count", ["desc"]));
|
|
|
|
// Array
|
|
case "labels__name":
|
|
return this.getIssueIds(
|
|
orderBy(array, [
|
|
this.getSortOrderToFilterEmptyValues.bind(null, "label_ids"), //preferring sorting based on empty values to always keep the empty values below
|
|
(issue) => this.populateIssueDataForSorting("label_ids", issue["label_ids"], "asc"),
|
|
])
|
|
);
|
|
case "-labels__name":
|
|
return this.getIssueIds(
|
|
orderBy(
|
|
array,
|
|
[
|
|
this.getSortOrderToFilterEmptyValues.bind(null, "label_ids"), //preferring sorting based on empty values to always keep the empty values below
|
|
(issue) => this.populateIssueDataForSorting("label_ids", issue["label_ids"], "desc"),
|
|
],
|
|
["asc", "desc"]
|
|
)
|
|
);
|
|
|
|
case "issue_module__module__name":
|
|
return this.getIssueIds(
|
|
orderBy(array, [
|
|
this.getSortOrderToFilterEmptyValues.bind(null, "module_ids"), //preferring sorting based on empty values to always keep the empty values below
|
|
(issue) => this.populateIssueDataForSorting("module_ids", issue["module_ids"], "asc"),
|
|
])
|
|
);
|
|
case "-issue_module__module__name":
|
|
return this.getIssueIds(
|
|
orderBy(
|
|
array,
|
|
[
|
|
this.getSortOrderToFilterEmptyValues.bind(null, "module_ids"), //preferring sorting based on empty values to always keep the empty values below
|
|
(issue) => this.populateIssueDataForSorting("module_ids", issue["module_ids"], "desc"),
|
|
],
|
|
["asc", "desc"]
|
|
)
|
|
);
|
|
|
|
case "issue_cycle__cycle__name":
|
|
return this.getIssueIds(
|
|
orderBy(array, [
|
|
this.getSortOrderToFilterEmptyValues.bind(null, "cycle_id"), //preferring sorting based on empty values to always keep the empty values below
|
|
(issue) => this.populateIssueDataForSorting("cycle_id", issue["cycle_id"], "asc"),
|
|
])
|
|
);
|
|
case "-issue_cycle__cycle__name":
|
|
return this.getIssueIds(
|
|
orderBy(
|
|
array,
|
|
[
|
|
this.getSortOrderToFilterEmptyValues.bind(null, "cycle_id"), //preferring sorting based on empty values to always keep the empty values below
|
|
(issue) => this.populateIssueDataForSorting("cycle_id", issue["cycle_id"], "desc"),
|
|
],
|
|
["asc", "desc"]
|
|
)
|
|
);
|
|
|
|
case "assignees__first_name":
|
|
return this.getIssueIds(
|
|
orderBy(array, [
|
|
this.getSortOrderToFilterEmptyValues.bind(null, "assignee_ids"), //preferring sorting based on empty values to always keep the empty values below
|
|
(issue) => this.populateIssueDataForSorting("assignee_ids", issue["assignee_ids"], "asc"),
|
|
])
|
|
);
|
|
case "-assignees__first_name":
|
|
return this.getIssueIds(
|
|
orderBy(
|
|
array,
|
|
[
|
|
this.getSortOrderToFilterEmptyValues.bind(null, "assignee_ids"), //preferring sorting based on empty values to always keep the empty values below
|
|
(issue) => this.populateIssueDataForSorting("assignee_ids", issue["assignee_ids"], "desc"),
|
|
],
|
|
["asc", "desc"]
|
|
)
|
|
);
|
|
|
|
default:
|
|
return this.getIssueIds(array);
|
|
}
|
|
};
|
|
|
|
getGroupArray(value: boolean | number | string | string[] | null, isDate: boolean = false): string[] {
|
|
if (!value || value === null || value === undefined) return ["None"];
|
|
if (Array.isArray(value))
|
|
if (value && value.length) return value;
|
|
else return ["None"];
|
|
else if (typeof value === "boolean") return [value ? "True" : "False"];
|
|
else if (typeof value === "number") return [value.toString()];
|
|
else if (isDate) return [renderFormattedPayloadDate(value) || "None"];
|
|
else return [value || "None"];
|
|
}
|
|
|
|
storePreviousPaginationValues = (
|
|
issuesResponse: TIssuesResponse,
|
|
options?: IssuePaginationOptions,
|
|
groupId?: string,
|
|
subGroupId?: string
|
|
) => {
|
|
if (options) this.paginationOptions = options;
|
|
|
|
this.setPaginationData(
|
|
issuesResponse.prev_cursor,
|
|
issuesResponse.next_cursor,
|
|
issuesResponse.next_page_results,
|
|
groupId,
|
|
subGroupId
|
|
);
|
|
};
|
|
|
|
processIssueResponse(issueResponse: TIssuesResponse): {
|
|
issueList: TIssue[];
|
|
groupedIssues: TIssues;
|
|
groupedIssueCount: TGroupedIssueCount;
|
|
} {
|
|
const issueResult = issueResponse?.results;
|
|
|
|
if (!issueResult)
|
|
return {
|
|
issueList: [],
|
|
groupedIssues: {},
|
|
groupedIssueCount: {},
|
|
};
|
|
|
|
if (Array.isArray(issueResult)) {
|
|
return {
|
|
issueList: issueResult,
|
|
groupedIssues: {
|
|
[ALL_ISSUES]: issueResult.map((issue) => issue.id),
|
|
},
|
|
groupedIssueCount: {
|
|
[ALL_ISSUES]: issueResponse.total_count,
|
|
},
|
|
};
|
|
}
|
|
|
|
const issueList: TIssue[] = [];
|
|
const groupedIssues: TGroupedIssues | TSubGroupedIssues = {};
|
|
const groupedIssueCount: TGroupedIssueCount = {};
|
|
|
|
set(groupedIssueCount, [ALL_ISSUES], issueResponse.total_count);
|
|
|
|
for (const groupId in issueResult) {
|
|
const groupIssuesObject = issueResult[groupId];
|
|
const groupIssueResult = groupIssuesObject?.results;
|
|
|
|
if (!groupIssueResult) continue;
|
|
|
|
set(groupedIssueCount, [groupId], groupIssuesObject.total_results);
|
|
|
|
if (Array.isArray(groupIssueResult)) {
|
|
issueList.push(...groupIssueResult);
|
|
set(
|
|
groupedIssues,
|
|
[groupId],
|
|
groupIssueResult.map((issue) => issue.id)
|
|
);
|
|
continue;
|
|
}
|
|
|
|
for (const subGroupId in groupIssueResult) {
|
|
const subGroupIssuesObject = groupIssueResult[subGroupId];
|
|
const subGroupIssueResult = subGroupIssuesObject?.results;
|
|
|
|
if (!subGroupIssueResult) continue;
|
|
|
|
set(groupedIssueCount, [this.getGroupKey(groupId, subGroupId)], subGroupIssuesObject.total_results);
|
|
|
|
if (Array.isArray(subGroupIssueResult)) {
|
|
issueList.push(...subGroupIssueResult);
|
|
set(
|
|
groupedIssues,
|
|
[groupId, subGroupId],
|
|
subGroupIssueResult.map((issue) => issue.id)
|
|
);
|
|
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
return { issueList, groupedIssues, groupedIssueCount };
|
|
}
|
|
|
|
setPaginationData(
|
|
prevCursor: string,
|
|
nextCursor: string,
|
|
nextPageResults: boolean,
|
|
groupId?: string,
|
|
subGroupId?: string
|
|
) {
|
|
const cursorObject = {
|
|
prevCursor,
|
|
nextCursor,
|
|
nextPageResults,
|
|
};
|
|
|
|
set(this.issuePaginationData, [this.getGroupKey(groupId, subGroupId)], cursorObject);
|
|
}
|
|
|
|
getGroupKey(groupId?: string, subGroupId?: string) {
|
|
if (groupId && subGroupId) return `${groupId}_${subGroupId}`;
|
|
|
|
if (groupId) return groupId;
|
|
|
|
return ALL_ISSUES;
|
|
}
|
|
|
|
getPaginationData = computedFn(
|
|
(groupId: string | undefined, subGroupId: string | undefined): TPaginationData | undefined => {
|
|
return get(this.issuePaginationData, [this.getGroupKey(groupId, subGroupId)]);
|
|
}
|
|
);
|
|
|
|
getGroupIssueCount = computedFn(
|
|
(
|
|
groupId: string | undefined,
|
|
subGroupId: string | undefined,
|
|
isSubGroupCumulative: boolean
|
|
): number | undefined => {
|
|
if (isSubGroupCumulative && subGroupId) {
|
|
const groupIssuesKeys = Object.keys(this.groupedIssueCount);
|
|
let subGroupCumulativeCount = 0;
|
|
|
|
for (const groupKey of groupIssuesKeys) {
|
|
if (groupKey.includes(subGroupId)) subGroupCumulativeCount += this.groupedIssueCount[groupKey];
|
|
}
|
|
|
|
return subGroupCumulativeCount;
|
|
}
|
|
|
|
return get(this.groupedIssueCount, [this.getGroupKey(groupId, subGroupId)]);
|
|
}
|
|
);
|
|
}
|