Merge pull request 'AURORA-1882: явная метка «На удаление» у задач в списке и на странице задачи' (#314) from feat/313-aurora-1882-deletion-mark-chip into test
This commit is contained in:
commit
f4f9c325e2
11
web/src/components/common/DeletionMarkChip.jsx
Normal file
11
web/src/components/common/DeletionMarkChip.jsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { Tooltip } from '@mui/material';
|
||||
import { formatDeletionHint } from '../../utils/formatDeletionHint';
|
||||
import { Chip } from '../styles/StyledChip';
|
||||
|
||||
const chipSx = { background: '#fde8e8', color: '#b42318', whiteSpace: 'nowrap', width: 'max-content', cursor: 'default' };
|
||||
|
||||
export const DeletionMarkChip = ({ task, usersById, sx = {} }) => (
|
||||
<Tooltip title={formatDeletionHint(task, usersById)} arrow>
|
||||
<Chip sx={[chipSx, ...(Array.isArray(sx) ? sx : [sx])]}>На удаление</Chip>
|
||||
</Tooltip>
|
||||
);
|
||||
@ -9,6 +9,7 @@ import { TasksApi } from '../../api/tasks';
|
||||
import { FORM_TYPE_TRANSLATE } from '../../constants/constants';
|
||||
import { ExportButton } from '../common/Buttons/ButtonsActions';
|
||||
import { Chip } from '../styles/StyledChip';
|
||||
import { DeletionMarkChip } from './DeletionMarkChip';
|
||||
import { LastOpenedBadge } from './LastOpenedBadge';
|
||||
import { IconWithContent } from './IconWithContent';
|
||||
import Modal from './Modal/Modal';
|
||||
@ -62,6 +63,7 @@ export const TaskCardComponent = ({
|
||||
isAdmin = false,
|
||||
onChanged,
|
||||
currentUserId = null,
|
||||
usersById = {},
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
@ -213,7 +215,7 @@ export const TaskCardComponent = ({
|
||||
<CardHeader>
|
||||
<TaskId>ID: {id}</TaskId>
|
||||
<Stack direction='row' gap={0.75} alignItems='center'>
|
||||
{taskForm.to_delete && <Chip sx={{ background: '#fde8e8', color: '#b42318' }}>На удаление</Chip>}
|
||||
{taskForm.to_delete && <DeletionMarkChip task={taskForm} usersById={usersById} />}
|
||||
{isLastOpened && <LastOpenedBadge sx={{ mb: 0 }}>Последняя открытая</LastOpenedBadge>}
|
||||
</Stack>
|
||||
</CardHeader>
|
||||
|
||||
@ -4,9 +4,11 @@ import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import { ProjectsApi } from '../../api/projects'; // Добавьте импорт API для проектов
|
||||
import { TasksApi } from '../../api/tasks';
|
||||
import { UsersApi } from '../../api/users';
|
||||
import { SettingModal as SettingStageModal } from '../../components/Stages/SettingModal';
|
||||
import { DangerOutlinedButton, PrimaryButton, PrimaryOutlinedButton } from '../../components/common/Buttons/Buttons';
|
||||
import { ExportWithTextButton } from '../../components/common/Buttons/ButtonsActions';
|
||||
import { DeletionMarkChip } from '../../components/common/DeletionMarkChip';
|
||||
import { IconWithContent } from '../../components/common/IconWithContent';
|
||||
import { HeaderPortal } from '../../components/common/HeaderPortal';
|
||||
import SearchComponent from '../../components/common/SearchComponent';
|
||||
@ -49,6 +51,15 @@ export default function TaskPage() {
|
||||
const [addYearModalOpen, setAddYearModalOpen] = useState(false);
|
||||
const [isAddingYear, setIsAddingYear] = useState(false);
|
||||
const [dataReloadKey, setDataReloadKey] = useState(0);
|
||||
const [usersById, setUsersById] = useState({});
|
||||
const isMarkedForDeletion = Boolean(task?.to_delete);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMarkedForDeletion) return;
|
||||
UsersApi.getAll()
|
||||
.then((data) => setUsersById(Object.fromEntries((data.result || []).map((item) => [item.id, item]))))
|
||||
.catch(() => toast.error('Ошибка загрузки пользователей'));
|
||||
}, [isMarkedForDeletion]);
|
||||
|
||||
useEffect(() => {
|
||||
const getData = async () => {
|
||||
@ -162,6 +173,7 @@ export default function TaskPage() {
|
||||
<span>Таблиц: {tables.length}</span>
|
||||
</IconWithContent>
|
||||
</Chip>
|
||||
{isMarkedForDeletion && <DeletionMarkChip task={task} usersById={usersById} />}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'space-between' }}>
|
||||
|
||||
@ -5,6 +5,7 @@ import { Link as RouterLink, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import { TasksApi } from '../../api/tasks';
|
||||
import { ExportButton, ExportExitButton, ExportWithTextButton } from '../../components/common/Buttons/ButtonsActions';
|
||||
import { DeletionMarkChip } from '../../components/common/DeletionMarkChip';
|
||||
import FormBadge from '../../components/common/FormBadge';
|
||||
import { LastOpenedBadge } from '../../components/common/LastOpenedBadge';
|
||||
import Modal from '../../components/common/Modal/Modal';
|
||||
@ -84,6 +85,7 @@ export default function TasksTable({
|
||||
sx={{ color: 'inherit', fontWeight: 500, fontSize: '1rem' }}>
|
||||
{cell.getValue()}
|
||||
</Link>
|
||||
{row.original.to_delete && <DeletionMarkChip task={row.original} usersById={usersById} sx={{ mb: 0 }} />}
|
||||
{String(row.original.id) === lastOpenedId && (
|
||||
<span ref={lastOpenedRef}>
|
||||
<LastOpenedBadge sx={{ mb: 0 }}>Последняя открытая</LastOpenedBadge>
|
||||
@ -106,7 +108,7 @@ export default function TasksTable({
|
||||
<FormBadge label={creatorName} muted={row.original.to_delete} />
|
||||
{creator?.is_active === false && (
|
||||
<Typography component='span' variant='caption' sx={{ color: 'text.disabled', whiteSpace: 'nowrap' }}>
|
||||
Отправлен на удаление
|
||||
Пользователь удалён
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
22
web/src/utils/formatDeletionHint.js
Normal file
22
web/src/utils/formatDeletionHint.js
Normal file
@ -0,0 +1,22 @@
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
|
||||
const formatDateTime = (value) => {
|
||||
if (!value) return '';
|
||||
const d = new Date(value);
|
||||
if (Number.isNaN(d.getTime())) return '';
|
||||
return `${pad(d.getDate())}.${pad(d.getMonth() + 1)}.${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
};
|
||||
|
||||
/** Текст подсказки для чипа «На удаление»: когда и кем задача отправлена на удаление. */
|
||||
export const formatDeletionHint = (task, usersById) => {
|
||||
const parts = ['Отправлена на удаление'];
|
||||
const when = formatDateTime(task?.deleted_at);
|
||||
if (when) parts[0] += ` ${when}`;
|
||||
const userId = task?.deleted_by;
|
||||
if (userId !== null && userId !== undefined && userId !== '') {
|
||||
const user = usersById?.[userId];
|
||||
const name = user?.full_name?.trim() || user?.username || `Пользователь #${userId}`;
|
||||
parts.push(name);
|
||||
}
|
||||
return parts.join(' — ');
|
||||
};
|
||||
35
web/src/utils/formatDeletionHint.test.js
Normal file
35
web/src/utils/formatDeletionHint.test.js
Normal file
@ -0,0 +1,35 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { formatDeletionHint } from './formatDeletionHint.js';
|
||||
|
||||
const at = new Date(2026, 10, 5, 9, 7).toISOString();
|
||||
const users = { 7: { id: 7, full_name: ' Иванов Иван ', username: 'ivanov' } };
|
||||
|
||||
test('дата и ФИО', () => {
|
||||
assert.equal(formatDeletionHint({ deleted_at: at, deleted_by: 7 }, users), 'Отправлена на удаление 05.11.2026 09:07 — Иванов Иван');
|
||||
});
|
||||
|
||||
test('без deleted_by', () => {
|
||||
assert.equal(formatDeletionHint({ deleted_at: at, deleted_by: null }, users), 'Отправлена на удаление 05.11.2026 09:07');
|
||||
});
|
||||
|
||||
test('без deleted_at', () => {
|
||||
assert.equal(formatDeletionHint({ deleted_by: 7 }, users), 'Отправлена на удаление — Иванов Иван');
|
||||
});
|
||||
|
||||
test('без даты и пользователя', () => {
|
||||
assert.equal(formatDeletionHint({}, users), 'Отправлена на удаление');
|
||||
assert.equal(formatDeletionHint(null, null), 'Отправлена на удаление');
|
||||
});
|
||||
|
||||
test('deleted_by нет в справочнике', () => {
|
||||
assert.equal(formatDeletionHint({ deleted_at: at, deleted_by: 99 }, users), 'Отправлена на удаление 05.11.2026 09:07 — Пользователь #99');
|
||||
assert.equal(formatDeletionHint({ deleted_by: 99 }, undefined), 'Отправлена на удаление — Пользователь #99');
|
||||
});
|
||||
|
||||
test('нет full_name — username; невалидная дата игнорируется', () => {
|
||||
assert.equal(
|
||||
formatDeletionHint({ deleted_at: 'мусор', deleted_by: 8 }, { 8: { username: 'petrov' } }),
|
||||
'Отправлена на удаление — petrov',
|
||||
);
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user