--no-edit

This commit is contained in:
Тимур Городецкий
2026-08-07 10:59:43 +05:00
24 changed files with 960 additions and 267 deletions
@@ -28,7 +28,23 @@ export const ApprovalMatrix: React.FC<IApprovalMatrixProps> = ({
emptyText = 'Согласующие не заданы.',
pickerPlaceholder = 'Добавить согласующего: начните вводить ФИО…',
}) => {
const { rows, loading, error, editable } = editor;
const { rows, loading, error, editable, blocks } = editor;
// Строка → шаг, которому она принадлежит. Нужно, чтобы отрисовать ячейку
// «Шаг» один раз на весь шаг (rowSpan) и понять, можно ли двигать его выше/ниже.
const blockByRow: Array<{ index: number; start: number; size: number }> = [];
blocks.forEach((b, bi) => {
for (let i = b.start; i <= b.end; i += 1) {
blockByRow[i] = { index: bi, start: b.start, size: b.end - b.start + 1 };
}
});
// Пустые колонки не показываем — столбец из одних прочерков только шумит.
const hasStatuses = rows.some((r) => r.status && r.status !== '—');
const hasDates = rows.some((r) => !!r.date);
// Число колонок (для colSpan) — на случай будущих нужд не используется: комментарий
// выводим внутри ячейки согласующего, чтобы не ломать rowSpan колонки «Шаг».
return (
<div className={styles.matrixWrap}>
@@ -40,68 +56,100 @@ export const ApprovalMatrix: React.FC<IApprovalMatrixProps> = ({
)}
{!loading && !error && rows.length > 0 && (
<div className={styles.matrixScroll}>
<table className={styles.matrix}>
<thead>
<tr>
<th>{roleLabel}</th>
<th>Должность</th>
<th>Статус</th>
<th>Дата</th>
{editable && <th />}
</tr>
</thead>
<tbody>
{rows.map((r, i) => (
<React.Fragment key={i}>
<tr className={r.comment ? styles.rowNoBorder : undefined}>
<td data-label={roleLabel}>{r.who}</td>
<td data-label="Должность">{r.position}</td>
<td data-label="Статус">
{r.status && r.status !== '—' ? <StatusBadge status={r.status} /> : '—'}
</td>
<td data-label="Дата">{r.date}</td>
{editable && (
<td className={styles.matrixActions}>
<button
type="button"
className={styles.iconBtn}
disabled={i === 0}
onClick={() => editor.moveUp(i)}
title="Выше"
>
<Icon name="chevron-down" size="sm" className={styles.flipY} />
</button>
<button
type="button"
className={styles.iconBtn}
disabled={i === rows.length - 1}
onClick={() => editor.moveDown(i)}
title="Ниже"
>
<Icon name="chevron-down" size="sm" />
</button>
<button
type="button"
className={`${styles.iconBtn} ${styles.iconBtnDanger}`}
onClick={() => editor.remove(i)}
title="Удалить"
>
<Icon name="trash" size="sm" />
</button>
</td>
)}
</tr>
{r.comment && (
<tr>
<td colSpan={editable ? 5 : 4} className={styles.commentCell}>
<span className={styles.commentLabel}>Комментарий:</span> {r.comment}
<table className={styles.matrix}>
<thead>
<tr>
<th className={styles.matrixStepCol}>Шаг</th>
<th>{roleLabel}</th>
<th>Должность</th>
{hasStatuses && <th>Статус</th>}
{hasDates && <th>Дата</th>}
{editable && <th />}
{editable && <th />}
</tr>
</thead>
<tbody>
{rows.map((r, i) => {
const block = blockByRow[i];
const isBlockStart = block.start === i;
return (
<tr key={i} className={block.size > 1 ? styles.parallelRow : undefined}>
{isBlockStart && (
<td className={styles.matrixStepCol} rowSpan={block.size}>
<span className={styles.stepNo}>{r.order}</span>
</td>
)}
<td data-label={roleLabel}>
{r.who}
{/* Комментарий решения — мелким текстом под именем (не отдельной
строкой: иначе ломается rowSpan колонки «Шаг» у параллельных). */}
{r.comment && (
<span className={styles.commentInline}>
<span className={styles.commentLabel}>Комментарий:</span> {r.comment}
</span>
)}
</td>
<td data-label="Должность">{r.position}</td>
{hasStatuses && (
<td data-label="Статус">
{r.status && r.status !== '—' ? <StatusBadge status={r.status} /> : '—'}
</td>
)}
{hasDates && <td data-label="Дата">{r.date}</td>}
{/* Перемещение действует на ШАГ целиком (параллельные строки
переезжают вместе): одна пара стрелок на весь шаг. */}
{editable && isBlockStart && (
<td className={styles.matrixMoveCol} rowSpan={block.size}>
<button
type="button"
className={styles.iconBtn}
disabled={block.index === 0}
onClick={() => editor.moveBlockUp(i)}
title="Шаг выше"
>
<Icon name="chevron-down" size="sm" className={styles.flipY} />
</button>
<button
type="button"
className={styles.iconBtn}
disabled={block.index === blocks.length - 1}
onClick={() => editor.moveBlockDown(i)}
title="Шаг ниже"
>
<Icon name="chevron-down" size="sm" />
</button>
</td>
)}
{editable && (
<td className={styles.matrixActions}>
<button
type="button"
className={`${styles.iconBtn} ${r.parallelWithPrev ? styles.iconBtnActive : ''}`}
disabled={i === 0}
onClick={() => editor.toggleParallel(i)}
title={
r.parallelWithPrev
? 'Отцепить: выполнять после предыдущего шага'
: 'Объединить: выполнять одновременно с предыдущим шагом'
}
>
<Icon name="user-check" size="sm" />
</button>
<button
type="button"
className={`${styles.iconBtn} ${styles.iconBtnDanger}`}
onClick={() => editor.remove(i)}
title="Удалить"
>
<Icon name="trash" size="sm" />
</button>
</td>
)}
</tr>
)}
</React.Fragment>
))}
</tbody>
</table>
);
})}
</tbody>
</table>
</div>
)}
{editable && !loading && (
@@ -121,15 +121,11 @@
}
}
// Строка, у которой ниже идёт комментарий: убираем нижнюю границу, чтобы строка
// и её комментарий читались как единый блок (граница переезжает на .commentCell).
.rowNoBorder td {
border-bottom: 0 !important;
}
.commentCell {
padding: 0 8px 8px !important;
border-bottom: 1px solid $gray-200;
// Комментарий решения — мелким текстом под именем согласующего (в той же ячейке,
// а не отдельной строкой: иначе ломается rowSpan колонки «Шаг» у параллельных).
.commentInline {
display: block;
margin-top: 3px;
font-size: $fs-caption;
color: $gray-500;
line-height: 1.45;
@@ -141,16 +137,43 @@
font-weight: 500;
}
// ВАЖНО: не display:flex — это сняло бы с ячейки display:table-cell, и она
// перестала бы вставать в строку рядом с соседней (кнопки уезжали на вторую
// строку). Ячейка остаётся табличной, кнопки держатся в строке через nowrap.
.matrixActions {
display: flex;
gap: $sp-1;
justify-content: flex-end;
white-space: nowrap;
text-align: right;
vertical-align: middle;
// Отступ между любыми соседними элементами: кроме кнопок тут бывают распорки
// (см. .iconBtnSpacer), на которые селектор .iconBtn + .iconBtn не сработал бы.
> * + * {
margin-left: $sp-1;
}
}
// Стрелки перемещения шага. Ячейка объединена по вертикали (rowSpan) на все
// строки шага, поэтому пара кнопок стоит по центру блока, а не у его первой
// строки. Как и .matrixActions — остаётся табличной ячейкой (см. комментарий
// там), кнопки держатся в строке через nowrap.
.matrixMoveCol {
width: 1%; // ужимает колонку по содержимому, ширину забирают текстовые колонки
white-space: nowrap;
text-align: right;
vertical-align: middle;
> * + * {
margin-left: $sp-1;
}
}
.iconBtn {
display: inline-flex;
align-items: center;
justify-content: center;
// Инлайновые кнопки иначе выравниваются по baseline и «прыгают» относительно
// кнопок соседней ячейки.
vertical-align: middle;
width: 26px;
height: 26px;
padding: 0;
@@ -179,6 +202,41 @@
}
}
// Включённый режим-переключатель (шаг помечен как параллельный) — в отличие от
// остальных кнопок это состояние, а не разовое действие, поэтому оно залито.
.iconBtnActive {
border-color: $gold-500;
background: $gold-500;
color: $navy-950;
&:hover:not(:disabled) {
border-color: $gold-600;
background: $gold-600;
color: $navy-950;
}
}
// Колонка с номером шага. Объединяется по вертикали (rowSpan) на все строки
// одного шага, поэтому содержимое центрируем — иначе номер и кнопки прижимались
// бы к верхней строке блока.
.matrixStepCol {
width: 48px;
white-space: nowrap;
vertical-align: middle;
}
.stepNo {
font-weight: 600;
color: $navy-700;
}
// Строки, входящие в шаг из нескольких параллельных согласующих — лёгкая
// заливка, чтобы блок читался как единое целое (границы ячейки «Шаг» его уже
// объединяют, но без фона строки визуально распадаются).
.parallelRow > td {
background: rgba($gold-500, .06);
}
.flipY {
transform: scaleY(-1);
}
@@ -205,13 +263,9 @@
padding: 10px 0;
}
// Строка с комментарием: линия — на самом комментарии, не на строке.
.rowNoBorder {
border-bottom: 0 !important;
}
.matrix td {
display: flex;
flex-wrap: wrap; // чтобы комментарий (100%) переносился на свою строку
align-items: center;
gap: $sp-2;
border: 0 !important;
@@ -227,13 +281,14 @@
font-weight: 500;
}
.matrixActions {
// Комментарий под именем — на всю ширину ячейки (своя строка).
.commentInline {
flex-basis: 100%;
}
.matrixActions,
.matrixMoveCol {
justify-content: flex-start;
padding-top: $sp-2 !important;
}
.commentCell {
display: block;
padding: 2px 0 6px !important;
}
}