feat: init
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
chuan
2025-11-11 01:56:44 +08:00
commit bba4bb40c8
4638 changed files with 447437 additions and 0 deletions

View File

@@ -0,0 +1,515 @@
import { concat, get, set, uniq, update } from "lodash-es";
import { action, makeObservable, observable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
// plane imports
import { ALL_ISSUES } from "@plane/constants";
import { SitesIssueService } from "@plane/services";
import type {
TIssueGroupByOptions,
TGroupedIssues,
TSubGroupedIssues,
TLoader,
IssuePaginationOptions,
TIssues,
TIssuePaginationData,
TGroupedIssueCount,
TPaginationData,
} from "@plane/types";
// types
import type { IIssue, TIssuesResponse } from "@/types/issue";
import type { CoreRootStore } from "../root.store";
// constants
// helpers
export type TIssueDisplayFilterOptions = Exclude<TIssueGroupByOptions, null | "team_project"> | "target_date";
export enum EIssueGroupedAction {
ADD = "ADD",
DELETE = "DELETE",
REORDER = "REORDER",
}
export interface IBaseIssuesStore {
// observable
loader: Record<string, TLoader>;
// actions
addIssue(issues: IIssue[], shouldReplace?: boolean): void;
// helper methods
groupedIssueIds: TGroupedIssues | TSubGroupedIssues | undefined; // object to store Issue Ids based on group or subgroup
groupedIssueCount: TGroupedIssueCount; // map of groupId/subgroup and issue count of that particular group/subgroup
issuePaginationData: TIssuePaginationData; // map of groupId/subgroup and pagination Data of that particular group/subgroup
// helper methods
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;
}
export const ISSUE_FILTER_DEFAULT_DATA: Record<TIssueDisplayFilterOptions, keyof IIssue> = {
project: "project_id",
cycle: "cycle_id",
module: "module_ids",
state: "state_id",
"state_detail.group": "state_group" as keyof IIssue, // 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",
};
export abstract class BaseIssuesStore implements IBaseIssuesStore {
loader: Record<string, TLoader> = {};
groupedIssueIds: TIssues | undefined = undefined;
issuePaginationData: TIssuePaginationData = {};
groupedIssueCount: TGroupedIssueCount = {};
//
paginationOptions: IssuePaginationOptions | undefined = undefined;
issueService;
// root store
rootIssueStore;
constructor(_rootStore: CoreRootStore) {
makeObservable(this, {
// observable
loader: observable,
groupedIssueIds: observable,
issuePaginationData: observable,
groupedIssueCount: observable,
paginationOptions: observable,
// action
storePreviousPaginationValues: action.bound,
onfetchIssues: action.bound,
onfetchNexIssues: action.bound,
clear: action.bound,
setLoader: action.bound,
});
this.rootIssueStore = _rootStore;
this.issueService = new SitesIssueService();
}
getIssueIds = (groupId?: string, subGroupId?: string) => {
const groupedIssueIds = this.groupedIssueIds;
if (!groupedIssueIds) return undefined;
const allIssues = groupedIssueIds[ALL_ISSUES] ?? [];
if (allIssues && Array.isArray(allIssues)) {
return allIssues as string[];
}
if (groupId && groupedIssueIds?.[groupId] && Array.isArray(groupedIssueIds[groupId])) {
return (groupedIssueIds[groupId] ?? []) as string[];
}
if (groupId && subGroupId) {
return ((groupedIssueIds as TSubGroupedIssues)[groupId]?.[subGroupId] ?? []) as string[];
}
return undefined;
};
/**
* @description This method will add issues to the issuesMap
* @param {IIssue[]} issues
* @returns {void}
*/
addIssue = (issues: IIssue[], shouldReplace = false) => {
if (issues && issues.length <= 0) return;
runInAction(() => {
issues.forEach((issue) => {
if (!this.rootIssueStore.issueDetail.getIssueById(issue.id) || shouldReplace)
set(this.rootIssueStore.issueDetail.details, issue.id, issue);
});
});
};
/**
* Store the pagination data required for next subsequent issue pagination calls
* @param prevCursor cursor value of previous page
* @param nextCursor cursor value of next page
* @param nextPageResults boolean to indicate if the next page results exist i.e, have we reached end of pages
* @param groupId groupId and subGroupId to add the pagination data for the particular group/subgroup
* @param subGroupId
*/
setPaginationData(
prevCursor: string,
nextCursor: string,
nextPageResults: boolean,
groupId?: string,
subGroupId?: string
) {
const cursorObject = {
prevCursor,
nextCursor,
nextPageResults,
};
set(this.issuePaginationData, [this.getGroupKey(groupId, subGroupId)], cursorObject);
}
/**
* Sets the loader value of the particular groupId/subGroupId, or to ALL_ISSUES if both are undefined
* @param loaderValue
* @param groupId
* @param subGroupId
*/
setLoader(loaderValue: TLoader, groupId?: string, subGroupId?: string) {
runInAction(() => {
set(this.loader, this.getGroupKey(groupId, subGroupId), loaderValue);
});
}
/**
* gets the Loader value of particular group/subgroup/ALL_ISSUES
*/
getIssueLoader = (groupId?: string, subGroupId?: string) => get(this.loader, this.getGroupKey(groupId, subGroupId));
/**
* gets the pagination data of particular group/subgroup/ALL_ISSUES
*/
getPaginationData = computedFn(
(groupId: string | undefined, subGroupId: string | undefined): TPaginationData | undefined =>
get(this.issuePaginationData, [this.getGroupKey(groupId, subGroupId)])
);
/**
* gets the issue count of particular group/subgroup/ALL_ISSUES
*
* if isSubGroupCumulative is true, sum up all the issueCount of the subGroupId, across all the groupIds
*/
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)]);
}
);
/**
* This Method is called after fetching the first paginated issues
*
* This method updates the appropriate issue list based on if groupByKey or subGroupByKey are defined
* If both groupByKey and subGroupByKey are not defined, then the issue list are added to another group called ALL_ISSUES
* @param issuesResponse Paginated Response received from the API
* @param options Pagination options
* @param workspaceSlug
* @param projectId
* @param id Id can be anything from cycleId, moduleId, viewId or userId based on the store
*/
onfetchIssues(issuesResponse: TIssuesResponse, options: IssuePaginationOptions) {
// Process the Issue Response to get the following data from it
const { issueList, groupedIssues, groupedIssueCount } = this.processIssueResponse(issuesResponse);
// The Issue list is added to the main Issue Map
this.addIssue(issueList);
// Update all the GroupIds to this Store's groupedIssueIds and update Individual group issue counts
runInAction(() => {
this.updateGroupedIssueIds(groupedIssues, groupedIssueCount);
this.loader[this.getGroupKey()] = undefined;
});
// store Pagination options for next subsequent calls and data like next cursor etc
this.storePreviousPaginationValues(issuesResponse, options);
}
/**
* This Method is called on the subsequent pagination calls after the first initial call
*
* This method updates the appropriate issue list based on if groupId or subgroupIds are Passed
* @param issuesResponse Paginated Response received from the API
* @param groupId
* @param subGroupId
*/
onfetchNexIssues(issuesResponse: TIssuesResponse, groupId?: string, subGroupId?: string) {
// Process the Issue Response to get the following data from it
const { issueList, groupedIssues, groupedIssueCount } = this.processIssueResponse(issuesResponse);
// The Issue list is added to the main Issue Map
this.addIssue(issueList);
// Update all the GroupIds to this Store's groupedIssueIds and update Individual group issue counts
runInAction(() => {
this.updateGroupedIssueIds(groupedIssues, groupedIssueCount, groupId, subGroupId);
this.loader[this.getGroupKey(groupId, subGroupId)] = undefined;
});
// store Pagination data like next cursor etc
this.storePreviousPaginationValues(issuesResponse, undefined, groupId, subGroupId);
}
/**
* Method called to clear out the current store
*/
clear(shouldClearPaginationOptions = true) {
runInAction(() => {
this.groupedIssueIds = undefined;
this.issuePaginationData = {};
this.groupedIssueCount = {};
if (shouldClearPaginationOptions) {
this.paginationOptions = undefined;
}
});
}
/**
* This method processes the issueResponse to provide data that can be used to update the store
* @param issueResponse
* @returns issueList, list of issue Data
* @returns groupedIssues, grouped issue Ids
* @returns groupedIssueCount, object containing issue counts of individual groups
*/
processIssueResponse(issueResponse: TIssuesResponse): {
issueList: IIssue[];
groupedIssues: TIssues;
groupedIssueCount: TGroupedIssueCount;
} {
const issueResult = issueResponse?.results;
// if undefined return empty objects
if (!issueResult)
return {
issueList: [],
groupedIssues: {},
groupedIssueCount: {},
};
//if is an array then it's an ungrouped response. return values with groupId as ALL_ISSUES
if (Array.isArray(issueResult)) {
return {
issueList: issueResult,
groupedIssues: {
[ALL_ISSUES]: issueResult.map((issue) => issue.id),
},
groupedIssueCount: {
[ALL_ISSUES]: issueResponse.total_count,
},
};
}
const issueList: IIssue[] = [];
const groupedIssues: TGroupedIssues | TSubGroupedIssues = {};
const groupedIssueCount: TGroupedIssueCount = {};
// update total issue count to ALL_ISSUES
set(groupedIssueCount, [ALL_ISSUES], issueResponse.total_count);
// loop through all the groupIds from issue Result
for (const groupId in issueResult) {
const groupIssuesObject = issueResult[groupId];
const groupIssueResult = groupIssuesObject?.results;
// if groupIssueResult is undefined then continue the loop
if (!groupIssueResult) continue;
// set grouped Issue count of the current groupId
set(groupedIssueCount, [groupId], groupIssuesObject.total_results);
// if groupIssueResult, the it is not subGrouped
if (Array.isArray(groupIssueResult)) {
// add the result to issueList
issueList.push(...groupIssueResult);
// set the issue Ids to the groupId path
set(
groupedIssues,
[groupId],
groupIssueResult.map((issue) => issue.id)
);
continue;
}
// loop through all the subGroupIds from issue Result
for (const subGroupId in groupIssueResult) {
const subGroupIssuesObject = groupIssueResult[subGroupId];
const subGroupIssueResult = subGroupIssuesObject?.results;
// if subGroupIssueResult is undefined then continue the loop
if (!subGroupIssueResult) continue;
// set sub grouped Issue count of the current groupId
set(groupedIssueCount, [this.getGroupKey(groupId, subGroupId)], subGroupIssuesObject.total_results);
if (Array.isArray(subGroupIssueResult)) {
// add the result to issueList
issueList.push(...subGroupIssueResult);
// set the issue Ids to the [groupId, subGroupId] path
set(
groupedIssues,
[groupId, subGroupId],
subGroupIssueResult.map((issue) => issue.id)
);
continue;
}
}
}
return { issueList, groupedIssues, groupedIssueCount };
}
/**
* This method is used to update the grouped issue Ids to it's respected lists and also to update group Issue Counts
* @param groupedIssues Object that contains list of issueIds with respect to their groups/subgroups
* @param groupedIssueCount Object the contains the issue count of each groups
* @param groupId groupId string
* @param subGroupId subGroupId string
* @returns updates the store with the values
*/
updateGroupedIssueIds(
groupedIssues: TIssues,
groupedIssueCount: TGroupedIssueCount,
groupId?: string,
subGroupId?: string
) {
// if groupId exists and groupedIssues has ALL_ISSUES as a group,
// then it's an individual group/subgroup pagination
if (groupId && groupedIssues[ALL_ISSUES] && Array.isArray(groupedIssues[ALL_ISSUES])) {
const issueGroup = groupedIssues[ALL_ISSUES];
const issueGroupCount = groupedIssueCount[ALL_ISSUES];
const issuesPath = [groupId];
// issuesPath is the path for the issue List in the Grouped Issue List
// issuePath is either [groupId] for grouped pagination or [groupId, subGroupId] for subGrouped pagination
if (subGroupId) issuesPath.push(subGroupId);
// update the issue Count of the particular group/subGroup
set(this.groupedIssueCount, [this.getGroupKey(groupId, subGroupId)], issueGroupCount);
// update the issue list in the issuePath
this.updateIssueGroup(issueGroup, issuesPath);
return;
}
// if not in the above condition the it's a complete grouped pagination not individual group/subgroup pagination
// update total issue count as ALL_ISSUES count in `groupedIssueCount` object
set(this.groupedIssueCount, [ALL_ISSUES], groupedIssueCount[ALL_ISSUES]);
// loop through the groups of groupedIssues.
for (const groupId in groupedIssues) {
const issueGroup = groupedIssues[groupId];
const issueGroupCount = groupedIssueCount[groupId];
// update the groupId's issue count
set(this.groupedIssueCount, [groupId], issueGroupCount);
// This updates the group issue list in the store, if the issueGroup is a string
const storeUpdated = this.updateIssueGroup(issueGroup, [groupId]);
// if issueGroup is indeed a string, continue
if (storeUpdated) continue;
// if issueGroup is not a string, loop through the sub group Issues
for (const subGroupId in issueGroup) {
const issueSubGroup = (issueGroup as TGroupedIssues)[subGroupId];
const issueSubGroupCount = groupedIssueCount[this.getGroupKey(groupId, subGroupId)];
// update the subGroupId's issue count
set(this.groupedIssueCount, [this.getGroupKey(groupId, subGroupId)], issueSubGroupCount);
// This updates the subgroup issue list in the store
this.updateIssueGroup(issueSubGroup, [groupId, subGroupId]);
}
}
}
/**
* This Method is used to update the issue Id list at the particular issuePath
* @param groupedIssueIds could be an issue Id List for grouped issues or an object that contains a issue Id list in case of subGrouped
* @param issuePath array of string, to identify the path of the issueList to be updated with the above issue Id list
* @returns a boolean that indicates if the groupedIssueIds is indeed a array Id list, in which case the issue Id list is added to the store at issuePath
*/
updateIssueGroup(groupedIssueIds: TGroupedIssues | string[], issuePath: string[]): boolean {
if (!groupedIssueIds) return true;
// if groupedIssueIds is an array, update the `groupedIssueIds` store at the issuePath
if (groupedIssueIds && Array.isArray(groupedIssueIds)) {
update(this, ["groupedIssueIds", ...issuePath], (issueIds: string[] = []) =>
uniq(concat(issueIds, groupedIssueIds as string[]))
);
// return true to indicate the store has been updated
return true;
}
// return false to indicate the store has been updated and the groupedIssueIds is likely Object for subGrouped Issues
return false;
}
/**
* This method is used to update the count of the issues at the path with the increment
* @param path issuePath, corresponding key is to be incremented
* @param increment
*/
updateIssueCount(accumulatedUpdatesForCount: { [key: string]: EIssueGroupedAction }) {
const updateKeys = Object.keys(accumulatedUpdatesForCount);
for (const updateKey of updateKeys) {
const update = accumulatedUpdatesForCount[updateKey];
if (!update) continue;
const increment = update === EIssueGroupedAction.ADD ? 1 : -1;
// get current count at the key
const issueCount = get(this.groupedIssueCount, updateKey) ?? 0;
// update the count at the key
set(this.groupedIssueCount, updateKey, issueCount + increment);
}
}
/**
* This Method is called to store the pagination options and paginated data from response
* @param issuesResponse issue list response
* @param options pagination options to be stored for next page call
* @param groupId
* @param subGroupId
*/
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
);
};
/**
* 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
*/
getGroupKey = (groupId?: string, subGroupId?: string) => {
if (groupId && subGroupId && subGroupId !== "null") return `${groupId}_${subGroupId}`;
if (groupId) return groupId;
return ALL_ISSUES;
};
}

View File

@@ -0,0 +1,73 @@
import { EIssueGroupByToServerOptions, EServerGroupByToFilterOptions } from "@plane/constants";
import type { IssuePaginationOptions, TIssueParams } from "@plane/types";
/**
* 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
*/
export const 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 = EIssueGroupByToServerOptions[options.groupedBy];
}
// If group by is specifically sent through options, like that for calendar layout, use that to group
if (options.subGroupedBy) {
paginationParams.sub_group_by = EIssueGroupByToServerOptions[options.subGroupedBy];
}
// If group by is specifically sent through options, like that for calendar layout, use that to group
if (options.orderBy) {
paginationParams.order_by = options.orderBy;
}
// 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;
};