Compare commits

...

1 Commits

Author SHA1 Message Date
Prateek Shourya
5546bd2305 feat: sync mobx issue store with local db. 2024-12-30 21:51:51 +05:30
20 changed files with 426 additions and 29 deletions

View File

@@ -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
View 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 };
};

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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) => {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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);
}
});
});
};

View File

@@ -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;

View File

@@ -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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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);

View File

@@ -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

View File

@@ -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;
};