diff --git a/frontend/src/webparts/approvalAdmin/components/screens/ManageRequestsScreen.tsx b/frontend/src/webparts/approvalAdmin/components/screens/ManageRequestsScreen.tsx index e14cb84..6ea90d0 100644 --- a/frontend/src/webparts/approvalAdmin/components/screens/ManageRequestsScreen.tsx +++ b/frontend/src/webparts/approvalAdmin/components/screens/ManageRequestsScreen.tsx @@ -15,6 +15,7 @@ import { } from '../../models'; import { texts } from '../../texts'; import { useRequestTypes } from '../../hooks/useRequestTypes'; +import { useRequestTypeListPartitions } from '../../hooks/useRequestTypeListPartitions'; import { usePortalEmployees } from '../../hooks/usePortalEmployees'; import { useLocalStorageState } from '../../hooks/useLocalStorageState'; import { findEmployeeByEmail } from '../../utils/employee'; @@ -47,6 +48,14 @@ export interface IManageRequestsScreenProps { route?: IManageRequestRouteParams; } +// Подпись заявки в комбобоксе — уникальный код (RequestUniqueCode, тот же, что +// видит заявитель на портале), а не служебный числовой Id списка SharePoint. +// Запасной вариант на #id — для заявок без заполненного кода (старые заявки, +// либо список данных этой колонки не заводит). +function requestLabel(r: Pick): string { + return `${r.uniqueCode || `#${r.id}`} ${r.title}`; +} + // Цвет плашки статуса — те же группы смысла, что и в request-portal // (StatusBadge.tsx): решено положительно/отрицательно/ожидает/черновик. function statusVariant(status: string): BadgeVariant { @@ -158,7 +167,19 @@ export const ManageRequestsScreen: React.FC = ({ api if (!route) return; setSelection({ requestTypeId: route.requestTypeId, - selectedRequest: { id: Number(route.requestId), title: '', status: '', created: '', authorName: '' } + // listId из ссылки — какая партиция, если ссылку скопировали уже после + // этого фикса; пусто у старых ссылок — тогда reloadMatrix ниже + // подставит текущую партицию (тот же эффект, что был раньше, но теперь + // явный, а не единственно возможный). + selectedRequest: { + id: Number(route.requestId), + title: '', + status: '', + created: '', + authorName: '', + uniqueCode: null, + listId: route.listId + } }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -167,11 +188,15 @@ export const ManageRequestsScreen: React.FC = ({ api // прислать коллеге. replaceRoute, а не navigate: смена заявки — это правка // состояния текущего экрана, а не переход на новую страницу с отдельным // шагом в истории браузера. title/status заявки в URL не пишем — их выводит - // сам requestId, хранить производные данные в ссылке незачем. + // сам requestId, хранить производные данные в ссылке незачем. listId — + // пишем: без него по ссылке нельзя однозначно понять, в какой партиции + // искать requestId (см. IManageRequestRouteParams). React.useEffect(() => { replaceRoute({ tab: 'manageRequests', - manage: selectedRequest ? { requestTypeId, requestId: String(selectedRequest.id) } : undefined + manage: selectedRequest + ? { requestTypeId, requestId: String(selectedRequest.id), listId: selectedRequest.listId } + : undefined }); }, [requestTypeId, selectedRequest]); const [matrix, setMatrix] = React.useState(); @@ -192,6 +217,11 @@ export const ManageRequestsScreen: React.FC = ({ api // и читать заявки этого типа отсюда просто негде. const manageableRequestTypes = requestTypes.items.filter(rt => rt.listId && rt.siteUrl); + // Вся серия партиций выбранного типа (архив + текущая) — заявки этого типа + // физически лежат в любой из них (см. ТЗ по архивации), искать и открывать + // нужно по всем сразу, не только по текущей. + const partitions = useRequestTypeListPartitions(api, requestType?.id); + const selectRequestType = (id: string): void => { // Заявка выбрана для ДРУГОГО типа — сбрасываем, иначе комбобокс продолжит // показывать заявку не того типа, что выбран сейчас. @@ -205,10 +235,12 @@ export const ManageRequestsScreen: React.FC = ({ api const searchRequests = React.useCallback( (query: string, page: number) => { - if (!requestType) return Promise.resolve({ items: [], hasMore: false }); - return requestManage.searchRequests(requestType, query, page); + if (!requestType || !requestType.siteUrl || partitions.items.length === 0) { + return Promise.resolve({ items: [], hasMore: false }); + } + return requestManage.searchRequests(partitions.items, requestType.siteUrl, query, page); }, - [requestManage, requestType] + [requestManage, requestType, partitions.items] ); // silent=true — фоновое обновление (после своего же действия, автообновление @@ -219,18 +251,38 @@ export const ManageRequestsScreen: React.FC = ({ api const reloadMatrix = React.useCallback( (silent = false) => { if (!requestType || !selectedRequest) return; + // Партиции ещё не подтянулись (первый рендер после смены типа/захода по + // ссылке) — без них нельзя даже подставить запасной вариант (текущая + // партиция) для ссылок без listId, ждём следующего рендера. + if (partitions.isLoading) return; + if (!requestType.siteUrl) { + setMatrixError('У типа заявки не указан список SharePoint.'); + return; + } + // listId заявки уже известен (выбрана из комбобокса, либо ссылка после + // этого фикса) — используем его. Иначе (старая ссылка без listId) — + // единственный разумный запасной вариант: текущая партиция, тот же + // список, что искался бы и раньше. + const listId = selectedRequest.listId || partitions.items.filter(p => p.isCurrent)[0]?.listId; + if (!listId) { + setMatrixError('Для типа заявки не найдено ни одной партиции списка SharePoint.'); + return; + } if (!silent) setMatrixLoading(true); setMatrixError(undefined); requestManage - .getMatrix(requestType, selectedRequest.id) + .getMatrix(requestType.siteUrl, listId, selectedRequest.id) .then(m => { setMatrix(m); - // title/status в комбобоксе (а значит, и в localStorage) держим + // title/status/listId в комбобоксе (а значит, и в localStorage) держим // синхронными с сервером — особенно важно после захода по глубокой - // ссылке, где изначально они пустые (см. эффект-затравку выше). + // ссылке, где изначально они пустые (см. эффект-затравку выше), и + // после подстановки запасной (текущей) партиции выше: как только + // реальная партиция заявки известна, оседает в выборе — следующий + // reloadMatrix пойдёт уже по ней напрямую. // - // ВАЖНО: возвращаем ТОТ ЖЕ prev (без изменений), если title/status не - // отличаются — m.request это каждый раз НОВЫЙ объект (свежий парсинг + // ВАЖНО: возвращаем ТОТ ЖЕ prev (без изменений), если ничего не + // отличается — m.request это каждый раз НОВЫЙ объект (свежий парсинг // ответа), и если писать его в состояние безусловно, selectedRequest // получает новую ссылку на КАЖДОЙ загрузке, даже когда содержимое не // поменялось. Это меняет зависимости reloadMatrix → пересоздаёт саму @@ -240,7 +292,7 @@ export const ManageRequestsScreen: React.FC = ({ api setSelection(prev => { const sel = prev.selectedRequest; if (!sel || sel.id !== m.request.id) return prev; - const same = sel.title === m.request.title && sel.status === m.request.status; + const same = sel.title === m.request.title && sel.status === m.request.status && sel.listId === m.request.listId; return same ? prev : { ...prev, selectedRequest: m.request }; }); }) @@ -249,7 +301,7 @@ export const ManageRequestsScreen: React.FC = ({ api if (!silent) setMatrixLoading(false); }); }, - [requestManage, requestType, selectedRequest] + [requestManage, requestType, selectedRequest, partitions.items, partitions.isLoading] ); React.useEffect(() => { @@ -533,14 +585,14 @@ export const ManageRequestsScreen: React.FC = ({ api key={requestTypeId || 'none'} value={selectedRequest ? String(selectedRequest.id) : ''} - selectedLabel={selectedRequest ? `#${selectedRequest.id} ${selectedRequest.title}` : undefined} + selectedLabel={selectedRequest ? requestLabel(selectedRequest) : undefined} selectedSublabel={selectedRequest?.status} disabled={!requestType} placeholder={texts.manageRequests.requestPlaceholder} searchPlaceholder={texts.manageRequests.requestSearchPlaceholder} search={searchRequests} getValue={r => String(r.id)} - getLabel={r => `#${r.id} ${r.title}`} + getLabel={requestLabel} getSublabel={r => r.status} onChange={(_, item) => selectRequest(item)} /> diff --git a/frontend/src/webparts/approvalAdmin/hooks/useRequestTypeListPartitions.ts b/frontend/src/webparts/approvalAdmin/hooks/useRequestTypeListPartitions.ts new file mode 100644 index 0000000..781dd4f --- /dev/null +++ b/frontend/src/webparts/approvalAdmin/hooks/useRequestTypeListPartitions.ts @@ -0,0 +1,20 @@ +import * as React from 'react'; +import { RequestListPartition } from '../models'; +import { ApprovalRouteApi } from '../services/api'; +import { AsyncList, useAsyncList } from './useAsyncList'; + +/** + * Серия партиций (архив + текущая) ОДНОГО типа заявки — нужна экрану + * редактирования заявок: заявка может лежать в любой из них, не только в + * текущей (см. requestManageService.ts). + */ +export function useRequestTypeListPartitions( + api: ApprovalRouteApi, + requestTypeId: number | undefined +): AsyncList { + const load = React.useCallback( + () => (requestTypeId ? api.requestTypes.lists(requestTypeId) : Promise.resolve([])), + [api, requestTypeId] + ); + return useAsyncList(load); +} diff --git a/frontend/src/webparts/approvalAdmin/models/requestManage.ts b/frontend/src/webparts/approvalAdmin/models/requestManage.ts index 19f9078..b9e9207 100644 --- a/frontend/src/webparts/approvalAdmin/models/requestManage.ts +++ b/frontend/src/webparts/approvalAdmin/models/requestManage.ts @@ -78,6 +78,21 @@ export interface ManagedRequestSummary { status: string; created: string; // ISO authorName: string; + /** + * Уникальный код заявки (колонка RequestUniqueCode списка данных) — то, что + * реально видит заявитель, в отличие от служебного числового Id списка + * SharePoint. null — у заявки его нет (старая заявка, колонка не заполнена) + * или список данных этой колонки не заводит; тогда в UI показывается id. + */ + // eslint-disable-next-line @rushstack/no-new-null -- зеркалит SharePoint: колонки может не быть + uniqueCode: string | null; + /** + * Список-партиция, в котором физически лежит эта заявка. У разных партиций + * независимая нумерация Id (см. ТЗ по архивации), поэтому один и тот же + * requestId в разных партициях — это РАЗНЫЕ заявки: без listId открыть + * заявку однозначно нельзя. + */ + listId: string; } export interface ManagedRequestPage { diff --git a/frontend/src/webparts/approvalAdmin/routing.test.ts b/frontend/src/webparts/approvalAdmin/routing.test.ts index 5246dae..9eb0a52 100644 --- a/frontend/src/webparts/approvalAdmin/routing.test.ts +++ b/frontend/src/webparts/approvalAdmin/routing.test.ts @@ -19,7 +19,12 @@ describe('parseRoute', () => { it('manageRequests с aamtype+aamreq — даёт заполненный manage', () => { const route = parseRoute('?aatab=manageRequests&aamtype=5&aamreq=42'); expect(route.tab).toBe('manageRequests'); - expect(route.manage).toEqual({ requestTypeId: '5', requestId: '42' }); + expect(route.manage).toEqual({ requestTypeId: '5', requestId: '42', listId: '' }); + }); + + it('manageRequests с aamlist — партиция заявки читается в manage.listId', () => { + const route = parseRoute('?aatab=manageRequests&aamtype=5&aamreq=42&aamlist=%2Fsites%2Fx%2FLists%2FForm2'); + expect(route.manage).toEqual({ requestTypeId: '5', requestId: '42', listId: '/sites/x/Lists/Form2' }); }); it('manageRequests без aamreq — manage не собирается (id заявки обязателен)', () => { @@ -78,14 +83,14 @@ describe('parseRoute', () => { describe('navigate', () => { it('после navigate() параметры читаются обратно тем же parseRoute (round-trip)', () => { - navigate({ tab: 'manageRequests', manage: { requestTypeId: '3', requestId: '10' } }); + navigate({ tab: 'manageRequests', manage: { requestTypeId: '3', requestId: '10', listId: '/sites/x/Lists/Form' } }); const roundTripped = parseRoute(window.location.search); expect(roundTripped.tab).toBe('manageRequests'); - expect(roundTripped.manage).toEqual({ requestTypeId: '3', requestId: '10' }); + expect(roundTripped.manage).toEqual({ requestTypeId: '3', requestId: '10', listId: '/sites/x/Lists/Form' }); }); it('navigate на dashboard полностью убирает параметры вкладки из адреса', () => { - navigate({ tab: 'manageRequests', manage: { requestTypeId: '3', requestId: '10' } }); + navigate({ tab: 'manageRequests', manage: { requestTypeId: '3', requestId: '10', listId: '/sites/x/Lists/Form' } }); navigate({ tab: 'dashboard' }); expect(window.location.search).toBe(''); }); diff --git a/frontend/src/webparts/approvalAdmin/routing.ts b/frontend/src/webparts/approvalAdmin/routing.ts index 3e99d88..65b6d31 100644 --- a/frontend/src/webparts/approvalAdmin/routing.ts +++ b/frontend/src/webparts/approvalAdmin/routing.ts @@ -36,6 +36,14 @@ export interface IResolveRouteParams { export interface IManageRequestRouteParams { requestTypeId: string; requestId: string; + /** + * Партиция (список-архив данных), где физически лежит заявка — у разных + * партиций независимая нумерация Id (см. ТЗ по архивации), поэтому без + * listId requestId неоднозначен. Пусто — старая ссылка (до архивации) или + * ссылка ещё не резолвнута; тогда экран считает, что заявка в текущей + * партиции (см. ManageRequestsScreen). + */ + listId: string; } export interface IAdminRoute { @@ -64,7 +72,7 @@ const TABS: ScreenKey[] = ['dashboard', 'requestTypes', 'routes', 'resolve', 'ma const PARAM_KEYS = [ 'aatab', 'aaroute', 'aarfilter', 'aartype', 'aauser', 'aaproject', 'aaprojectname', 'aastatus', 'aafields', 'aadept', - 'aamtype', 'aamreq' + 'aamtype', 'aamreq', 'aamlist' ]; // raw — результат URLSearchParams.get() (DOM API, возвращает string | null). @@ -114,7 +122,7 @@ export function parseRoute(search: string = window.location.search): IAdminRoute const manageReqId = params.get('aamreq'); const manage: IManageRequestRouteParams | undefined = tab === 'manageRequests' && manageTypeId && manageReqId - ? { requestTypeId: manageTypeId, requestId: manageReqId } + ? { requestTypeId: manageTypeId, requestId: manageReqId, listId: params.get('aamlist') || '' } : undefined; return { @@ -152,6 +160,7 @@ function buildQuery(route: IAdminRoute): string { if (route.tab === 'manageRequests' && route.manage && route.manage.requestTypeId && route.manage.requestId) { params.set('aamtype', route.manage.requestTypeId); params.set('aamreq', route.manage.requestId); + if (route.manage.listId) params.set('aamlist', route.manage.listId); } const query = params.toString(); diff --git a/frontend/src/webparts/approvalAdmin/services/api.ts b/frontend/src/webparts/approvalAdmin/services/api.ts index adc80bc..4b8fe10 100644 --- a/frontend/src/webparts/approvalAdmin/services/api.ts +++ b/frontend/src/webparts/approvalAdmin/services/api.ts @@ -6,6 +6,7 @@ import { PatchRequestTypeInput, PatchTemplateRouteInput, PatchTemplateRouteStepInput, + RequestListPartition, RequestType, RequestTypeListSeries, ResolveTemplateRouteQuery, @@ -47,7 +48,13 @@ export class ApprovalRouteApi { // портала, которому нужен весь список адресов разом, а не по одному запросу // на тип. listsAll: (): Promise => - this.http.get('/api/request-types/lists') + this.http.get('/api/request-types/lists'), + + // Вся серия партиций ОДНОГО типа заявки (архив + текущая) — нужна экрану + // редактирования заявок: заявка может лежать в любой из них, не только в + // текущей (см. requestManageService.ts). + lists: (requestTypeId: number): Promise => + this.http.get(`/api/request-types/${requestTypeId}/lists`) }; public readonly templateRoutes = { diff --git a/frontend/src/webparts/approvalAdmin/services/requestManageService.ts b/frontend/src/webparts/approvalAdmin/services/requestManageService.ts index 47f6b42..3e94173 100644 --- a/frontend/src/webparts/approvalAdmin/services/requestManageService.ts +++ b/frontend/src/webparts/approvalAdmin/services/requestManageService.ts @@ -12,7 +12,7 @@ import { ManagedRequestMatrix, ManagedRequestPage, ManagedRequestSummary, - RequestType + RequestListPartition } from '../models'; // Список данных «<Форма> — данные» → список согласования «<Форма> — согласование» @@ -86,15 +86,19 @@ interface IRawDataRow { RequestStatus?: string; Created?: string; Author?: { Title?: string }; + // eslint-disable-next-line @rushstack/no-new-null -- зеркалит SharePoint: колонки может не быть в старых списках/строках + RequestUniqueCode?: string | null; } -function toSummary(r: IRawDataRow): ManagedRequestSummary { +function toSummary(r: IRawDataRow, listId: string): ManagedRequestSummary { return { id: r.Id, title: r.Title || `#${r.Id}`, status: r.RequestStatus || '', created: r.Created || '', - authorName: r.Author?.Title || '' + authorName: r.Author?.Title || '', + uniqueCode: r.RequestUniqueCode || null, + listId }; } @@ -124,24 +128,41 @@ export class RequestManageService { return { dataListTitle: info.Title, approvalListTitle }; } + // К какой партиции относится хит Search — по префиксу его Path. Партиции + // определяются вызывающей стороной (ListPartitionService.ListAsync), их + // список короткий (единицы), линейный поиск тут не проблема. + private partitionForPath( + siteUrl: string, + partitions: RequestListPartition[], + path: string + ): RequestListPartition | undefined { + return partitions.filter(p => path.indexOf(listAbsUrl(siteUrl, p.listId)) === 0)[0]; + } + /** - * Заявки типа requestType постранично, для комбобокса выбора заявки. Сначала - * Search по названию (свободный терм, пустой query — просто последние без - * фильтра), затем дочитывание полей REST'ом по найденным Id — Search не - * отдаёт произвольные поля списка надёжно (та же двухшаговая схема, что в - * request-portal). + * Заявки по ВСЕМ партициям списка данных этого типа (архив + текущая) + * постранично, для комбобокса выбора заявки. Сначала Search по названию + * (свободный терм, пустой query — просто последние без фильтра), затем + * дочитывание полей REST'ом по найденным Id — Search не отдаёт произвольные + * поля списка надёжно (та же двухшаговая схема, что в request-portal). + * + * Заявки, набранные ростом одного типа, физически «переезжают» на новый + * список-партицию (см. ТЗ по архивации) — искать нужно сразу по всем, иначе + * заявки из архивных партиций в комбобоксе просто не найти. */ public async searchRequests( - requestType: RequestType, + partitions: RequestListPartition[], + siteUrl: string, query: string, page: number, pageSize = 20 ): Promise { - if (!requestType.listId || !requestType.siteUrl) return { items: [], hasMore: false }; + if (partitions.length === 0) return { items: [], hasMore: false }; - const sp = this.clientFor(requestType.siteUrl); - const absUrl = listAbsUrl(requestType.siteUrl, requestType.listId); - const clauses = [`Path:"${absUrl}*"`]; + const sp = this.clientFor(siteUrl); + const pathClauses = partitions.map(p => `Path:"${listAbsUrl(siteUrl, p.listId)}*"`); + const pathClause = pathClauses.length > 1 ? `(${pathClauses.join(' OR ')})` : pathClauses[0]; + const clauses = [pathClause]; const text = freeTextClause(query); if (text) clauses.push(text); @@ -154,65 +175,83 @@ export class RequestManageService { SortList: [{ Property: 'Created', Direction: 1 }] }); - const rawRows = (result.PrimarySearchResults ?? []) as Array<{ ListItemID?: string }>; - // Дедуп по Id обязателен: TrimDuplicates: false выше выключает штатную - // дедупликацию Search (она иногда схлопывает нужные нам заявки), и тогда - // индекс может отдать один и тот же элемент несколькими хитами - // (переиндексация, несколько «версий» в crawl). Без этого фильтра хиты - // превращались в пункты комбобокса один-в-один, и заявка показывалась в - // списке дважды — с одинаковым React-ключом. Та же причина и то же лечение, - // что в request-portal (features/myRequests/myRequests.service.ts/hitsToEntries). - const seen = new Set(); - const ids = rawRows - .map(r => Number(r.ListItemID)) - .filter(id => !isNaN(id) && id > 0) - .filter(id => { - if (seen.has(id)) return false; - seen.add(id); - return true; - }); - if (ids.length === 0) return { items: [], hasMore: false }; + const rawRows = (result.PrimarySearchResults ?? []) as Array<{ ListItemID?: string; Path?: string }>; + // Дедуп обязателен по паре (listId, Id), а НЕ по одному Id: у разных + // партиций независимая нумерация элементов (см. ТЗ по архивации), поэтому + // одинаковый номер в разных партициях — это две РАЗНЫЕ заявки. TrimDuplicates: + // false выше выключает штатную дедупликацию Search (она иногда схлопывает + // нужные нам заявки), и тогда индекс может отдать один и тот же элемент + // несколькими хитами (переиндексация, несколько «версий» в crawl) — та же + // причина и то же лечение, что в request-portal + // (features/myRequests/myRequests.service.ts/hitsToEntries). + const seen = new Set(); + const hits: Array<{ id: number; partition: RequestListPartition }> = []; + rawRows.forEach(r => { + const id = Number(r.ListItemID); + if (isNaN(id) || id <= 0 || !r.Path) return; + const partition = this.partitionForPath(siteUrl, partitions, r.Path); + if (!partition) return; + const key = `${partition.listId}|${id}`; + if (seen.has(key)) return; + seen.add(key); + hits.push({ id, partition }); + }); + if (hits.length === 0) return { items: [], hasMore: false }; - const list = sp.web.getList(requestType.listId); - const dataRows = (await list.items - .filter(ids.map(id => `Id eq ${id}`).join(' or ')) - .select('Id', 'Title', 'RequestStatus', 'Created', 'Author/Title') - .expand('Author') - .top(ids.length)()) as IRawDataRow[]; + const idsByListId = new Map(); + hits.forEach(h => idsByListId.set(h.partition.listId, [...(idsByListId.get(h.partition.listId) || []), h.id])); + + const rowByKey = new Map(); + await Promise.all( + Array.from(idsByListId.entries()).map(async ([listId, ids]) => { + const list = sp.web.getList(listId); + const dataRows = (await list.items + .filter(ids.map(id => `Id eq ${id}`).join(' or ')) + .select('Id', 'Title', 'RequestStatus', 'Created', 'Author/Title', 'RequestUniqueCode') + .expand('Author') + .top(ids.length)()) as IRawDataRow[]; + dataRows.forEach(r => rowByKey.set(`${listId}|${r.Id}`, r)); + }) + ); - const byId = new Map(dataRows.map(r => [r.Id, r])); // Порядок задаёт Search (SortList по дате), не порядок ответа REST. - const items = ids - .map(id => byId.get(id)) - .filter((r): r is IRawDataRow => !!r) - .map(toSummary); + const items = hits + .map(h => { + const row = rowByKey.get(`${h.partition.listId}|${h.id}`); + return row ? toSummary(row, h.partition.listId) : undefined; + }) + .filter((r): r is ManagedRequestSummary => !!r); const totalRows = (result as unknown as { TotalRows?: number }).TotalRows ?? items.length; return { items, hasMore: page * pageSize < totalRows }; } - /** Заявка + полный маршрут (обе фазы, по StepOrder) — для экрана редактирования. */ - public async getMatrix(requestType: RequestType, requestId: number): Promise { - if (!requestType.listId || !requestType.siteUrl) { - throw new Error('У типа заявки не указан список SharePoint.'); - } - const sp = this.clientFor(requestType.siteUrl); - const dataList = sp.web.getList(requestType.listId); + /** + * Заявка + полный маршрут (обе фазы, по StepOrder) — для экрана редактирования. + * + * listId — КОНКРЕТНАЯ партиция, где физически лежит заявка (из + * ManagedRequestSummary.listId, отданного searchRequests/предыдущей + * getMatrix) — не requestType.listId: та партиция может быть уже не той, где + * лежит эта заявка (см. ТЗ по архивации). + */ + public async getMatrix(siteUrl: string, listId: string, requestId: number): Promise { + const sp = this.clientFor(siteUrl); + const dataList = sp.web.getList(listId); const [requestRow, { dataListTitle, approvalListTitle }] = await Promise.all([ dataList.items .getById(requestId) - .select('Id', 'Title', 'RequestStatus', 'Created', 'Author/Title') + .select('Id', 'Title', 'RequestStatus', 'Created', 'Author/Title', 'RequestUniqueCode') .expand('Author')() as Promise, - this.listTitles(sp, requestType.listId) + this.listTitles(sp, listId) ]); - const dataListUrl = listAbsUrl(requestType.siteUrl, requestType.listId); + const dataListUrl = listAbsUrl(siteUrl, listId); if (!approvalListTitle) { return { - request: toSummary(requestRow), - siteUrl: requestType.siteUrl, + request: toSummary(requestRow, listId), + siteUrl, dataListTitle, dataListUrl, approvalListTitle: '', @@ -264,7 +303,7 @@ export class RequestManageService { }; }); - return { request: toSummary(requestRow), siteUrl: requestType.siteUrl, dataListTitle, dataListUrl, approvalListTitle, rows }; + return { request: toSummary(requestRow, listId), siteUrl, dataListTitle, dataListUrl, approvalListTitle, rows }; } /** email → числовой Id пользователя сайта (Person-поле требует Id, не email). */