mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
666d35afb9
* dev: separate order by of issue queryset to separate utilty function * dev: pagination for spreadhseet and gantt * dev: group pagination * dev: paginate single entities * dev: refactor pagination * dev: paginating issue apis * dev: grouped pagination for empty groups * dev: ungrouped list * dev: fix paginator for single groups * dev: fix paginating true list * dev: state__group pagination * fix: imports * dev: fix grouping on taget date and project_id * dev: remove unused imports * dev: add ruff in dependencies * make store changes for pagination * fix some build errors due to type changes * dev: add total pages key * chore: paginator changes * implement pagination for spreadsheet, list, kanban and calendar * fix: order by grouped pagination * dev: sub group paginator * dev: grouped paginator * dev: sub grouping paginator * restructure gantt layout charts * dev: fix pagination count * dev: date filtering for issues * dev: group by counts * implement new logic for pagination layouts * fix: label id and assignee id interchange * dev: fix priority ordering * fix group by bugs * dev: grouping for priority * fix reeordering while update * dev: fix order by for pagination * fix: total results for sub group pagination * dev: add comments and fix ordering * fix orderby priority for spreadsheet * fix subGroupCount * Fix logic for load more in Kanban * fix issue quick add * dev: fix issue creation * dev: add sorting * fix order by for modules and cycles * fix non render of Issues * fix subGroupKey generation when subGroupId is null * dev: fix cycle and module issue * dev: fix sub grouping * fix: imports * fix minor build errors * fix major build errors * fix priority order by * grouped pagination cursor logic changes * fix calendar pagination * active cycle issues pagination * dev: fix lint errors * fix Kanban subgroup dnd * fix empty subgroup kanbans * fix updation from an empty field with groupBy * fix issue count of groups * fix issue sorting on first page fetch * dev: remove pagination from list endpoint add ordering for sub grouping and handle error for empty issues * refactor module and cycle issues * fix quick add refactor * refactor gantt roots * fix empty states * fix filter params * fix group by module * minor UX changes * fix sub grouping in Kanban * remove unnecessary sorting logic in backend (Nikhil's changes) * dev: add error handling when using without on results * calendar layout loader improvement * list per page count logic change * spreadsheet loader improvement * Added loader for issues load more pagination * fix quick add in gantt * dev: add profile issue pagination * fix all issue and profile issues logic * remove empty state from calendar layout * use useEffect instead of swr to fetch issues to have quick switching between views cycles etc * dev: add aggregation for multi fields * fix priority sorting for workspace issues * fix move from draft for draft issues * fix pagination loader for spreadsheet * fetch project, module and cycle stats on update, create and delete of issues * increase horizontal margin * change load more pagination to on scroll pagination for active cycle issues * fix linting error * dev: fix ordering when order by m2m * dev: fix null paginations * dev: commenting * 0add comments to the issue stores methods * fix order by for array properties * fix: priority ordering * perform optimistic updates while adding or removing cycles or modules * fix build errors * dev: add default values when iterating through sub group * Move code from EE to CE repo * chore: folder structure updates * Move sortabla and radio input to packages/ui * chore: updated empty and loading screens * chore: delete an estimate point * chore: estimate point response change * chore: updated create estimate and handled the build error * chore: migration fixes * chore: updated create estimate * [WEB-1322] dev: conflict free pages collaboration (#4463) * chore: pages realtime * chore: empty binary response * chore: added a ypy package * feat: pages collaboration * chore: update fetching logic * chore: degrade ypy version * chore: replace useEffect fetch logic with useSWR * chore: move all the update logic to the page store * refactor: remove react-hook-form * chore: save description_html as well * chore: migrate old data logic * fix: added description_binary as field name * fix: code cleanup * refactor: create separate hook to handle page description * fix: build errors * chore: combine updates instead of using the whole document * chore: removed ypy package * chore: added conflict resolving logic to the client side * chore: add a save changes button * chore: add read-only validation * chore: remove saving state information * chore: added permission class * chore: removed the migration file * chore: corrected the model field * chore: rename pageStore to page * chore: update collaboration provider * chore: add try catch to handle error --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> * chore: create estimate workflow update * chore: editing and deleting the existing estimate updates * chore: updating the new estinates in update modal * chore: ui changed * chore: response changes of get and post * chore: new field added in estimates * chore: individual endpoint for estimate points * chore: typo changes * chore: create estimate point * chore: integrated new endpoints * chore: update key value pair * chore: update sorting in the estimates * Add custom option in the estimate templates * chore: handled current project active estimate * chore: handle estimate update worklfow * chore: AIO docker images for preview deployments (#4605) * fix: adding single docker base file * action added * fix action * dockerfile.base modified * action fix * dockerfile * fix: base aio dockerfile * fix: dockerfile.base * fix: dockerfile base * fix: modified folder structure * fix: action * fix: dockerfile * fix: dockerfile.base * fix: supervisor file name changed * fix: base dockerfile updated * fix dockerfile base * fix: base dockerfile * fix: docker files * fix: base dockerfile * update base image * modified docker aio base * aio base modified to debian-12-slim * fixes * finalize the dockerfiles with volume exposure * modified the aio build and dockerfile * fix: codacy suggestions implemented * fix: codacy fix * update aio build action --------- Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com> * chore: handled estimates switch * chore: handled estimate edit * chore: handled close button in estimate edit * chore: updated ceate estimare workflow * chore: updated switch estimate * fix minor bugs in base issues store * single column scroll pagination * UI changes for load more button * chore: UI and typos * chore: resolved build error * [WEB-1184] feat: issue bulk operations (#4530) * chore: bulk operations * chore: archive bulk issues * chore: bulk ops keys changed * chore: bulk delete and archive confirmation modals * style: list layout spacing * chore: create hoc for multi-select groups * chore: update multiple select components * chore: archive, target and start date error messsage * chore: edge case handling * chore: bulk ops in spreadsheet layout * chore: update UI * chore: scroll element into view * fix: shift + arrow navigation * chore: implement bulk ops in the gantt layout * fix: ui bugs * chore: move selection logic to store * fix: group selection * refactor: multiple select store * style: dropdowns UI * fix: bulk assignee and label update mutation * chore: removed migrations * refactor: entities grouping logic * fix performance issue is selection of bulk ops * fix: shift keyboard navigation * fix: group click action * chore: start and target date validation * chore: remove optimistic updates, check archivability in frontend * chore: code optimisation * chore: add store comments * refactor: component fragmentation * style: issue active state --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: rahulramesha <rahulramesham@gmail.com> * fix a performance issue when there are too many groups * chore: updated delete dropdown and handled the repeated values while creating and updating the estimate point * [WEB-1424] chore: page and view logo implementation, and emoji/icon picker improvement (#4583) * chore: added logo_props * chore: logo props in cycles, views and modules * chore: emoji icon picker types updated * chore: info icon added to plane ui package * chore: icon color adjust helper function added * style: icon picker ui improvement and default color options updated * chore: update page logo action added in store * chore: emoji code to unicode helper function added * chore: common logo renderer component added * chore: app header project logo updated * chore: project logo updated across platform * chore: page logo picker added * chore: control link component improvement * chore: list item improvement * chore: emoji picker component updated * chore: space app and package logo prop type updated * chore: migration * chore: logo added to project view * chore: page logo picker added in create modal and breadcrumbs * chore: view logo picker added in create modal and updated breadcrumbs * fix: build error * chore: AIO docker images for preview deployments (#4605) * fix: adding single docker base file * action added * fix action * dockerfile.base modified * action fix * dockerfile * fix: base aio dockerfile * fix: dockerfile.base * fix: dockerfile base * fix: modified folder structure * fix: action * fix: dockerfile * fix: dockerfile.base * fix: supervisor file name changed * fix: base dockerfile updated * fix dockerfile base * fix: base dockerfile * fix: docker files * fix: base dockerfile * update base image * modified docker aio base * aio base modified to debian-12-slim * fixes * finalize the dockerfiles with volume exposure * modified the aio build and dockerfile * fix: codacy suggestions implemented * fix: codacy fix * update aio build action --------- Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com> * fix: merge conflict * chore: lucide react added to planu ui package * chore: new emoji picker component added with lucid icon and code refactor * chore: logo component updated * chore: emoji picker updated for pages and views --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: Manish Gupta <59428681+mguptahub@users.noreply.github.com> Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com> * chore: handled inline errors in the estimate switch * fix module and cycle drag and drop * Fix issue count bug for accumulated actions * chore: handled active and availability vadilation * chore: handled create and update components in projecr estimates * chore: added migration * Add category specific values for custom template * chore: estimate dropdown handled in issues * chore: estimate alerts * fix bulk updates * chore: updated alerts * add optional chaining * Extract the list row actions * change color of load more to match new Issues * list group collapsible * fix: updated and handled the estimate points * fix: upgrader ee banner * Fix issues with sortable * Fix sortable spacing issue in create estimate modal * fix: updated the issue create sorting * chore: removed radio button from ui and updated in the estimates * chore: resolved import error in packaged ui * chore: handled props in create modal * chore: removed ee files * chore: changed default analytics * fix: pagination ordering for grouped and subgrouped * chore: removed the migration file * chore: estimate point value in graph * chore: estimate point key change * chore: squashed migration (#4634) * chore: squashed migration * chore: removed instance migraion * chore: key changes * chore: issue activity back migration * dev: replaced estimate key with estimate id and replaced estimate type from number to string in issue * chore: estimate point value field * chore: estimate point activity * chore: removed the unused function * chore: resolved merge conflicts * chore: deploy board keys changed * chore: yarn lock file change * chore: resolved frontend build --------- Co-authored-by: guru_sainath <gurusainath007@gmail.com> * [WEB-1516] refactor: space app routing and layouts (#4705) * dev: change layout * chore: replace workspace slug and project id with anchor * chore: migration fixes * chore: update filtering logic * chore: endpoint changes * chore: update endpoint * chore: changed url pratterns * chore: use client side for layout and page * chore: issue vote changes * chore: project deploy board response change * refactor: publish project store and components * fix: update layout options after fetching settings * chore: remove unnecessary types * style: peek overview * refactor: components folder structure * fix: redirect from old path * chore: make the whole issue block clickable * chore: removed the migration file * chore: add server side redirection for old routes * chore: is enabled key change * chore: update types * chore: removed the migration file --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> * Merge develop into revamp-estimates-ce * chore: removed migration file and updated the estimate system order and removed ee banner * chore: initial radio select in create estimate * chore: space key changes * Fix sortable component as the sort order was broken. * fix: formatting and linting errors * fix Alignment for load more * add logic to approuter * fix approuter changes and fix build * chore: removed the linting issue --------- Co-authored-by: pablohashescobar <nikhilschacko@gmail.com> Co-authored-by: Satish Gandham <satish.iitg@gmail.com> Co-authored-by: guru_sainath <gurusainath007@gmail.com> Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Co-authored-by: Manish Gupta <59428681+mguptahub@users.noreply.github.com> Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com> Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Co-authored-by: pushya22 <130810100+pushya22@users.noreply.github.com>
1820 lines
66 KiB
TypeScript
1820 lines
66 KiB
TypeScript
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<TIssueGroupByOptions, null> | "target_date";
|
|
|
|
export enum EIssueGroupedAction {
|
|
ADD = "ADD",
|
|
DELETE = "DELETE",
|
|
REORDER = "REORDER",
|
|
}
|
|
export interface IBaseIssuesStore {
|
|
// observable
|
|
loader: Record<string, TLoader>;
|
|
|
|
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<void>;
|
|
// helper methods
|
|
getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined;
|
|
issuesSortWithOrderBy(issueIds: string[], key: Partial<TIssueOrderByOptions>): 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<void>;
|
|
removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
|
|
addCycleToIssue: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
|
|
removeCycleFromIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
|
|
|
addIssuesToModule: (
|
|
workspaceSlug: string,
|
|
projectId: string,
|
|
moduleId: string,
|
|
issueIds: string[],
|
|
fetchAddedIssues?: boolean
|
|
) => Promise<void>;
|
|
removeIssuesFromModule: (
|
|
workspaceSlug: string,
|
|
projectId: string,
|
|
moduleId: string,
|
|
issueIds: string[]
|
|
) => Promise<void>;
|
|
changeModulesInIssue(
|
|
workspaceSlug: string,
|
|
projectId: string,
|
|
issueId: string,
|
|
addModuleIds: string[],
|
|
removeModuleIds: string[]
|
|
): Promise<void>;
|
|
}
|
|
|
|
// This constant maps the group by keys to the respective issue property that the key relies on
|
|
const ISSUE_GROUP_BY_KEY: 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",
|
|
};
|
|
|
|
export const ISSUE_FILTER_DEFAULT_DATA: Record<TIssueDisplayFilterOptions, keyof TIssue> = {
|
|
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<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 abstract class BaseIssuesStore implements IBaseIssuesStore {
|
|
loader: Record<string, TLoader> = {};
|
|
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<TIssue>,
|
|
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<TIssue>,
|
|
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<TIssue>) {
|
|
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<TIssue>) {
|
|
// 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<TIssue>,
|
|
issueBeforeUpdate?: Partial<TIssue>,
|
|
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<TIssue> | undefined,
|
|
issueBeforeUpdate: Partial<TIssue> | 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
|
|
);
|
|
};
|
|
}
|