ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: стабилизация read-only фильтров двусторонней доски внешних контуров

This commit is contained in:
DCCONSTRUCTIONS
2026-04-20 21:51:02 +03:00
parent c880c0a319
commit 6a3adcd245
6 changed files with 529 additions and 33 deletions
@@ -8,6 +8,7 @@ import { API_BASE_URL } from "@plane/constants";
import type {
TExternalContourBoardFilter,
TExternalContourBoardResponse,
TExternalContourBoardSorting,
TExternalContourRequest,
TExternalContourRequestResponse,
TExternalContourTargetOptions,
@@ -32,10 +33,11 @@ export class ExternalContourService extends APIService {
async listBoard(
workspaceSlug: string,
projectId: string,
filters: Partial<TExternalContourBoardFilter> = {}
filters: Partial<TExternalContourBoardFilter> = {},
sorting: TExternalContourBoardSorting = {}
): Promise<TExternalContourBoardResponse> {
const params = Object.fromEntries(
Object.entries(filters).flatMap(([key, value]) => {
Object.entries({ ...filters, ...sorting }).flatMap(([key, value]) => {
if (value === undefined || value === null || value === "") return [];
if (Array.isArray(value)) return [[key, value.join(",")]];
return [[key, String(value)]];
@@ -16,7 +16,18 @@ import { EInboxIssueCurrentTab } from "@plane/types";
import { ExternalContourService } from "@/services/external-contours";
import type { CoreRootStore } from "../root.store";
type TLoader = "init-loading" | undefined;
type TLoader = "init-loading" | "loading" | undefined;
const DEFAULT_SORTING: TExternalContourBoardSorting = { order_by: "updated_at", sort_by: "desc" };
const sanitizeBoardFilters = (filters: Partial<TExternalContourBoardFilter>): Partial<TExternalContourBoardFilter> =>
Object.fromEntries(
Object.entries(filters).flatMap(([key, value]) => {
if (value === undefined || value === null || value === "") return [];
if (Array.isArray(value) && value.length === 0) return [];
return [[key, value]];
})
) as Partial<TExternalContourBoardFilter>;
export interface IProjectExternalContoursBoardStore {
currentProjectId: string;
@@ -29,12 +40,18 @@ export interface IProjectExternalContoursBoardStore {
columnIdsMap: Record<TExternalContourBoardDirection, string[]>;
columnCountMap: Record<TExternalContourBoardDirection, number>;
tabCountMap: Record<TInboxIssueCurrentTab, number>;
activeFiltersCount: number;
fetchBoard: (workspaceSlug: string, projectId: string, tab?: TInboxIssueCurrentTab) => Promise<void>;
getColumnRequestIds: (direction: TExternalContourBoardDirection) => string[];
getColumnTotalCount: (direction: TExternalContourBoardDirection) => number;
getRequestById: (requestId: string) => TExternalContourRequest | undefined;
handleCurrentTab: (workspaceSlug: string, projectId: string, tab: TInboxIssueCurrentTab) => Promise<void>;
hasAnyItems: boolean;
isFiltering: boolean;
isSortingDefault: boolean;
clearFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
updateFilters: (workspaceSlug: string, projectId: string, filters: Partial<TExternalContourBoardFilter>) => Promise<void>;
updateSorting: (workspaceSlug: string, projectId: string, sorting: TExternalContourBoardSorting) => Promise<void>;
upsertBoardItems: (items: TExternalContourRequest[]) => void;
}
@@ -45,7 +62,7 @@ export class ProjectExternalContoursBoardStore implements IProjectExternalContou
filters: Partial<TExternalContourBoardFilter> = { status: [EInboxIssueCurrentTab.OPEN] };
items: Record<string, TExternalContourRequest> = {};
loader: TLoader = "init-loading";
sorting: TExternalContourBoardSorting = { order_by: "updated_at", sort_by: "desc" };
sorting: TExternalContourBoardSorting = DEFAULT_SORTING;
columnIdsMap: Record<TExternalContourBoardDirection, string[]> = {
outgoing: [],
incoming: [],
@@ -58,6 +75,8 @@ export class ProjectExternalContoursBoardStore implements IProjectExternalContou
[EInboxIssueCurrentTab.OPEN]: 0,
[EInboxIssueCurrentTab.CLOSED]: 0,
};
hydratedProjectId = "";
lastIssuedRequestId = 0;
externalContourService;
@@ -73,9 +92,15 @@ export class ProjectExternalContoursBoardStore implements IProjectExternalContou
columnIdsMap: observable,
columnCountMap: observable,
tabCountMap: observable,
activeFiltersCount: computed,
hasAnyItems: computed,
isFiltering: computed,
isSortingDefault: computed,
clearFilters: action,
fetchBoard: action,
handleCurrentTab: action,
updateFilters: action,
updateSorting: action,
upsertBoardItems: action,
});
@@ -86,6 +111,24 @@ export class ProjectExternalContoursBoardStore implements IProjectExternalContou
return this.columnIdsMap.outgoing.length > 0 || this.columnIdsMap.incoming.length > 0;
}
get isFiltering() {
return this.loader === "loading";
}
get isSortingDefault() {
return this.sorting.order_by === DEFAULT_SORTING.order_by && this.sorting.sort_by === DEFAULT_SORTING.sort_by;
}
get activeFiltersCount() {
return Object.entries(this.filters).reduce((count, [key, value]) => {
if (key === "status") return count;
if (value === undefined || value === null || value === "") return count;
if (Array.isArray(value)) return count + (value.length > 0 ? 1 : 0);
if (typeof value === "boolean") return count + (value ? 1 : 0);
return count + 1;
}, 0);
}
getRequestById = (requestId: string) => this.items[requestId];
getColumnRequestIds = (direction: TExternalContourBoardDirection) => this.columnIdsMap[direction] ?? [];
@@ -101,36 +144,75 @@ export class ProjectExternalContoursBoardStore implements IProjectExternalContou
};
handleCurrentTab = async (workspaceSlug: string, projectId: string, tab: TInboxIssueCurrentTab) => {
this.currentProjectId = projectId;
this.currentTab = tab;
this.filters = {
this.filters = sanitizeBoardFilters({
...this.filters,
status: [tab],
};
});
await this.fetchBoard(workspaceSlug, projectId, tab);
};
updateFilters = async (
workspaceSlug: string,
projectId: string,
filters: Partial<TExternalContourBoardFilter>
) => {
this.filters = sanitizeBoardFilters({
...this.filters,
...filters,
status: [this.currentTab],
});
await this.fetchBoard(workspaceSlug, projectId, this.currentTab);
};
updateSorting = async (workspaceSlug: string, projectId: string, sorting: TExternalContourBoardSorting) => {
this.sorting = sorting;
await this.fetchBoard(workspaceSlug, projectId, this.currentTab);
};
clearFilters = async (workspaceSlug: string, projectId: string) => {
this.filters = { status: [this.currentTab] };
this.sorting = DEFAULT_SORTING;
await this.fetchBoard(workspaceSlug, projectId, this.currentTab);
};
fetchBoard = async (workspaceSlug: string, projectId: string, tab = this.currentTab) => {
this.loader = "init-loading";
const hasProjectChanged = !!this.currentProjectId && this.currentProjectId !== projectId;
const isInitialLoad = this.hydratedProjectId !== projectId;
const nextFilters = sanitizeBoardFilters({
...(hasProjectChanged ? {} : this.filters),
status: [tab],
});
const nextSorting = hasProjectChanged ? DEFAULT_SORTING : this.sorting;
const requestId = ++this.lastIssuedRequestId;
this.loader = isInitialLoad ? "init-loading" : "loading";
this.error = undefined;
if (hasProjectChanged) {
this.items = {};
this.columnIdsMap = { outgoing: [], incoming: [] };
this.columnCountMap = { outgoing: 0, incoming: 0 };
this.tabCountMap = {
[EInboxIssueCurrentTab.OPEN]: 0,
[EInboxIssueCurrentTab.CLOSED]: 0,
};
}
this.currentProjectId = projectId;
this.currentTab = tab;
this.filters = {
...this.filters,
status: [tab],
};
this.filters = nextFilters;
this.sorting = nextSorting;
try {
const response = await this.externalContourService.listBoard(workspaceSlug, projectId, {
status: tab,
});
const response = await this.externalContourService.listBoard(workspaceSlug, projectId, nextFilters, nextSorting);
if (requestId !== this.lastIssuedRequestId) return;
runInAction(() => {
this.items = {};
this.columnIdsMap = { outgoing: [], incoming: [] };
this.columnCountMap = { outgoing: 0, incoming: 0 };
this.filters = response.filters || { status: [tab] };
this.sorting = response.sorting || { order_by: "updated_at", sort_by: "desc" };
this.filters = sanitizeBoardFilters(response.filters || nextFilters);
this.sorting = response.sorting || nextSorting;
this.hydratedProjectId = projectId;
response.columns.forEach((column) => {
this.columnIdsMap[column.key] = column.results.map((request) => request.id);
@@ -146,6 +228,8 @@ export class ProjectExternalContoursBoardStore implements IProjectExternalContou
this.loader = undefined;
});
} catch (error: any) {
if (requestId !== this.lastIssuedRequestId) return;
runInAction(() => {
this.loader = undefined;
this.error = {