коды и панель поправлены
This commit is contained in:
@@ -55,21 +55,44 @@
|
||||
min-height: 78vh; // «присутствие» рейла даже когда контента мало
|
||||
}
|
||||
|
||||
// Рейл-колонка: раньше тянулась по высоте .main (align-items: stretch), из-за
|
||||
// чего высота рейла плавала вместе с содержимым соседней колонки — например, у
|
||||
// вкладки «Инициированные мной» появляется доп. фильтр по статусу, .main
|
||||
// становится чуть выше, и градиентный фон Sidebar (посчитанный от своей же
|
||||
// высоты) заметно менял оттенок. Прибиваем рейл к высоте окна (sticky + 100vh):
|
||||
// теперь он не зависит от того, что показано рядом. На мобильном брейкпоинте
|
||||
// сам Sidebar уходит в position:fixed (см. @tnsu/ui-kit) — там эти правила уже
|
||||
// не действуют.
|
||||
// Рейл-колонка.
|
||||
//
|
||||
// Здесь сходятся два требования, которые по отдельности решаются по-разному:
|
||||
//
|
||||
// 1. Градиент рейла не должен менять оттенок от высоты соседней колонки. Раньше
|
||||
// колонка тянулась по .main (align-items: stretch), и, например, доп. фильтр
|
||||
// по статусу на вкладке «Инициированные мной» делал .main чуть выше — фон
|
||||
// Sidebar (градиент от своей же высоты, см. $grad-header в @tnsu/ui-kit)
|
||||
// заметно менял тон. Поэтому САМ РЕЙЛ прибит к высоте окна (100vh) и от
|
||||
// содержимого рядом больше не зависит.
|
||||
// 2. Под рейлом не должно быть белого провала на длинных таблицах. Прежняя
|
||||
// версия задавала height:100vh самой КОЛОНКЕ — и всё, что ниже первого
|
||||
// экрана, оставалось непрокрашенным: на длинном списке заявок рейл выглядел
|
||||
// обрезанным. Ставку на sticky тут держать нельзя: в SharePoint прокручивается
|
||||
// не окно, а внутренний контейнер страницы, и sticky там нередко не
|
||||
// включается вовсе — тогда рейл просто уезжает вверх вместе с контентом.
|
||||
//
|
||||
// Отсюда разделение: КОЛОНКА тянется во всю высоту раскладки и закрашена
|
||||
// $navy-800 — это цвет, которым градиент рейла заканчивается (на 85% и до
|
||||
// конца), поэтому стык с липкой частью не виден. РЕЙЛ внутри — sticky на 100vh
|
||||
// поверх этой заливки. Не включился sticky — под рейлом всё равно та же
|
||||
// заливка, а не белизна.
|
||||
//
|
||||
// На мобильном брейкпоинте сам Sidebar уходит в position:fixed (см.
|
||||
// @tnsu/ui-kit) — там эти правила уже не действуют, см. @media ниже.
|
||||
.side {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
align-self: flex-start;
|
||||
height: 100vh;
|
||||
align-self: stretch;
|
||||
background: $navy-800;
|
||||
|
||||
// Прямой потомок — корень Sidebar из @tnsu/ui-kit (своего класса снаружи не
|
||||
// видно, поэтому селектор по структуре, а не по имени).
|
||||
> * {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
@@ -109,6 +132,20 @@
|
||||
.mobileNav {
|
||||
display: block;
|
||||
}
|
||||
|
||||
// Снимаем десктопные правила рейла явно. Здесь ui-kit сам делает Sidebar
|
||||
// position:fixed (top/bottom/left), и наш sticky+100vh спорил бы с ним при
|
||||
// равной специфичности — исход решался бы порядком подключения стилей.
|
||||
// Заливка колонки тоже не нужна: колонка схлопнута, drawer красит себя сам.
|
||||
.side {
|
||||
align-self: flex-start;
|
||||
background: none;
|
||||
|
||||
> * {
|
||||
position: static;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Полоса отладочной подмены личности (см. shared/impersonation.ts). Красная и
|
||||
|
||||
@@ -209,9 +209,13 @@ const Table: React.FC<{
|
||||
// комментарий — по columnsFor (зависит от раздела и фильтра). Кастомные ячейки
|
||||
// (статус → StatusBadge, дата → формат) задаются через render.
|
||||
const columns: IDataTableColumn<IMyRequest>[] = [
|
||||
{ key: 'id', title: 'ID', width: '70px' },
|
||||
// Код заявки, а не Id элемента: Id уникален только внутри своей партиции
|
||||
// (в каждом списке SharePoint нумерация с единицы), поэтому после архивации
|
||||
// в таблице встречались разные заявки под одним номером. Кода нет у заявок,
|
||||
// созданных до появления колонки — для них показываем Id, как раньше.
|
||||
{ key: 'code', title: 'Код', width: '130px', render: (r) => r.code || `#${r.id}` },
|
||||
{ key: 'type', title: 'Тип', width: '200px', render: (r) => r.requestTypeName },
|
||||
{ key: 'title', title: 'Название', render: (r) => r.title || `#${r.id}` },
|
||||
{ key: 'title', title: 'Название', render: (r) => r.title || r.code || `#${r.id}` },
|
||||
{ key: 'date', title: 'Дата', width: '110px', render: (r) => formatDate(r.created) }
|
||||
];
|
||||
if (cols.author) {
|
||||
|
||||
@@ -126,7 +126,9 @@
|
||||
}
|
||||
|
||||
.colId {
|
||||
width: 70px;
|
||||
// Под код заявки («13-2608-42»), а не под Id элемента: прежних 70px на него
|
||||
// не хватало (см. IMyRequest.code).
|
||||
width: 130px;
|
||||
color: $gray-400;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ const Table: React.FC<{ rows: IMyRequest[]; onOpen: (request: IMyRequest) => voi
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={styles.colId}>ID</th>
|
||||
<th className={styles.colId}>Код</th>
|
||||
<th className={styles.colType}>Тип</th>
|
||||
<th className={styles.colTitle}>Название</th>
|
||||
<th className={styles.colDate}>Дата</th>
|
||||
@@ -92,9 +92,10 @@ const Table: React.FC<{ rows: IMyRequest[]; onOpen: (request: IMyRequest) => voi
|
||||
}
|
||||
}}
|
||||
>
|
||||
<td className={styles.colId}>{r.id}</td>
|
||||
{/* Код заявки, а не Id элемента — см. IMyRequest.code и колонки в RequestsTableView. */}
|
||||
<td className={styles.colId}>{r.code || `#${r.id}`}</td>
|
||||
<td className={styles.colType}>{r.requestTypeName}</td>
|
||||
<td className={styles.colTitle}>{r.title || `#${r.id}`}</td>
|
||||
<td className={styles.colTitle}>{r.title || r.code || `#${r.id}`}</td>
|
||||
<td className={styles.colDate}>{formatDate(r.created)}</td>
|
||||
<td className={styles.colAuthor}>
|
||||
{[r.authorName, r.authorEmail].filter(Boolean).join(' · ') || '—'}
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
// Собирается из списка данных SharePoint конкретного типа заявки (см. myRequests.service.ts).
|
||||
export interface IMyRequest {
|
||||
id: number;
|
||||
/**
|
||||
* Уникальный код заявки (колонка RequestUniqueCode) — вида «13-2608-42», см.
|
||||
* shared/requestCode.ts. Именно он показывается пользователю вместо Id
|
||||
* элемента: Id уникален только ВНУТРИ своей партиции (в каждом списке
|
||||
* SharePoint нумерация с единицы), поэтому после архивации в одной таблице
|
||||
* оказывались разные заявки с одинаковым номером.
|
||||
*
|
||||
* Пустая строка — заявка создана до появления колонки либо код не успел
|
||||
* записаться (он проставляется вторым запросом, уже после создания элемента,
|
||||
* см. saveRequestItem): тогда в таблице показывается Id, как раньше.
|
||||
*/
|
||||
code: string;
|
||||
title: string;
|
||||
status: string;
|
||||
authorEmail: string;
|
||||
|
||||
@@ -617,7 +617,7 @@ async function finalizeApprovalTarget(
|
||||
const ticketRows = (await traced(`ticketRowsRest:${type.name}#${target.listId}`, () =>
|
||||
list.items
|
||||
.filter(ticketFilter)
|
||||
.select('Id', 'Title', 'RequestStatus', 'Created')
|
||||
.select('Id', 'RequestUniqueCode', 'Title', 'RequestStatus', 'Created')
|
||||
.top(ticketIds.length)()
|
||||
)) as Array<Record<string, unknown>>;
|
||||
|
||||
@@ -995,7 +995,7 @@ async function loadByIds(
|
||||
const rows = (await traced(`loadByIds:${kind}:${type.name}#${target.listId}`, () =>
|
||||
list.items
|
||||
.filter(idsChunk.map((id) => `Id eq ${id}`).join(' or '))
|
||||
.select('Id', 'Title', 'RequestStatus', 'Created', 'Author/EMail', 'Author/Title')
|
||||
.select('Id', 'RequestUniqueCode', 'Title', 'RequestStatus', 'Created', 'Author/EMail', 'Author/Title')
|
||||
.expand('Author')
|
||||
.top(idsChunk.length)()
|
||||
)) as Array<Record<string, unknown>>;
|
||||
@@ -1036,7 +1036,7 @@ async function getOwnRowsForType(context: WebPartContext, type: IRequestType): P
|
||||
const rows = (await sp.web
|
||||
.getList(target.listId)
|
||||
.items.filter(`AuthorId eq ${me.Id}`)
|
||||
.select('Id', 'Title', 'RequestStatus', 'Created', 'Author/EMail', 'Author/Title')
|
||||
.select('Id', 'RequestUniqueCode', 'Title', 'RequestStatus', 'Created', 'Author/EMail', 'Author/Title')
|
||||
.expand('Author')
|
||||
.orderBy('Created', false)
|
||||
.top(OWN_REST_TOP)()) as Array<Record<string, unknown>>;
|
||||
@@ -1089,7 +1089,7 @@ async function getRecentlyChangedOwnRowsForType(context: WebPartContext, type: I
|
||||
const rows = (await sp.web
|
||||
.getList(target.listId)
|
||||
.items.filter(`AuthorId eq ${me.Id} and Modified ge '${cutoffIso}'`)
|
||||
.select('Id', 'Title', 'RequestStatus', 'Created', 'Author/EMail', 'Author/Title')
|
||||
.select('Id', 'RequestUniqueCode', 'Title', 'RequestStatus', 'Created', 'Author/EMail', 'Author/Title')
|
||||
.expand('Author')
|
||||
.top(OWN_REST_TOP)()) as Array<Record<string, unknown>>;
|
||||
|
||||
@@ -1509,6 +1509,7 @@ function toMyRequest(
|
||||
): IMyRequest {
|
||||
return {
|
||||
id: r.Id as number,
|
||||
code: (r.RequestUniqueCode as string) || '',
|
||||
title: (r.Title as string) || '',
|
||||
status: (r.RequestStatus as string) || '',
|
||||
authorEmail: author?.EMail || '',
|
||||
|
||||
@@ -4,6 +4,7 @@ import { IPage, pageKey, pageSignature, readPage, writePage } from './pageCache'
|
||||
function request(over: Partial<IMyRequest> = {}): IMyRequest {
|
||||
return {
|
||||
id: 1,
|
||||
code: '1-2608-1',
|
||||
title: 'Заявка №1',
|
||||
status: 'Черновик',
|
||||
authorEmail: 'ivanov@tnsu.kz',
|
||||
@@ -113,6 +114,15 @@ describe('pageSignature', () => {
|
||||
expect(pageSignature(before)).not.toBe(pageSignature(after));
|
||||
});
|
||||
|
||||
it('появление кода заявки меняет подпись — он показан колонкой «Код»', () => {
|
||||
// Код проставляется вторым запросом, уже после создания элемента (см.
|
||||
// saveRequestItem), поэтому первая выдача приходит без него. Не учитывать
|
||||
// его в подписи — значит оставить в таблице «#1» до перезагрузки страницы.
|
||||
const before = page({ rows: [request({ code: '' })] });
|
||||
const after = page({ rows: [request({ code: '1-2608-1' })] });
|
||||
expect(pageSignature(before)).not.toBe(pageSignature(after));
|
||||
});
|
||||
|
||||
it('пустая страница имеет стабильную подпись', () => {
|
||||
expect(pageSignature({ rows: [], totalRows: 0 })).toBe(pageSignature({ rows: [], totalRows: 0 }));
|
||||
});
|
||||
|
||||
@@ -36,10 +36,15 @@ export function writePage(key: string, page: IPage): void {
|
||||
// которые видны в таблице: если они совпали, перерисовывать нечего — иначе при
|
||||
// каждом возврате на вкладку и при каждом фоновом обновлении таблица моргала бы,
|
||||
// перерисовываясь тем же самым содержимым.
|
||||
// code — тоже видимое поле (колонка «Код»), и его обязательно учитывать: он
|
||||
// проставляется ВТОРЫМ запросом, уже после создания элемента (см.
|
||||
// saveRequestItem), поэтому первая выдача может прийти без него. Без code в
|
||||
// слепке подъехавший позже код совпал бы с прежней подписью, перерисовки не
|
||||
// случилось бы, и в таблице до перезагрузки страницы висело бы «#5».
|
||||
export function pageSignature(page: IPage): string {
|
||||
return `${page.totalRows}|${page.rows
|
||||
.map((r) =>
|
||||
[r.requestTypeId, r.id, r.title, r.status, r.created, r.authorName, r.decisionComment ?? ''].join('~')
|
||||
[r.requestTypeId, r.id, r.code, r.title, r.status, r.created, r.authorName, r.decisionComment ?? ''].join('~')
|
||||
)
|
||||
.join(';')}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user