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