import { action, computed, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; 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 set from "lodash/set"; import get from "lodash/get"; import isEqual from "lodash/isEqual"; import isNil from "lodash/isNil"; // types import { TIssue, TIssueGroupByOptions, TIssueOrderByOptions, TGroupedIssues, TSubGroupedIssues, TLoader, IssuePaginationOptions, TIssuesResponse, TIssues, TIssuePaginationData, TGroupedIssueCount, TPaginationData, TBulkOperationsPayload, } from "@plane/types"; import { IIssueRootStore } from "../root.store"; import { IBaseIssueFilterStore } from "./issue-filter-helper.store"; // constants import { ALL_ISSUES, EIssueLayoutTypes, ISSUE_PRIORITIES } from "@/constants/issue"; // helpers // services import { IssueArchiveService, IssueDraftService, IssueService } from "@/services/issue"; import { ModuleService } from "@/services/module.service"; import { CycleService } from "@/services/cycle.service"; import { getDifference, getGroupIssueKeyActions, getGroupKey, getIssueIds, getSortOrderToFilterEmptyValues, getSubGroupIssueKeyActions, } from "./base-issues-utils"; import { convertToISODateString } from "@/helpers/date-time.helper"; export type TIssueDisplayFilterOptions = Exclude | "target_date"; export enum EIssueGroupedAction { ADD = "ADD", DELETE = "DELETE", REORDER = "REORDER", } export interface IBaseIssuesStore { // observable loader: Record; 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 //actions removeIssue(workspaceSlug: string, projectId: string, issueId: string): Promise; // helper methods getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined; issuesSortWithOrderBy(issueIds: string[], key: Partial): string[]; 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; addIssueToCycle: ( workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[], fetchAddedIssues?: boolean ) => Promise; removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise; addCycleToIssue: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise; removeCycleFromIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; addIssuesToModule: ( workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[], fetchAddedIssues?: boolean ) => Promise; removeIssuesFromModule: ( workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[] ) => Promise; changeModulesInIssue( workspaceSlug: string, projectId: string, issueId: string, addModuleIds: string[], removeModuleIds: string[] ): Promise; } // This constant maps the group by keys to the respective issue property that the key relies on const ISSUE_GROUP_BY_KEY: Record = { 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", }; export const ISSUE_FILTER_DEFAULT_DATA: Record = { project: "project_id", cycle: "cycle_id", module: "module_ids", state: "state_id", "state_detail.group": "state_group" 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", }; // This constant maps the order by keys to the respective issue property that the key relies on const ISSUE_ORDERBY_KEY: Record = { 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 abstract class BaseIssuesStore implements IBaseIssuesStore { loader: Record = {}; groupedIssueIds: TIssues | undefined = undefined; issuePaginationData: TIssuePaginationData = {}; groupedIssueCount: TGroupedIssueCount = {}; // paginationOptions: IssuePaginationOptions | undefined = undefined; isArchived: boolean; // services issueService; issueArchiveService; issueDraftService; moduleService; cycleService; // root store rootIssueStore; issueFilterStore; constructor(_rootStore: IIssueRootStore, issueFilterStore: IBaseIssueFilterStore, isArchived = false) { makeObservable(this, { // observable loader: observable, groupedIssueIds: observable, issuePaginationData: observable, groupedIssueCount: observable, paginationOptions: observable, // computed moduleId: computed, cycleId: computed, orderBy: computed, groupBy: computed, subGroupBy: computed, orderByKey: computed, issueGroupKey: computed, issueSubGroupKey: computed, // action storePreviousPaginationValues: action.bound, onfetchIssues: action.bound, onfetchNexIssues: action.bound, clear: action.bound, setLoader: action.bound, addIssue: action.bound, removeIssueFromList: action.bound, createIssue: action, updateIssue: action, createDraftIssue: action, updateDraftIssue: action, issueQuickAdd: action.bound, removeIssue: action.bound, archiveIssue: action.bound, removeBulkIssues: action.bound, bulkArchiveIssues: action.bound, bulkUpdateProperties: action.bound, addIssueToCycle: action.bound, removeIssueFromCycle: action.bound, addCycleToIssue: action.bound, removeCycleFromIssue: action.bound, addIssuesToModule: action.bound, removeIssuesFromModule: action.bound, changeModulesInIssue: action.bound, }); this.rootIssueStore = _rootStore; this.issueFilterStore = issueFilterStore; this.isArchived = isArchived; this.issueService = new IssueService(); this.issueArchiveService = new IssueArchiveService(); this.issueDraftService = new IssueDraftService(); this.moduleService = new ModuleService(); this.cycleService = new CycleService(); } // Abstract class to be implemented to fetch parent stats such as project, module or cycle details abstract fetchParentStats: (workspaceSlug: string, projectId?: string, id?: string) => void; // current Module Id from url get moduleId() { return this.rootIssueStore.moduleId; } // current Cycle Id from url get cycleId() { return this.rootIssueStore.cycleId; } // current Order by value get orderBy() { const displayFilters = this.issueFilterStore?.issueFilters?.displayFilters; if (!displayFilters) return; return displayFilters?.order_by; } // current Group by value get groupBy() { const displayFilters = this.issueFilterStore?.issueFilters?.displayFilters; if (!displayFilters || !displayFilters?.layout) return; const layout = displayFilters?.layout; return layout === EIssueLayoutTypes.CALENDAR ? "target_date" : [EIssueLayoutTypes.LIST, EIssueLayoutTypes.KANBAN]?.includes(layout) ? displayFilters?.group_by : undefined; } // current Sub group by value 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; } getIssueIds = (groupId?: string, subGroupId?: string) => { const groupedIssueIds = this.groupedIssueIds; const displayFilters = this.issueFilterStore?.issueFilters?.displayFilters; if (!displayFilters || !groupedIssueIds) return undefined; const subGroupBy = displayFilters?.sub_group_by; const groupBy = displayFilters?.group_by; if (!groupBy && !subGroupBy && Array.isArray(groupedIssueIds)) { return groupedIssueIds as string[]; } if (groupBy && groupId && groupedIssueIds?.[groupId] && Array.isArray(groupedIssueIds[groupId])) { return groupedIssueIds[groupId] as string[]; } if (groupBy && subGroupBy && groupId && subGroupId) { return (groupedIssueIds as TSubGroupedIssues)?.[subGroupId]?.[groupId] as string[]; } return undefined; }; // The Issue Property corresponding to the order by value get orderByKey() { const orderBy = this.orderBy; if (!orderBy) return; return ISSUE_ORDERBY_KEY[orderBy]; } // The Issue Property corresponding to the group by value get issueGroupKey() { const groupBy = this.groupBy; if (!groupBy) return; return ISSUE_GROUP_BY_KEY[groupBy]; } // The Issue Property corresponding to the sub group by value get issueSubGroupKey() { const subGroupBy = this.subGroupBy; if (!subGroupBy) return; return ISSUE_GROUP_BY_KEY[subGroupBy]; } /** * 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, [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, getGroupKey(groupId, subGroupId), loaderValue); }); } /** * gets the Loader value of particular group/subgroup/ALL_ISSUES */ getIssueLoader = (groupId?: string, subGroupId?: string) => { return get(this.loader, getGroupKey(groupId, subGroupId)); }; /** * gets the pagination data of particular group/subgroup/ALL_ISSUES */ getPaginationData = computedFn( (groupId: string | undefined, subGroupId: string | undefined): TPaginationData | undefined => { return get(this.issuePaginationData, [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, [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, workspaceSlug: string, projectId?: string, id?: 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.rootIssueStore.issues.addIssue(issueList); // Update all the GroupIds to this Store's groupedIssueIds and update Individual group issue counts runInAction(() => { this.updateGroupedIssueIds(groupedIssues, groupedIssueCount); this.loader[getGroupKey()] = undefined; }); // fetch parent stats if required, to be handled in the Implemented class this.fetchParentStats(workspaceSlug, projectId, id); // 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.rootIssueStore.issues.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[getGroupKey(groupId, subGroupId)] = undefined; }); // store Pagination data like next cursor etc this.storePreviousPaginationValues(issuesResponse, undefined, groupId, subGroupId); } /** * Method to create Issue. This method updates the store and calls the API to create an issue * @param workspaceSlug * @param projectId * @param data Default Issue Data * @param id optional id like moduleId and cycleId, not used here but required in overridden the Module or cycle issues methods * @param shouldUpdateList If false, then it would not update the Issue Id list but only makes an API call and adds to the main Issue Map * @returns */ async createIssue( workspaceSlug: string, projectId: string, data: Partial, id?: string, shouldUpdateList = true ) { try { // perform an API call const response = await this.issueService.createIssue(workspaceSlug, projectId, data); // add Issue to Store this.addIssue(response, shouldUpdateList); // If shouldUpdateList is true, call fetchParentStats shouldUpdateList && this.fetchParentStats(workspaceSlug, projectId); return response; } catch (error) { throw error; } } /** * Updates the Issue, by calling the API and also updating the store * @param workspaceSlug * @param projectId * @param issueId * @param data Partial Issue Data to be updated * @param shouldSync If False then only issue is to be updated in the store not call API to update * @returns */ async updateIssue( workspaceSlug: string, projectId: string, issueId: string, data: Partial, shouldSync = true ) { // Store Before state of the issue const issueBeforeUpdate = clone(this.rootIssueStore.issues.getIssueById(issueId)); try { // Update the Respective Stores this.rootIssueStore.issues.updateIssue(issueId, data); this.updateIssueList({ ...issueBeforeUpdate, ...data } as TIssue, issueBeforeUpdate); // Check if should Sync if (!shouldSync) return; // call API to update the issue await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data); // call fetch Parent Stats this.fetchParentStats(workspaceSlug, projectId); } catch (error) { // If errored out update store again to revert the change this.rootIssueStore.issues.updateIssue(issueId, issueBeforeUpdate ?? {}); this.updateIssueList(issueBeforeUpdate, { ...issueBeforeUpdate, ...data } as TIssue); throw error; } } /** * Similar to Create Issue but for creating Draft issues * @param workspaceSlug * @param projectId * @param data draft issue data * @returns */ async createDraftIssue(workspaceSlug: string, projectId: string, data: Partial) { try { // call API to create a Draft issue const response = await this.issueDraftService.createDraftIssue(workspaceSlug, projectId, data); // call Fetch parent stats this.fetchParentStats(workspaceSlug, projectId); // Add issue to store this.addIssue(response); return response; } catch (error) { throw error; } } /** * Similar to update issue but for draft issues. * @param workspaceSlug * @param projectId * @param issueId * @param data Partial Issue Data to be updated */ async updateDraftIssue(workspaceSlug: string, projectId: string, issueId: string, data: Partial) { // Store Before state of the issue const issueBeforeUpdate = clone(this.rootIssueStore.issues.getIssueById(issueId)); try { // Update the Respective Stores this.rootIssueStore.issues.updateIssue(issueId, data); this.updateIssueList({ ...issueBeforeUpdate, ...data } as TIssue, issueBeforeUpdate); // call API to update the issue await this.issueDraftService.updateDraftIssue(workspaceSlug, projectId, issueId, data); // call Fetch parent stats this.fetchParentStats(workspaceSlug, projectId); // If the issue is updated to not a draft issue anymore remove from the store list if (!isNil(data.is_draft) && !data.is_draft) this.removeIssueFromList(issueId); } catch (error) { // If errored out update store again to revert the change this.rootIssueStore.issues.updateIssue(issueId, issueBeforeUpdate ?? {}); this.updateIssueList(issueBeforeUpdate, { ...issueBeforeUpdate, ...data } as TIssue); throw error; } } /** * This method is called to delete an issue * @param workspaceSlug * @param projectId * @param issueId */ async removeIssue(workspaceSlug: string, projectId: string, issueId: string) { try { // Male API call await this.issueService.deleteIssue(workspaceSlug, projectId, issueId); // Remove from Respective issue Id list runInAction(() => { this.removeIssueFromList(issueId); }); // call fetch Parent stats this.fetchParentStats(workspaceSlug, projectId); // Remove issue from main issue Map store this.rootIssueStore.issues.removeIssue(issueId); } catch (error) { throw error; } } /** * This method is called to Archive an issue * @param workspaceSlug * @param projectId * @param issueId */ async archiveIssue(workspaceSlug: string, projectId: string, issueId: string) { try { // Male API call const response = await this.issueArchiveService.archiveIssue(workspaceSlug, projectId, issueId); // call fetch Parent stats this.fetchParentStats(workspaceSlug, projectId); runInAction(() => { // Update the Archived at of the issue from store this.rootIssueStore.issues.updateIssue(issueId, { archived_at: response.archived_at, }); // Since Archived remove the issue Id from the current store this.removeIssueFromList(issueId); }); } catch (error) { throw error; } } /** * Method to perform Quick add of issues * @param workspaceSlug * @param projectId * @param data * @returns */ async issueQuickAdd(workspaceSlug: string, projectId: string, data: TIssue) { try { // Add issue to store with a temporary Id this.addIssue(data); // call Create issue method const response = await this.createIssue(workspaceSlug, projectId, data); runInAction(() => { this.removeIssueFromList(data.id); this.rootIssueStore.issues.removeIssue(data.id); }); if (data.cycle_id && data.cycle_id !== "" && !this.cycleId) { await this.addCycleToIssue(workspaceSlug, projectId, data.cycle_id, response.id); } if (data.module_ids && data.module_ids.length > 0 && !this.moduleId) { await this.changeModulesInIssue(workspaceSlug, projectId, response.id, data.module_ids, []); } return response; } catch (error) { throw error; } } /** * This is a method to delete issues in bulk * @param workspaceSlug * @param projectId * @param issueIds * @returns */ async removeBulkIssues(workspaceSlug: string, projectId: string, issueIds: string[]) { try { // Make API call to bulk delete issues const response = await this.issueService.bulkDeleteIssues(workspaceSlug, projectId, { issue_ids: issueIds }); // call fetch parent stats this.fetchParentStats(workspaceSlug, projectId); // Remove issues from the store runInAction(() => { issueIds.forEach((issueId) => { this.removeIssueFromList(issueId); this.rootIssueStore.issues.removeIssue(issueId); }); }); return response; } catch (error) { throw error; } } /** * Bulk Archive issues * @param workspaceSlug * @param projectId * @param issueIds */ bulkArchiveIssues = async (workspaceSlug: string, projectId: string, issueIds: string[]) => { try { const response = await this.issueService.bulkArchiveIssues(workspaceSlug, projectId, { issue_ids: issueIds }); runInAction(() => { issueIds.forEach((issueId) => { this.updateIssue(workspaceSlug, projectId, issueId, { archived_at: response.archived_at, }); this.removeIssueFromList(issueId); }); }); } catch (error) { throw error; } }; /** * @description bulk update properties of selected issues * @param {TBulkOperationsPayload} data */ bulkUpdateProperties = async (workspaceSlug: string, projectId: string, data: TBulkOperationsPayload) => { const issueIds = data.issue_ids; try { // make request to update issue properties await this.issueService.bulkOperations(workspaceSlug, projectId, data); // update issues in the store runInAction(() => { issueIds.forEach((issueId) => { const issueBeforeUpdate = clone(this.rootIssueStore.issues.getIssueById(issueId)); if (!issueBeforeUpdate) throw new Error("Issue not found"); Object.keys(data.properties).forEach((key) => { const property = key as keyof TBulkOperationsPayload["properties"]; const propertyValue = data.properties[property]; // update root issue map properties if (Array.isArray(propertyValue)) { // if property value is array, append it to the existing values const existingValue = issueBeforeUpdate[property]; // convert existing value to an array const newExistingValue = Array.isArray(existingValue) ? existingValue : []; this.rootIssueStore.issues.updateIssue(issueId, { [property]: uniq([newExistingValue, ...propertyValue]), }); } else { // if property value is not an array, simply update the value this.rootIssueStore.issues.updateIssue(issueId, { [property]: propertyValue, }); } }); const issueDetails = this.rootIssueStore.issues.getIssueById(issueId); this.updateIssueList(issueDetails, issueBeforeUpdate); }); }); } catch (error) { throw error; } }; /** * This method is used to add issues to a particular Cycle * @param workspaceSlug * @param projectId * @param cycleId * @param issueIds * @param fetchAddedIssues If True we make an additional call to fetch all the issues from their Ids, Since the addIssueToCycle API does not return them */ async addIssueToCycle( workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[], fetchAddedIssues = true ) { try { // Perform an APi call to add issue to cycle await this.issueService.addIssueToCycle(workspaceSlug, projectId, cycleId, { issues: issueIds, }); // if cycle Id is the current Cycle Id then call fetch parent stats if (this.cycleId === cycleId) this.fetchParentStats(workspaceSlug, projectId); // if true, fetch the issue data for all the issueIds if (fetchAddedIssues) await this.rootIssueStore.issues.getIssues(workspaceSlug, projectId, issueIds); // Update issueIds from current store runInAction(() => { // If cycle Id is the current cycle Id, then, add issue to list of issueIds if (this.cycleId === cycleId) issueIds.forEach((issueId) => this.addIssueToList(issueId)); // If cycle Id is not the current cycle Id, then, remove issue to list of issueIds else if (this.cycleId) issueIds.forEach((issueId) => this.removeIssueFromList(issueId)); }); // For Each issue update cycle Id by calling current store's update Issue, without making an API call issueIds.forEach((issueId) => { this.updateIssue(workspaceSlug, projectId, issueId, { cycle_id: cycleId }, false); }); } catch (error) { throw error; } } /** * This method is used to remove issue from a cycle * @param workspaceSlug * @param projectId * @param cycleId * @param issueId */ async removeIssueFromCycle(workspaceSlug: string, projectId: string, cycleId: string, issueId: string) { try { // Perform an APi call to remove issue from cycle await this.issueService.removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId); // if cycle Id is the current Cycle Id then call fetch parent stats if (this.cycleId === cycleId) this.fetchParentStats(workspaceSlug, projectId); runInAction(() => { // If cycle Id is the current cycle Id, then, remove issue from list of issueIds this.cycleId === cycleId && this.removeIssueFromList(issueId); }); // update Issue cycle Id to null by calling current store's update Issue, without making an API call this.updateIssue(workspaceSlug, projectId, issueId, { cycle_id: null }, false); } catch (error) { throw error; } } addCycleToIssue = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => { const issueCycleId = this.rootIssueStore.issues.getIssueById(issueId)?.cycle_id; try { // Update issueIds from current store runInAction(() => { // If cycle Id is the current cycle Id, then, add issue to list of issueIds if (this.cycleId === cycleId) this.addIssueToList(issueId); // For Each issue update cycle Id by calling current store's update Issue, without making an API call this.updateIssue(workspaceSlug, projectId, issueId, { cycle_id: cycleId }, false); }); await this.issueService.addIssueToCycle(workspaceSlug, projectId, cycleId, { issues: [issueId], }); // if cycle Id is the current Cycle Id then call fetch parent stats if (this.cycleId === cycleId) this.fetchParentStats(workspaceSlug, projectId); } catch (error) { // remove the new issue ids from the cycle issues map runInAction(() => { // If cycle Id is the current cycle Id, then, remove issue to list of issueIds if (this.cycleId === cycleId) this.removeIssueFromList(issueId); // For Each issue update cycle Id to previous value by calling current store's update Issue, without making an API call this.updateIssue(workspaceSlug, projectId, issueId, { cycle_id: issueCycleId }, false); }); throw error; } }; /* * Remove a cycle from issue * @param workspaceSlug * @param projectId * @param issueId * @returns */ removeCycleFromIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { const issueCycleId = this.rootIssueStore.issues.getIssueById(issueId)?.cycle_id; if (!issueCycleId) return; try { // perform optimistic update, update store // Update issueIds from current store runInAction(() => { // If cycle Id is the current cycle Id, then, add issue to list of issueIds if (this.cycleId === issueCycleId) this.removeIssueFromList(issueId); // For Each issue update cycle Id by calling current store's update Issue, without making an API call this.updateIssue(workspaceSlug, projectId, issueId, { cycle_id: null }, false); }); // make API call await this.issueService.removeIssueFromCycle(workspaceSlug, projectId, issueCycleId, issueId); // if cycle Id is the current Cycle Id then call fetch parent stats if (this.cycleId === issueCycleId) this.fetchParentStats(workspaceSlug, projectId); } catch (error) { // revert back changes if fails // Update issueIds from current store runInAction(() => { // If cycle Id is the current cycle Id, then, add issue to list of issueIds if (this.cycleId === issueCycleId) this.addIssueToList(issueId); // For Each issue update cycle Id by calling current store's update Issue, without making an API call this.updateIssue(workspaceSlug, projectId, issueId, { cycle_id: issueCycleId }, false); }); throw error; } }; /** * This method is used to add issues to a module * @param workspaceSlug * @param projectId * @param moduleId * @param issueIds * @param fetchAddedIssues If True we make an additional call to fetch all the issues from their Ids, Since the addIssuesToModule API does not return them */ async addIssuesToModule( workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[], fetchAddedIssues = true ) { try { // Perform an APi call to add issue to module await this.moduleService.addIssuesToModule(workspaceSlug, projectId, moduleId, { issues: issueIds, }); // if true, fetch the issue data for all the issueIds if (fetchAddedIssues) await this.rootIssueStore.issues.getIssues(workspaceSlug, projectId, issueIds); // if module Id is the current Module Id then call fetch parent stats if (this.moduleId === moduleId) this.fetchParentStats(workspaceSlug, projectId); runInAction(() => { // if module Id is the current Module Id, then, add issue to list of issueIds this.moduleId === moduleId && issueIds.forEach((issueId) => this.addIssueToList(issueId)); }); // For Each issue update module Ids by calling current store's update Issue, without making an API call issueIds.forEach((issueId) => { const issueModuleIds = get(this.rootIssueStore.issues.issuesMap, [issueId, "module_ids"]) ?? []; const updatedIssueModuleIds = uniq(concat(issueModuleIds, [moduleId])); this.updateIssue(workspaceSlug, projectId, issueId, { module_ids: updatedIssueModuleIds }, false); }); } catch (error) { throw error; } } /** * This method is used to remove issue from a module * @param workspaceSlug * @param projectId * @param moduleId * @param issueIds * @returns */ async removeIssuesFromModule(workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) { try { // Perform an APi call to remove issue to module const response = await this.moduleService.removeIssuesFromModuleBulk( workspaceSlug, projectId, moduleId, issueIds ); // if module Id is the current Module Id then call fetch parent stats if (this.moduleId === moduleId) this.fetchParentStats(workspaceSlug, projectId); runInAction(() => { // if module Id is the current Module Id, then remove issue from list of issueIds this.moduleId === moduleId && issueIds.forEach((issueId) => this.removeIssueFromList(issueId)); }); // For Each issue update module Ids by calling current store's update Issue, without making an API call runInAction(() => { issueIds.forEach((issueId) => { const issueModuleIds = get(this.rootIssueStore.issues.issuesMap, [issueId, "module_ids"]) ?? []; const updatedIssueModuleIds = pull(issueModuleIds, moduleId); this.updateIssue(workspaceSlug, projectId, issueId, { module_ids: updatedIssueModuleIds }, false); }); }); return response; } catch (error) { throw error; } } /* * change modules array in issue * @param workspaceSlug * @param projectId * @param issueId * @param addModuleIds array of modules to be added * @param removeModuleIds array of modules to be removed */ async changeModulesInIssue( workspaceSlug: string, projectId: string, issueId: string, addModuleIds: string[], removeModuleIds: string[] ) { // keep a copy of the original module ids const originalModuleIds = get(this.rootIssueStore.issues.issuesMap, [issueId, "module_ids"]) ?? []; try { runInAction(() => { // get current Module Ids of the issue let currentModuleIds = [...originalModuleIds]; // remove the new issue id to the module issues removeModuleIds.forEach((moduleId) => { // If module Id is equal to current module Id, them remove Issue from List this.moduleId === moduleId && this.removeIssueFromList(issueId); currentModuleIds = pull(currentModuleIds, moduleId); }); // If current Module Id is included in the modules list, then add Issue to List if (addModuleIds.includes(this.moduleId ?? "")) this.addIssueToList(issueId); currentModuleIds = uniq(concat([...currentModuleIds], addModuleIds)); // For current Issue, update module Ids by calling current store's update Issue, without making an API call this.updateIssue(workspaceSlug, projectId, issueId, { module_ids: currentModuleIds }, false); }); //Perform API call await this.moduleService.addModulesToIssue(workspaceSlug, projectId, issueId, { modules: addModuleIds, removed_modules: removeModuleIds, }); if (addModuleIds.includes(this.moduleId || "") || removeModuleIds.includes(this.moduleId || "")) { this.fetchParentStats(workspaceSlug, projectId); } } catch (error) { // revert the issue back to its original module ids runInAction(() => { // If current Module Id is included in the add modules list, then remove Issue from List if (addModuleIds.includes(this.moduleId ?? "")) this.removeIssueFromList(issueId); // If current Module Id is included in the removed modules list, then add Issue to List if (removeModuleIds.includes(this.moduleId ?? "")) this.addIssueToList(issueId); // For current Issue, update module Ids by calling current store's update Issue, without making an API call this.updateIssue(workspaceSlug, projectId, issueId, { module_ids: originalModuleIds }, false); }); throw error; } } /** * Add issue to the store * @param issue * @param shouldUpdateList indicates if the issue Id to be added to the list */ addIssue(issue: TIssue, shouldUpdateList = true) { runInAction(() => { this.rootIssueStore.issues.addIssue([issue]); }); // if true, add issue id to the list if (shouldUpdateList) this.updateIssueList(issue, undefined, EIssueGroupedAction.ADD); } /** * Method called to clear out the current store */ clear() { runInAction(() => { this.groupedIssueIds = undefined; this.issuePaginationData = {}; this.groupedIssueCount = {}; this.paginationOptions = undefined; }); } /** * Method called to add issue id to list. * This will only work if the issue already exists in the main issue map * @param issueId */ addIssueToList(issueId: string) { const issue = this.rootIssueStore.issues.getIssueById(issueId); this.updateIssueList(issue, undefined, EIssueGroupedAction.ADD); } /** * Method called to remove issue id from list. * This will only work if the issue already exists in the main issue map * @param issueId */ removeIssueFromList(issueId: string) { const issue = this.rootIssueStore.issues.getIssueById(issueId); this.updateIssueList(issue, undefined, EIssueGroupedAction.DELETE); } /** * Method called to update the issue list, * If an action is passed, this method would add/remove the issue from list according to the action * If there is no action, this method compares before and after states of the issue to decide, where to remove the issue id from and where to add it to * if only issue is passed down then, the method determines where to add the issue Id and updates the list * @param issue current issue state * @param issueBeforeUpdate issue state before the update * @param action specific action can be provided to force the method to that action * @returns */ updateIssueList( issue?: TIssue, issueBeforeUpdate?: TIssue, action?: EIssueGroupedAction.ADD | EIssueGroupedAction.DELETE ) { if (!issue) return; // get issueUpdates from another method by passing down the three arguments // issueUpdates is nothing but an array of objects that contain the path of the issueId list that need updating and also the action that needs to be performed at the path const issueUpdates = this.getUpdateDetails(issue, issueBeforeUpdate, action); const accumulatedUpdatesForCount = {}; runInAction(() => { // The issueUpdates for (const issueUpdate of issueUpdates) { //if update is add, add it at a particular path if (issueUpdate.action === EIssueGroupedAction.ADD) { // add issue Id at the path update(this, ["groupedIssueIds", ...issueUpdate.path], (issueIds: string[] = []) => { return this.issuesSortWithOrderBy(uniq(concat(issueIds, issue.id)), this.orderBy); }); } //if update is delete, remove it at a particular path if (issueUpdate.action === EIssueGroupedAction.DELETE) { // remove issue Id from the path update(this, ["groupedIssueIds", ...issueUpdate.path], (issueIds: string[] = []) => { return pull(issueIds, issue.id); }); } // accumulate the updates so that we don't end up updating the count twice for the same issue this.accumulateIssueUpdates(accumulatedUpdatesForCount, issueUpdate.path, issueUpdate.action); //if update is reorder, reorder it at a particular path if (issueUpdate.action === EIssueGroupedAction.REORDER) { // re-order/re-sort the issue Ids at the path update(this, ["groupedIssueIds", ...issueUpdate.path], (issueIds: string[] = []) => { return this.issuesSortWithOrderBy(issueIds, this.orderBy); }); } } // update the respective counts from the accumulation object this.updateIssueCount(accumulatedUpdatesForCount); }); } /** * 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: TIssue[]; 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: TIssue[] = []; 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, [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, [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[getGroupKey(groupId, subGroupId)]; // update the subGroupId's issue count set(this.groupedIssueCount, [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[] = []) => { return this.issuesSortWithOrderBy(uniq(concat(issueIds, groupedIssueIds as string[])), this.orderBy); }); // 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; } /** * For Every issue update, accumulate it so that when an single issue is added to two groups, it doesn't increment the total count twice * @param accumulator * @param path * @param action * @returns */ accumulateIssueUpdates( accumulator: { [key: string]: EIssueGroupedAction }, path: string[], action: EIssueGroupedAction ) { const [groupId, subGroupId] = path; if (action !== EIssueGroupedAction.ADD && action !== EIssueGroupedAction.DELETE) return; // if both groupId && subGroupId exists update the subgroup key if (subGroupId && groupId) { const groupKey = getGroupKey(groupId, subGroupId); this.updateUpdateAccumulator(accumulator, groupKey, action); } // after above, if groupId exists update the group key if (groupId) { this.updateUpdateAccumulator(accumulator, groupId, action); } // if groupId is not ALL_ISSUES then update the All_ISSUES key // (if groupId is equal to ALL_ISSUES, it would have updated in the previous condition) if (groupId !== ALL_ISSUES) { this.updateUpdateAccumulator(accumulator, ALL_ISSUES, action); } } /** * This method's job is just to check and update the accumulator key * @param accumulator accumulator object * @param key object key like, subgroupKey, Group Key or ALL_ISSUES * @param action * @returns */ updateUpdateAccumulator( accumulator: { [key: string]: EIssueGroupedAction }, key: string, action: EIssueGroupedAction ) { // if the key for accumulator is undefined, they update it with the action if (!accumulator[key]) { accumulator[key] = action; return; } // if the key for accumulator is not the current action, // Meaning if the key already has an action ADD and the current one is REMOVE, // The the key is deleted as both the actions cancel each other out if (accumulator[key] !== action) { delete accumulator[key]; } } /** * 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 used to get update Details that would be used to update the issue Ids at the path * @param issue current state of issue * @param issueBeforeUpdate state of the issue before the update * @param action optional action, to force the method to return back that action * @returns an array of object that contains the path at which issue to be updated and the action to be performed at the path */ getUpdateDetails = ( issue: Partial, issueBeforeUpdate?: Partial, action?: EIssueGroupedAction.ADD | EIssueGroupedAction.DELETE ): { path: string[]; action: EIssueGroupedAction }[] => { // check the before and after states to return if there needs to be a re-sorting of issueId list if the issue property that orderBy depends on has changed const orderByUpdates = this.getOrderByUpdateDetails(issue, issueBeforeUpdate); // if unGrouped, then return the path as ALL_ISSUES along with orderByUpdates if (!this.issueGroupKey) return action ? [{ path: [ALL_ISSUES], action }, ...orderByUpdates] : orderByUpdates; // if grouped, the get the Difference between the two issue properties (this.issueGroupKey) on which groupBy is performed const groupActionsArray = getDifference( this.getArrayStringArray(issue[this.issueGroupKey], this.groupBy), this.getArrayStringArray(issueBeforeUpdate?.[this.issueGroupKey], this.groupBy), action ); // if not subGrouped, then use the differences to construct an updateDetails Array if (!this.issueSubGroupKey) return [ ...getGroupIssueKeyActions( groupActionsArray[EIssueGroupedAction.ADD], groupActionsArray[EIssueGroupedAction.DELETE] ), ...orderByUpdates, ]; // if subGrouped, the get the Difference between the two issue properties (this.issueGroupKey) on which subGroupBy is performed const subGroupActionsArray = getDifference( this.getArrayStringArray(issue[this.issueSubGroupKey], this.subGroupBy), this.getArrayStringArray(issueBeforeUpdate?.[this.issueSubGroupKey], this.subGroupBy), action ); // Use the differences to construct an updateDetails Array return [ ...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, ]; }; /** * This method is used to get update Details that would be used to re-order/re-sort the issue Ids at the path * @param issue current state of issue * @param issueBeforeUpdate state of the issue before the update * @returns an array of object that contains the path at which issue to be re-sorted/re-ordered */ getOrderByUpdateDetails( issue: Partial | undefined, issueBeforeUpdate: Partial | undefined ): { path: string[]; action: EIssueGroupedAction.REORDER }[] { // if before and after states of the issue prop on which orderBy depends on then return and empty Array if ( !issue || !issueBeforeUpdate || !this.orderByKey || isEqual(issue[this.orderByKey], issueBeforeUpdate[this.orderByKey]) ) return []; // if they are not equal and issues are not grouped then, provide path as ALL_ISSUES if (!this.issueGroupKey) return [{ path: [ALL_ISSUES], action: EIssueGroupedAction.REORDER }]; // if they are grouped then identify the paths based on props on which group by is dependent on const issueKeyActions: { path: string[]; action: EIssueGroupedAction.REORDER }[] = []; const groupByValues = this.getArrayStringArray(issue[this.issueGroupKey]); // if issues are not subGrouped then, provide path from groupByValues if (!this.issueSubGroupKey) { for (const groupKey of groupByValues) { issueKeyActions.push({ path: [groupKey], action: EIssueGroupedAction.REORDER }); } return issueKeyActions; } // if they are grouped then identify the paths based on props on which sub group by is dependent on const subGroupByValues = this.getArrayStringArray(issue[this.issueSubGroupKey]); // if issues are subGrouped then, provide path from subGroupByValues for (const groupKey of groupByValues) { for (const subGroupKey of subGroupByValues) { issueKeyActions.push({ path: [groupKey, subGroupKey], action: EIssueGroupedAction.REORDER }); } } return issueKeyActions; } /** * get the groupByKey issue property on which actions are to be decided in the form of array * @param value * @param groupByKey * @returns an array of issue property values */ getArrayStringArray = ( value: string | string[] | undefined | null, groupByKey?: TIssueGroupByOptions | undefined ): string[] => { // if value is not defined, return empty array if (!value) return []; // if array return the array if (Array.isArray(value)) return value; // if the groupKey is state group then return the group based on state_id if (groupByKey === "state_detail.group") { return [this.rootIssueStore.rootStore.state.stateMap?.[value]?.group]; } return [value]; }; /** * 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])[0] : dataValues) : dataValues[0]; } issuesSortWithOrderBy = (issueIds: string[], key: TIssueOrderByOptions | undefined): string[] => { const issues = this.rootIssueStore.issues.getIssuesByIds(issueIds, this.isArchived ? "archived" : "un-archived"); const array = orderBy(issues, (issue) => convertToISODateString(issue["created_at"]), ["desc"]); switch (key) { case "sort_order": return getIssueIds(orderBy(array, "sort_order")); case "state__name": return getIssueIds( orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue?.["state_id"])) ); case "-state__name": return getIssueIds( orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue?.["state_id"]), ["desc"]) ); // dates case "created_at": return getIssueIds(orderBy(array, (issue) => convertToISODateString(issue["created_at"]))); case "-created_at": return getIssueIds(orderBy(array, (issue) => convertToISODateString(issue["created_at"]), ["desc"])); case "updated_at": return getIssueIds(orderBy(array, (issue) => convertToISODateString(issue["updated_at"]))); case "-updated_at": return getIssueIds(orderBy(array, (issue) => convertToISODateString(issue["updated_at"]), ["desc"])); case "start_date": return getIssueIds(orderBy(array, [getSortOrderToFilterEmptyValues.bind(null, "start_date"), "start_date"])); //preferring sorting based on empty values to always keep the empty values below case "-start_date": return getIssueIds( orderBy( array, [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 getIssueIds(orderBy(array, [getSortOrderToFilterEmptyValues.bind(null, "target_date"), "target_date"])); //preferring sorting based on empty values to always keep the empty values below case "-target_date": return getIssueIds( orderBy( array, [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 getIssueIds(orderBy(array, (currentIssue: TIssue) => indexOf(sortArray, currentIssue?.priority))); } case "-priority": { const sortArray = ISSUE_PRIORITIES.map((i) => i.key); return getIssueIds( orderBy(array, (currentIssue: TIssue) => indexOf(sortArray, currentIssue?.priority), ["desc"]) ); } // number case "attachment_count": return getIssueIds(orderBy(array, "attachment_count")); case "-attachment_count": return getIssueIds(orderBy(array, "attachment_count", ["desc"])); case "estimate_point": return getIssueIds( orderBy(array, [getSortOrderToFilterEmptyValues.bind(null, "estimate_point"), "estimate_point"]) ); //preferring sorting based on empty values to always keep the empty values below case "-estimate_point": return getIssueIds( orderBy( array, [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 getIssueIds(orderBy(array, "link_count")); case "-link_count": return getIssueIds(orderBy(array, "link_count", ["desc"])); case "sub_issues_count": return getIssueIds(orderBy(array, "sub_issues_count")); case "-sub_issues_count": return getIssueIds(orderBy(array, "sub_issues_count", ["desc"])); // Array case "labels__name": return getIssueIds( orderBy(array, [ 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 getIssueIds( orderBy( array, [ 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"), ], ["asc", "desc"] ) ); case "issue_module__module__name": return getIssueIds( orderBy(array, [ 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 getIssueIds( orderBy( array, [ 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"), ], ["asc", "desc"] ) ); case "issue_cycle__cycle__name": return getIssueIds( orderBy(array, [ 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 getIssueIds( orderBy( array, [ 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"), ], ["asc", "desc"] ) ); case "assignees__first_name": return getIssueIds( orderBy(array, [ 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 getIssueIds( orderBy( array, [ 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"), ], ["asc", "desc"] ) ); default: return getIssueIds(array); } }; /** * 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 ); }; }