mirror of
https://github.com/makeplane/plane
synced 2025-08-07 19:59:33 +00:00
Compare commits
1 Commits
chore-name
...
feat-mobx-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5546bd2305 |
1
packages/types/src/index.d.ts
vendored
1
packages/types/src/index.d.ts
vendored
@@ -37,3 +37,4 @@ export * from "./command-palette";
|
||||
export * from "./timezone";
|
||||
export * from "./activity";
|
||||
export * from "./epics";
|
||||
export * from "./local-db";
|
||||
|
||||
15
packages/types/src/local-db.d.ts
vendored
Normal file
15
packages/types/src/local-db.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
import { TIssue } from "./issues/issue";
|
||||
|
||||
export type TIssueSyncEvent = {
|
||||
type: "issues:sync";
|
||||
data: TIssue[];
|
||||
};
|
||||
|
||||
export type TIssueRemoveEvent = {
|
||||
type: "issues:remove";
|
||||
data: string[];
|
||||
};
|
||||
|
||||
export type TIssueBroadcastEvent = {
|
||||
data: (TIssueSyncEvent | TIssueRemoveEvent) & { workspaceSlug: string; projectId: string };
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { EIssuesStoreType } from "@plane/constants";
|
||||
import { IProjectIssues, ProjectIssues } from "@/store/issue/project";
|
||||
import { IIssueRootStore } from "@/store/issue/root.store";
|
||||
import { IProjectEpicsFilter } from "./filter.store";
|
||||
@@ -9,6 +10,6 @@ export type IProjectEpics = IProjectIssues;
|
||||
// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
|
||||
export class ProjectEpics extends ProjectIssues implements IProjectEpics {
|
||||
constructor(_rootStore: IIssueRootStore, issueFilterStore: IProjectEpicsFilter) {
|
||||
super(_rootStore, issueFilterStore);
|
||||
super(_rootStore, issueFilterStore, EIssuesStoreType.EPIC);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { EIssuesStoreType } from "@plane/constants";
|
||||
import { IProjectViewIssues, ProjectViewIssues } from "@/store/issue/project-views";
|
||||
import { IIssueRootStore } from "@/store/issue/root.store";
|
||||
import { ITeamViewIssuesFilter } from "./filter.store";
|
||||
@@ -8,6 +9,6 @@ export type ITeamViewIssues = IProjectViewIssues;
|
||||
// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
|
||||
export class TeamViewIssues extends ProjectViewIssues implements IProjectViewIssues {
|
||||
constructor(_rootStore: IIssueRootStore, teamViewFilterStore: ITeamViewIssuesFilter) {
|
||||
super(_rootStore, teamViewFilterStore);
|
||||
super(_rootStore, teamViewFilterStore, EIssuesStoreType.TEAM_VIEW);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { EIssuesStoreType } from "@plane/constants";
|
||||
import { IProjectIssues, ProjectIssues } from "@/store/issue/project";
|
||||
import { IIssueRootStore } from "@/store/issue/root.store";
|
||||
import { ITeamIssuesFilter } from "./filter.store";
|
||||
@@ -8,6 +9,6 @@ export type ITeamIssues = IProjectIssues;
|
||||
// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
|
||||
export class TeamIssues extends ProjectIssues implements IProjectIssues {
|
||||
constructor(_rootStore: IIssueRootStore, teamIssueFilterStore: ITeamIssuesFilter) {
|
||||
super(_rootStore, teamIssueFilterStore);
|
||||
super(_rootStore, teamIssueFilterStore, EIssuesStoreType.TEAM);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
TIssuePriorities,
|
||||
TIssueGroupingFilters,
|
||||
ILayoutDisplayFiltersOptions,
|
||||
IIssueFilterOptions,
|
||||
TIssue,
|
||||
} from "@plane/types";
|
||||
import { ADDITIONAL_ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/plane-web/constants";
|
||||
|
||||
@@ -525,3 +527,16 @@ export const groupReactionEmojis = (reactions: any) => {
|
||||
|
||||
return groupedEmojis;
|
||||
};
|
||||
|
||||
// Map filter keys to their corresponding issue property keys
|
||||
export const FILTER_TO_ISSUE_MAP: Partial<Record<keyof IIssueFilterOptions, keyof TIssue>> = {
|
||||
assignees: "assignee_ids",
|
||||
created_by: "created_by",
|
||||
labels: "label_ids",
|
||||
priority: "priority",
|
||||
cycle: "cycle_id",
|
||||
module: "module_ids",
|
||||
project: "project_id",
|
||||
state: "state_id",
|
||||
issue_type: "type_id",
|
||||
} as const;
|
||||
|
||||
@@ -19,6 +19,8 @@ import { runQuery } from "./utils/query-executor";
|
||||
import { createTables } from "./utils/tables";
|
||||
import { clearOPFS, getGroupedIssueResults, getSubGroupedIssueResults, log, logError } from "./utils/utils";
|
||||
|
||||
const syncChannel = new BroadcastChannel(`hm-sync`);
|
||||
|
||||
const DB_VERSION = 1;
|
||||
const PAGE_SIZE = 500;
|
||||
const BATCH_SIZE = 50;
|
||||
@@ -39,7 +41,10 @@ export class Storage {
|
||||
this.db = null;
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("beforeunload", this.closeDBConnection);
|
||||
window.addEventListener("beforeunload", () => {
|
||||
this.closeDBConnection();
|
||||
syncChannel?.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +227,11 @@ export class Storage {
|
||||
const start = performance.now();
|
||||
const issueService = new IssueService();
|
||||
|
||||
const syncedIssues = [];
|
||||
const response = await issueService.getIssuesForSync(this.workspaceSlug, projectId, queryParams);
|
||||
if (response.total_results > 0 && Array.isArray(response.results)) {
|
||||
syncedIssues.push(...response.results);
|
||||
}
|
||||
|
||||
await addIssuesBulk(response.results, BATCH_SIZE);
|
||||
if (response.total_pages > 1) {
|
||||
@@ -234,11 +243,28 @@ export class Storage {
|
||||
const pages = await Promise.all(promiseArray);
|
||||
for (const page of pages) {
|
||||
await addIssuesBulk(page.results, BATCH_SIZE);
|
||||
if (page.total_results > 0 && Array.isArray(page.results)) {
|
||||
syncedIssues.push(...page.results);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Broadcast issue sync event
|
||||
syncChannel.postMessage({
|
||||
type: "issues:sync",
|
||||
workspaceSlug: this.workspaceSlug,
|
||||
projectId,
|
||||
data: syncedIssues,
|
||||
});
|
||||
|
||||
if (syncedAt) {
|
||||
await syncDeletesToLocal(this.workspaceSlug, projectId, { updated_at__gt: syncedAt });
|
||||
const deletedIssues = await syncDeletesToLocal(this.workspaceSlug, projectId, { updated_at__gt: syncedAt });
|
||||
// Broadcast deleted issue remove event
|
||||
syncChannel.postMessage({
|
||||
type: "issues:remove",
|
||||
workspaceSlug: this.workspaceSlug,
|
||||
projectId,
|
||||
data: deletedIssues,
|
||||
});
|
||||
}
|
||||
log("### Time taken to add issues", performance.now() - start);
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ export const syncDeletesToLocal = async (workspaceId: string, projectId: string,
|
||||
if (Array.isArray(response)) {
|
||||
response.map(async (issue) => deleteIssueFromLocal(issue));
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
const stageIssueInserts = async (issue: any) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { action, makeObservable, runInAction } from "mobx";
|
||||
// base class
|
||||
import { EIssuesStoreType } from "@plane/constants";
|
||||
import { TLoader, IssuePaginationOptions, TIssuesResponse, ViewFlags, TBulkOperationsPayload } from "@plane/types";
|
||||
// services
|
||||
// types
|
||||
@@ -51,7 +52,9 @@ export class ArchivedIssues extends BaseIssuesStore implements IArchivedIssues {
|
||||
};
|
||||
|
||||
constructor(_rootStore: IIssueRootStore, issueFilterStore: IArchivedIssuesFilter) {
|
||||
super(_rootStore, issueFilterStore, true);
|
||||
super(_rootStore, issueFilterStore, EIssuesStoreType.ARCHIVED, {
|
||||
isArchived: true,
|
||||
});
|
||||
makeObservable(this, {
|
||||
// action
|
||||
fetchIssues: action,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { action, observable, makeObservable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// types
|
||||
// plane constants
|
||||
import { ALL_ISSUES } from "@plane/constants";
|
||||
import { ALL_ISSUES, EIssuesStoreType } from "@plane/constants";
|
||||
import {
|
||||
TIssue,
|
||||
TLoader,
|
||||
@@ -111,7 +111,9 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
|
||||
issueFilterStore;
|
||||
|
||||
constructor(_rootStore: IIssueRootStore, issueFilterStore: ICycleIssuesFilter) {
|
||||
super(_rootStore, issueFilterStore);
|
||||
super(_rootStore, issueFilterStore, EIssuesStoreType.CYCLE, {
|
||||
isUsingLocalDB: true,
|
||||
});
|
||||
makeObservable(this, {
|
||||
// observable
|
||||
activeCycleIds: observable,
|
||||
|
||||
@@ -2,7 +2,15 @@ import { action, makeObservable, runInAction } from "mobx";
|
||||
// base class
|
||||
// services
|
||||
// types
|
||||
import { TIssue, TLoader, ViewFlags, IssuePaginationOptions, TIssuesResponse, TBulkOperationsPayload } from "@plane/types";
|
||||
import { EIssuesStoreType } from "@plane/constants";
|
||||
import {
|
||||
TIssue,
|
||||
TLoader,
|
||||
ViewFlags,
|
||||
IssuePaginationOptions,
|
||||
TIssuesResponse,
|
||||
TBulkOperationsPayload,
|
||||
} from "@plane/types";
|
||||
import { BaseIssuesStore, IBaseIssuesStore } from "../helpers/base-issues.store";
|
||||
import { IIssueRootStore } from "../root.store";
|
||||
import { IDraftIssuesFilter } from "./filter.store";
|
||||
@@ -50,7 +58,7 @@ export class DraftIssues extends BaseIssuesStore implements IDraftIssues {
|
||||
issueFilterStore: IDraftIssuesFilter;
|
||||
|
||||
constructor(_rootStore: IIssueRootStore, issueFilterStore: IDraftIssuesFilter) {
|
||||
super(_rootStore, issueFilterStore);
|
||||
super(_rootStore, issueFilterStore, EIssuesStoreType.DRAFT);
|
||||
makeObservable(this, {
|
||||
// action
|
||||
fetchIssues: action,
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import uniq from "lodash/uniq";
|
||||
import { ALL_ISSUES } from "@plane/constants";
|
||||
import { TIssue } from "@plane/types";
|
||||
// plane imports
|
||||
import { ALL_ISSUES, EIssuesStoreType } from "@plane/constants";
|
||||
import { IIssueDisplayFilterOptions, IIssueFilterOptions, TIssue } from "@plane/types";
|
||||
// constants
|
||||
import { FILTER_TO_ISSUE_MAP } from "@/constants/issue";
|
||||
// helpers
|
||||
import { checkDateCriteria, parseDateFilter } from "@/helpers/date-time.helper";
|
||||
// store
|
||||
import { store } from "@/lib/store-context";
|
||||
import { EIssueGroupedAction } from "./base-issues.store";
|
||||
|
||||
/**
|
||||
@@ -173,3 +181,125 @@ export const getSortOrderToFilterEmptyValues = (key: string, object: any) => {
|
||||
|
||||
// get IssueIds from Issue data List
|
||||
export const getIssueIds = (issues: TIssue[]) => issues.map((issue) => issue?.id);
|
||||
|
||||
/**
|
||||
* Helper method to get the active issue store type
|
||||
* @returns The active issue store type
|
||||
*/
|
||||
export const getActiveIssueStoreType = () => {
|
||||
const { globalViewId, viewId, projectId, cycleId, moduleId, userId, epicId, teamId } = store.router;
|
||||
|
||||
// Check the router store to determine the active issue store
|
||||
if (globalViewId) return EIssuesStoreType.GLOBAL;
|
||||
|
||||
if (userId) return EIssuesStoreType.PROFILE;
|
||||
|
||||
if (teamId && viewId) return EIssuesStoreType.TEAM_VIEW;
|
||||
|
||||
if (teamId) return EIssuesStoreType.TEAM;
|
||||
|
||||
if (projectId && viewId) return EIssuesStoreType.PROJECT_VIEW;
|
||||
|
||||
if (cycleId) return EIssuesStoreType.CYCLE;
|
||||
|
||||
if (moduleId) return EIssuesStoreType.MODULE;
|
||||
|
||||
if (epicId) return EIssuesStoreType.EPIC;
|
||||
|
||||
if (projectId) return EIssuesStoreType.PROJECT;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper method to determine if the current issue store is active
|
||||
* @param currentStoreType - The current issue store type
|
||||
* @returns true if the current issue store is active, false otherwise
|
||||
*/
|
||||
export const isCurrentIssueStoreActive = (currentStoreType: EIssuesStoreType) => {
|
||||
const activeStoreType: EIssuesStoreType | undefined = getActiveIssueStoreType();
|
||||
return currentStoreType === activeStoreType;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper method to get previous issues state
|
||||
* @param issues - The array of issues to get the previous state for.
|
||||
* @returns The previous state of the issues.
|
||||
*/
|
||||
export const getPreviousIssuesState = (issues: TIssue[]) => {
|
||||
const issueIds = issues.map((issue) => issue.id);
|
||||
const issuesPreviousState: Record<string, TIssue> = {};
|
||||
issueIds.forEach((issueId) => {
|
||||
if (store.issue.issues.issuesMap[issueId]) {
|
||||
issuesPreviousState[issueId] = cloneDeep(store.issue.issues.issuesMap[issueId]);
|
||||
}
|
||||
});
|
||||
return issuesPreviousState;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if an issue meets the date filter criteria
|
||||
* @param issue The issue to check
|
||||
* @param filterKey The date field to check ('start_date' or 'target_date')
|
||||
* @param dateFilters Array of date filter strings
|
||||
* @returns boolean indicating if the issue meets the date criteria
|
||||
*/
|
||||
export const checkIssueDateFilter = (
|
||||
issue: TIssue,
|
||||
filterKey: "start_date" | "target_date",
|
||||
dateFilters: string[]
|
||||
): boolean => {
|
||||
if (!dateFilters || dateFilters.length === 0) return true;
|
||||
|
||||
const issueDate = issue[filterKey];
|
||||
if (!issueDate) return false;
|
||||
|
||||
// Issue should match all the date filters (AND operation)
|
||||
return dateFilters.every((filterValue) => {
|
||||
const { type, date } = parseDateFilter(filterValue);
|
||||
return checkDateCriteria(new Date(issueDate), date, type);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters the given issues based on the provided filters and display filters.
|
||||
* @param issues - The array of issues to be filtered.
|
||||
* @param filters - The filters to be applied to the issues.
|
||||
* @param displayFilters - The display filters to be applied to the issues.
|
||||
* @returns The filtered array of issues.
|
||||
*/
|
||||
export const getFilteredIssues = (
|
||||
issues: TIssue[],
|
||||
filters: IIssueFilterOptions | undefined,
|
||||
displayFilters: IIssueDisplayFilterOptions | undefined
|
||||
): TIssue[] => {
|
||||
if (!filters) return issues;
|
||||
// Get all active filters
|
||||
const activeFilters = Object.entries(filters).filter(([, value]) => value && value.length > 0);
|
||||
|
||||
return issues.filter((issue) => {
|
||||
// Handle sub-issue display filter
|
||||
if (issue.parent_id !== null && displayFilters?.sub_issue === false) {
|
||||
return false;
|
||||
}
|
||||
// If no active filters, return all issues
|
||||
if (activeFilters.length === 0) {
|
||||
return true;
|
||||
}
|
||||
// Check all filter conditions (AND operation between different filters)
|
||||
return activeFilters.every(([filterKey, filterValues]) => {
|
||||
// Handle date filters separately
|
||||
if (filterKey === "start_date" || filterKey === "target_date") {
|
||||
return checkIssueDateFilter(issue, filterKey as "start_date" | "target_date", filterValues as string[]);
|
||||
}
|
||||
// Handle regular filters
|
||||
const issueKey = FILTER_TO_ISSUE_MAP[filterKey as keyof IIssueFilterOptions];
|
||||
if (!issueKey) return true; // Skip if no mapping exists
|
||||
const issueValue = issue[issueKey as keyof TIssue];
|
||||
// Handle array-based properties vs single value properties
|
||||
if (Array.isArray(issueValue)) {
|
||||
return filterValues!.some((filterValue: any) => issueValue.includes(filterValue));
|
||||
} else {
|
||||
return filterValues!.includes(issueValue as string);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import clone from "lodash/clone";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import concat from "lodash/concat";
|
||||
import get from "lodash/get";
|
||||
import indexOf from "lodash/indexOf";
|
||||
@@ -13,7 +14,7 @@ import update from "lodash/update";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// plane constants
|
||||
import { EIssueLayoutTypes, ALL_ISSUES, EIssueServiceType } from "@plane/constants";
|
||||
import { EIssueLayoutTypes, ALL_ISSUES, EIssueServiceType, EIssuesStoreType } from "@plane/constants";
|
||||
// types
|
||||
import {
|
||||
TIssue,
|
||||
@@ -29,6 +30,9 @@ import {
|
||||
TGroupedIssueCount,
|
||||
TPaginationData,
|
||||
TBulkOperationsPayload,
|
||||
TIssueBroadcastEvent,
|
||||
TIssueSyncEvent,
|
||||
TIssueRemoveEvent,
|
||||
} from "@plane/types";
|
||||
// components
|
||||
import { IBlockUpdateDependencyData } from "@/components/gantt-chart";
|
||||
@@ -47,11 +51,14 @@ import { ModuleService } from "@/services/module.service";
|
||||
import { IIssueRootStore } from "../root.store";
|
||||
import {
|
||||
getDifference,
|
||||
getFilteredIssues,
|
||||
getGroupIssueKeyActions,
|
||||
getGroupKey,
|
||||
getIssueIds,
|
||||
getPreviousIssuesState,
|
||||
getSortOrderToFilterEmptyValues,
|
||||
getSubGroupIssueKeyActions,
|
||||
isCurrentIssueStoreActive,
|
||||
} from "./base-issues-utils";
|
||||
import { IBaseIssueFilterStore } from "./issue-filter-helper.store";
|
||||
|
||||
@@ -62,6 +69,13 @@ export enum EIssueGroupedAction {
|
||||
DELETE = "DELETE",
|
||||
REORDER = "REORDER",
|
||||
}
|
||||
|
||||
export type TBaseIssueStoreOptions = {
|
||||
isArchived?: boolean;
|
||||
serviceType?: EIssueServiceType;
|
||||
isUsingLocalDB?: boolean;
|
||||
};
|
||||
|
||||
export interface IBaseIssuesStore {
|
||||
// observable
|
||||
loader: Record<string, TLoader>;
|
||||
@@ -180,6 +194,8 @@ const ISSUE_ORDERBY_KEY: Record<TIssueOrderByOptions, keyof TIssue> = {
|
||||
"-sub_issues_count": "sub_issues_count",
|
||||
};
|
||||
|
||||
const syncChannel = new BroadcastChannel(`hm-sync`);
|
||||
|
||||
export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
loader: Record<string, TLoader> = {};
|
||||
groupedIssueIds: TIssues | undefined = undefined;
|
||||
@@ -188,7 +204,7 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
groupedIssueCount: TGroupedIssueCount = {};
|
||||
//
|
||||
paginationOptions: IssuePaginationOptions | undefined = undefined;
|
||||
|
||||
storeType: EIssuesStoreType;
|
||||
isArchived: boolean;
|
||||
|
||||
// services
|
||||
@@ -206,8 +222,12 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
constructor(
|
||||
_rootStore: IIssueRootStore,
|
||||
issueFilterStore: IBaseIssueFilterStore,
|
||||
isArchived = false,
|
||||
serviceType = EIssueServiceType.ISSUES
|
||||
storeType: EIssuesStoreType,
|
||||
options: TBaseIssueStoreOptions = {
|
||||
isArchived: false,
|
||||
serviceType: EIssueServiceType.ISSUES,
|
||||
isUsingLocalDB: false,
|
||||
}
|
||||
) {
|
||||
makeObservable(this, {
|
||||
// observable
|
||||
@@ -257,11 +277,12 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
removeIssuesFromModule: action.bound,
|
||||
changeModulesInIssue: action.bound,
|
||||
});
|
||||
const { isArchived = false, serviceType = EIssueServiceType.ISSUES, isUsingLocalDB = false } = options;
|
||||
this.rootIssueStore = _rootStore;
|
||||
this.issueFilterStore = issueFilterStore;
|
||||
|
||||
this.storeType = storeType;
|
||||
this.isArchived = isArchived;
|
||||
|
||||
// services
|
||||
this.issueService = new IssueService(serviceType);
|
||||
this.issueArchiveService = new IssueArchiveService();
|
||||
this.issueDraftService = new IssueDraftService();
|
||||
@@ -269,6 +290,19 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
this.cycleService = new CycleService();
|
||||
|
||||
this.controller = new AbortController();
|
||||
|
||||
syncChannel.addEventListener("message", (event: TIssueBroadcastEvent) => {
|
||||
const isStoreActive = isCurrentIssueStoreActive(this.storeType);
|
||||
if (isUsingLocalDB && isStoreActive) {
|
||||
// Get the current workspace and project details
|
||||
const { workspaceSlug } = this.rootIssueStore.rootStore.router;
|
||||
// Ignore the broadcast message if it's not from the current workspace
|
||||
if (event.data.workspaceSlug !== workspaceSlug || event.data.projectId !== this.rootIssueStore.projectId) {
|
||||
return;
|
||||
}
|
||||
this.syncIssueStore(event.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Abstract class to be implemented to fetch parent stats such as project, module or cycle details
|
||||
@@ -276,6 +310,67 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
|
||||
abstract updateParentStats: (prevIssueState?: TIssue, nextIssueState?: TIssue, id?: string) => void;
|
||||
|
||||
// ------------- Local DB Sync Start --------------
|
||||
|
||||
// Helper method to get current filters
|
||||
getCurrentFilters = () => {
|
||||
// Get the current instance filters
|
||||
const filters = cloneDeep(this.issueFilterStore?.issueFilters?.filters);
|
||||
// Add current module and cycle to filters if present
|
||||
if (filters) {
|
||||
if (this.moduleId) {
|
||||
update(filters, "module", (value) => (value ? [...value, this.moduleId] : [this.moduleId]));
|
||||
}
|
||||
if (this.cycleId) {
|
||||
update(filters, "cycle", (value) => (value ? [...value, this.cycleId] : [this.cycleId]));
|
||||
}
|
||||
}
|
||||
return filters;
|
||||
};
|
||||
|
||||
// Helper method to handle issue store sync
|
||||
syncIssueStore = (event: TIssueSyncEvent | TIssueRemoveEvent) => {
|
||||
const eventType = event.type;
|
||||
switch (eventType) {
|
||||
case "issues:sync": {
|
||||
const issues: TIssue[] = event.data;
|
||||
const issuesPreviousState = getPreviousIssuesState(issues);
|
||||
// Get the current instance filters
|
||||
const currentFilters = this.getCurrentFilters();
|
||||
// Filter issues based on currently applied filters
|
||||
const filteredIssues = getFilteredIssues(
|
||||
issues,
|
||||
currentFilters,
|
||||
this.issueFilterStore?.issueFilters?.displayFilters
|
||||
);
|
||||
runInAction(() => {
|
||||
// Add or update issues in issueMap
|
||||
this.rootIssueStore.issues.addIssue(issues);
|
||||
// Update issue list based on filtered issues
|
||||
for (const issue of filteredIssues) {
|
||||
const prevIssueState = issuesPreviousState[issue.id];
|
||||
this.updateIssueList(issue, prevIssueState, !prevIssueState ? EIssueGroupedAction.ADD : undefined);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "issues:remove": {
|
||||
const issueIds: string[] = event.data;
|
||||
runInAction(() => {
|
||||
issueIds.forEach((issueId) => {
|
||||
// Remove issue from issue list
|
||||
this.removeIssueFromList(issueId);
|
||||
// Remove issue from issue map
|
||||
this.rootIssueStore.issues.removeIssue(issueId);
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ------------- Local DB Sync End --------------
|
||||
|
||||
// current Module Id from url
|
||||
get moduleId() {
|
||||
return this.rootIssueStore.moduleId;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { action, makeObservable, runInAction } from "mobx";
|
||||
import { EIssuesStoreType } from "@plane/constants";
|
||||
// base class
|
||||
import {
|
||||
TIssue,
|
||||
@@ -64,7 +65,9 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues {
|
||||
issueFilterStore: IModuleIssuesFilter;
|
||||
|
||||
constructor(_rootStore: IIssueRootStore, issueFilterStore: IModuleIssuesFilter) {
|
||||
super(_rootStore, issueFilterStore);
|
||||
super(_rootStore, issueFilterStore, EIssuesStoreType.MODULE, {
|
||||
isUsingLocalDB: true,
|
||||
});
|
||||
|
||||
makeObservable(this, {
|
||||
// action
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { action, observable, makeObservable, computed, runInAction } from "mobx";
|
||||
// base class
|
||||
import { TIssue, TLoader, IssuePaginationOptions, TIssuesResponse, ViewFlags, TBulkOperationsPayload } from "@plane/types";
|
||||
import { EIssuesStoreType } from "@plane/constants";
|
||||
import {
|
||||
TIssue,
|
||||
TLoader,
|
||||
IssuePaginationOptions,
|
||||
TIssuesResponse,
|
||||
ViewFlags,
|
||||
TBulkOperationsPayload,
|
||||
} from "@plane/types";
|
||||
import { UserService } from "@/services/user.service";
|
||||
|
||||
// services
|
||||
@@ -53,7 +61,7 @@ export class ProfileIssues extends BaseIssuesStore implements IProfileIssues {
|
||||
userService;
|
||||
|
||||
constructor(_rootStore: IIssueRootStore, issueFilterStore: IProfileIssuesFilter) {
|
||||
super(_rootStore, issueFilterStore);
|
||||
super(_rootStore, issueFilterStore, EIssuesStoreType.PROFILE);
|
||||
makeObservable(this, {
|
||||
// observable
|
||||
currentView: observable.ref,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { action, makeObservable, runInAction } from "mobx";
|
||||
import { EIssuesStoreType } from "@plane/constants";
|
||||
// base class
|
||||
import {
|
||||
TIssue,
|
||||
@@ -56,8 +57,16 @@ export class ProjectViewIssues extends BaseIssuesStore implements IProjectViewIs
|
||||
//filter store
|
||||
issueFilterStore: IProjectViewIssuesFilter;
|
||||
|
||||
constructor(_rootStore: IIssueRootStore, issueFilterStore: IProjectViewIssuesFilter) {
|
||||
super(_rootStore, issueFilterStore);
|
||||
constructor(
|
||||
_rootStore: IIssueRootStore,
|
||||
issueFilterStore: IProjectViewIssuesFilter,
|
||||
storeType: EIssuesStoreType,
|
||||
isUsingLocalDB = false
|
||||
) {
|
||||
// Intentionally setting storeType and isUsingLocalDB using constructor props as this store is extended by other stores as well.
|
||||
super(_rootStore, issueFilterStore, storeType, {
|
||||
isUsingLocalDB,
|
||||
});
|
||||
makeObservable(this, {
|
||||
// action
|
||||
fetchIssues: action,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { action, makeObservable, runInAction } from "mobx";
|
||||
// types
|
||||
import { EIssuesStoreType } from "@plane/constants";
|
||||
import {
|
||||
TIssue,
|
||||
TLoader,
|
||||
@@ -56,8 +57,16 @@ export class ProjectIssues extends BaseIssuesStore implements IProjectIssues {
|
||||
// filter store
|
||||
issueFilterStore: IProjectIssuesFilter;
|
||||
|
||||
constructor(_rootStore: IIssueRootStore, issueFilterStore: IProjectIssuesFilter) {
|
||||
super(_rootStore, issueFilterStore);
|
||||
constructor(
|
||||
_rootStore: IIssueRootStore,
|
||||
issueFilterStore: IProjectIssuesFilter,
|
||||
storeType: EIssuesStoreType,
|
||||
isUsingLocalDB = false
|
||||
) {
|
||||
// Intentionally setting storeType and isUsingLocalDB using constructor props as this store is extended by other stores as well.
|
||||
super(_rootStore, issueFilterStore, storeType, {
|
||||
isUsingLocalDB,
|
||||
});
|
||||
makeObservable(this, {
|
||||
fetchIssues: action,
|
||||
fetchNextIssues: action,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import { autorun, makeObservable, observable } from "mobx";
|
||||
// types
|
||||
import { EIssueServiceType } from "@plane/constants";
|
||||
import { EIssueServiceType, EIssuesStoreType } from "@plane/constants";
|
||||
import { ICycle, IIssueLabel, IModule, IProject, IState, IUserLite, TIssueServiceType } from "@plane/types";
|
||||
// plane web store
|
||||
import { IProjectEpics, IProjectEpicsFilter, ProjectEpics, ProjectEpicsFilter } from "@/plane-web/store/issue/epic";
|
||||
@@ -238,7 +238,7 @@ export class IssueRootStore implements IIssueRootStore {
|
||||
this.workspaceDraftIssues = new WorkspaceDraftIssues(this);
|
||||
|
||||
this.projectIssuesFilter = new ProjectIssuesFilter(this);
|
||||
this.projectIssues = new ProjectIssues(this, this.projectIssuesFilter);
|
||||
this.projectIssues = new ProjectIssues(this, this.projectIssuesFilter, EIssuesStoreType.PROJECT, true);
|
||||
|
||||
this.teamIssuesFilter = new TeamIssuesFilter(this);
|
||||
this.teamIssues = new TeamIssues(this, this.teamIssuesFilter);
|
||||
@@ -253,7 +253,12 @@ export class IssueRootStore implements IIssueRootStore {
|
||||
this.teamViewIssues = new TeamViewIssues(this, this.teamViewIssuesFilter);
|
||||
|
||||
this.projectViewIssuesFilter = new ProjectViewIssuesFilter(this);
|
||||
this.projectViewIssues = new ProjectViewIssues(this, this.projectViewIssuesFilter);
|
||||
this.projectViewIssues = new ProjectViewIssues(
|
||||
this,
|
||||
this.projectViewIssuesFilter,
|
||||
EIssuesStoreType.PROJECT_VIEW,
|
||||
true
|
||||
);
|
||||
|
||||
this.archivedIssuesFilter = new ArchivedIssuesFilter(this);
|
||||
this.archivedIssues = new ArchivedIssues(this, this.archivedIssuesFilter);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { action, makeObservable, runInAction } from "mobx";
|
||||
// base class
|
||||
import { EIssuesStoreType } from "@plane/constants";
|
||||
import {
|
||||
IssuePaginationOptions,
|
||||
TBulkOperationsPayload,
|
||||
@@ -60,7 +61,7 @@ export class WorkspaceIssues extends BaseIssuesStore implements IWorkspaceIssues
|
||||
issueFilterStore;
|
||||
|
||||
constructor(_rootStore: IIssueRootStore, issueFilterStore: IWorkspaceIssuesFilter) {
|
||||
super(_rootStore, issueFilterStore);
|
||||
super(_rootStore, issueFilterStore, EIssuesStoreType.GLOBAL);
|
||||
|
||||
makeObservable(this, {
|
||||
// action
|
||||
|
||||
@@ -405,3 +405,66 @@ export const generateDateArray = (startDate: string | Date, endDate: string | Da
|
||||
|
||||
return dateArray;
|
||||
};
|
||||
|
||||
/**
|
||||
* Processes relative date strings like "1_weeks", "2_months" etc and returns a Date
|
||||
* @param value The relative date string (e.g., "1_weeks", "2_months")
|
||||
* @returns Date object representing the calculated date
|
||||
*/
|
||||
export const processRelativeDate = (value: string): Date => {
|
||||
const [amount, unit] = value.split("_");
|
||||
const date = new Date();
|
||||
|
||||
switch (unit) {
|
||||
case "weeks":
|
||||
date.setDate(date.getDate() + parseInt(amount) * 7);
|
||||
break;
|
||||
case "months":
|
||||
date.setMonth(date.getMonth() + parseInt(amount));
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported time unit: ${unit}`);
|
||||
}
|
||||
|
||||
return date;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a date filter string and returns the comparison type and date
|
||||
* @param filterValue The date filter string (e.g., "1_weeks;after;fromnow" or "2024-12-01;after")
|
||||
* @returns Object containing the comparison type and target date
|
||||
*/
|
||||
export const parseDateFilter = (filterValue: string): { type: "after" | "before"; date: Date } => {
|
||||
const parts = filterValue.split(";");
|
||||
const dateStr = parts[0];
|
||||
const type = parts[1] as "after" | "before";
|
||||
|
||||
let date: Date;
|
||||
if (dateStr.includes("_")) {
|
||||
// Handle relative dates (e.g., "1_weeks;after;fromnow")
|
||||
date = processRelativeDate(dateStr);
|
||||
} else {
|
||||
// Handle absolute dates (e.g., "2024-12-01;after")
|
||||
date = new Date(dateStr);
|
||||
}
|
||||
|
||||
return { type, date };
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a date meets the filter criteria
|
||||
* @param dateToCheck The date to check
|
||||
* @param filterDate The filter date to compare against
|
||||
* @param type The type of comparison ('after' or 'before')
|
||||
* @returns boolean indicating if the date meets the criteria
|
||||
*/
|
||||
export const checkDateCriteria = (dateToCheck: Date | null, filterDate: Date, type: "after" | "before"): boolean => {
|
||||
if (!dateToCheck) return false;
|
||||
|
||||
const checkDate = new Date(dateToCheck);
|
||||
// Reset time components for date-only comparison
|
||||
checkDate.setHours(0, 0, 0, 0);
|
||||
filterDate.setHours(0, 0, 0, 0);
|
||||
|
||||
return type === "after" ? checkDate >= filterDate : checkDate <= filterDate;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user