From 9cc5d52bab5fbacdf0ae20931fe8c22745664f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B8=D0=BC=D1=83=D1=80=20=D0=93=D0=BE=D1=80=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D1=86=D0=BA=D0=B8=D0=B9?= Date: Thu, 8 Oct 2026 16:42:20 +0500 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=81=D0=BE=D1=80=D1=82=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=BA=D0=B0=20=C2=AB=D0=92=D1=81=D0=B5=D1=85=20?= =?UTF-8?q?=D0=B7=D0=B0=D1=8F=D0=B2=D0=BE=D0=BA=C2=BB=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=87=D0=B5=D0=B5=20=D0=BC=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=BE=20=D0=BF=D0=BE=20=D0=B4=D0=B0=D1=82=D0=B5=20=D0=B2?= =?UTF-8?q?=D1=8B=D1=85=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit По дате выхода удобнее планировать подготовку рабочих мест. Даты для сортировки дочитываются для всего списка пачками (предел длины URL), заявки без даты — всегда в конце. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../features/myRequests/RequestsTableView.tsx | 9 ++++++++- .../features/myRequests/filtersStorage.ts | 2 +- .../myRequests/requestsTableFilters.test.ts | 10 ++++++++++ .../myRequests/requestsTableFilters.ts | 14 +++++++++++-- .../features/myRequests/startDates.service.ts | 20 ++++++++++++++++--- 5 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/webparts/requestPortal/features/myRequests/RequestsTableView.tsx b/src/webparts/requestPortal/features/myRequests/RequestsTableView.tsx index 6c3344b..4edf6ac 100644 --- a/src/webparts/requestPortal/features/myRequests/RequestsTableView.tsx +++ b/src/webparts/requestPortal/features/myRequests/RequestsTableView.tsx @@ -293,7 +293,7 @@ const Table: React.FC<{ if (cols.startDate) { columns.push({ key: 'startDate', - title: 'Дата выхода', + title: sortable('Дата выхода', 'startDate'), width: '120px', render: (r) => formatDate(r.startDate ?? '') || '—' }); @@ -592,6 +592,13 @@ export const RequestsTableView: React.FC = ({ const full = reuseFull && cachedFull && cachedFull.key === fullKey ? cachedFull.rows : await fetchAndRememberFull(); + // Дату выхода обычно дочитываем только для видимой страницы, но сортировать + // по ней надо весь список — дочитываем для всех строк, у кого её ещё нет + // (полный список переиспользуется, повторно по сети не идём). + if (sort.key === 'startDate') { + await hydrateStartDates(context, full.filter((r) => r.startDate === undefined)); + } + const refined = sortRows(filterRows(full, localFilters), sort); const pageRows = refined.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); await hydratePendingAuthors(context, types, pageRows); diff --git a/src/webparts/requestPortal/features/myRequests/filtersStorage.ts b/src/webparts/requestPortal/features/myRequests/filtersStorage.ts index a70385d..40e36ad 100644 --- a/src/webparts/requestPortal/features/myRequests/filtersStorage.ts +++ b/src/webparts/requestPortal/features/myRequests/filtersStorage.ts @@ -21,7 +21,7 @@ export interface IStoredRefinement { /** Версия формата: поменяется структура — старые записи просто не прочитаются. */ const STORAGE_PREFIX = 'rp-table-filters:v1'; -const SORT_KEYS: SortKey[] = ['code', 'type', 'date', 'status']; +const SORT_KEYS: SortKey[] = ['code', 'type', 'date', 'status', 'startDate']; const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/; function storageKey(scope: string, tableKey: string): string { diff --git a/src/webparts/requestPortal/features/myRequests/requestsTableFilters.test.ts b/src/webparts/requestPortal/features/myRequests/requestsTableFilters.test.ts index 2540891..8083d47 100644 --- a/src/webparts/requestPortal/features/myRequests/requestsTableFilters.test.ts +++ b/src/webparts/requestPortal/features/myRequests/requestsTableFilters.test.ts @@ -118,6 +118,16 @@ describe('sortRows', () => { expect(codes(sortRows(rows, { key: 'type', direction: 'asc' }))).toEqual(['b', 'a']); }); + it('по дате выхода — в обе стороны, заявки без даты всегда в конце', () => { + const rows = [ + request({ code: 'none', startDate: '' }), + request({ code: 'oct', startDate: '2026-10-06T00:00:00Z' }), + request({ code: 'sep', startDate: '2026-09-27T00:00:00Z' }), + ]; + expect(codes(sortRows(rows, { key: 'startDate', direction: 'asc' }))).toEqual(['sep', 'oct', 'none']); + expect(codes(sortRows(rows, { key: 'startDate', direction: 'desc' }))).toEqual(['oct', 'sep', 'none']); + }); + it('не мутирует исходный массив', () => { const rows = [request({ code: '4-2609-2' }), request({ code: '4-2609-1' })]; sortRows(rows, { key: 'code', direction: 'asc' }); diff --git a/src/webparts/requestPortal/features/myRequests/requestsTableFilters.ts b/src/webparts/requestPortal/features/myRequests/requestsTableFilters.ts index 1447dd3..505a8ef 100644 --- a/src/webparts/requestPortal/features/myRequests/requestsTableFilters.ts +++ b/src/webparts/requestPortal/features/myRequests/requestsTableFilters.ts @@ -5,7 +5,7 @@ import { IMyRequest } from './models/IMyRequest'; // одной странице (иначе отсортировались бы 20 строк, а не все заявки). /** Колонки, по которым можно сортировать. */ -export type SortKey = 'code' | 'type' | 'date' | 'status'; +export type SortKey = 'code' | 'type' | 'date' | 'status' | 'startDate'; export type SortDirection = 'asc' | 'desc'; export interface ISortState { @@ -108,13 +108,23 @@ function compareBy(key: SortKey, a: IMyRequest, b: IMyRequest): number { return a.status.localeCompare(b.status, 'ru'); case 'date': return a.created.localeCompare(b.created); + case 'startDate': + return (a.startDate ?? '').localeCompare(b.startDate ?? ''); } } -/** Новый массив; при равенстве по выбранной колонке — свежие сверху. */ +/** + * Новый массив; при равенстве по выбранной колонке — свежие сверху. + * Заявки без даты выхода — всегда в конце, в любом направлении: иначе при + * сортировке «по возрастанию» сверху оказались бы незаполненные. + */ export function sortRows(rows: IMyRequest[], sort: ISortState): IMyRequest[] { const sign = sort.direction === 'asc' ? 1 : -1; return [...rows].sort((a, b) => { + if (sort.key === 'startDate') { + const emptyDiff = Number(!a.startDate) - Number(!b.startDate); + if (emptyDiff !== 0) return emptyDiff; + } const primary = compareBy(sort.key, a, b) * sign; return primary !== 0 ? primary : b.created.localeCompare(a.created); }); diff --git a/src/webparts/requestPortal/features/myRequests/startDates.service.ts b/src/webparts/requestPortal/features/myRequests/startDates.service.ts index dfb6749..0feb8b8 100644 --- a/src/webparts/requestPortal/features/myRequests/startDates.service.ts +++ b/src/webparts/requestPortal/features/myRequests/startDates.service.ts @@ -6,10 +6,17 @@ import { spForSite } from '../../shared/pnp/pnpjs'; import { traced } from '../../shared/perfTrace'; import { IMyRequest } from './models/IMyRequest'; +// Сколько Id в одном $filter: при сортировке по дате выхода дочитывается весь +// список типа (сотни заявок), а «Id eq … or …» на сотни Id упирается в предел +// длины URL. +const IDS_PER_REQUEST = 50; + /** * Проставляет startDate — дату выхода сотрудника (колонка StartDate списка - * данных). Только для видимой страницы; группировка по партиции строки, как в - * hydrateAuthors: в другой партиции тот же Id — чужая заявка. Мутирует строки. + * данных). Обычно для видимой страницы, при сортировке по дате выхода — для + * всего списка (см. fetchRefinedPage в RequestsTableView). Группировка по + * партиции строки, как в hydrateAuthors: в другой партиции тот же Id — чужая + * заявка. Мутирует строки. */ export async function hydrateStartDates(context: WebPartContext, rows: IMyRequest[]): Promise { if (rows.length === 0) return; @@ -29,9 +36,16 @@ async function hydrateStartDatesInner(context: WebPartContext, rows: IMyRequest[ byPartition.set(r.listId.toLowerCase(), bucket); }); + const requests: Array<{ siteUrl: string; listId: string; typeName: string; ids: number[] }> = []; + byPartition.forEach((bucket) => { + for (let i = 0; i < bucket.ids.length; i += IDS_PER_REQUEST) { + requests.push({ ...bucket, ids: bucket.ids.slice(i, i + IDS_PER_REQUEST) }); + } + }); + const byKey = new Map(); await Promise.all( - Array.from(byPartition.values()).map(async ({ siteUrl, listId, typeName, ids }) => { + requests.map(async ({ siteUrl, listId, typeName, ids }) => { try { const sp = spForSite(siteUrl, context); const dataRows = (await sp.web