Merge pull request '[AURORA-1225] - логические ограничения для столбцов (требования №1 и требования №2)' (#65) from AURORA-1225 into test
Reviewed-on: #65 Reviewed-by: PotapovaA <potapovaanna113@mail.ru>
This commit is contained in:
commit
42c766cac9
@ -25,6 +25,12 @@ const LOCKED_STYLES = {
|
|||||||
opacity: 0.85,
|
opacity: 0.85,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const INVALID_STYLES = {
|
||||||
|
border: '2px solid #D32F2F',
|
||||||
|
backgroundColor: '#FDEDED',
|
||||||
|
color: '#C60C0C',
|
||||||
|
};
|
||||||
|
|
||||||
const LOCK_BADGE_STYLES = {
|
const LOCK_BADGE_STYLES = {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '2px',
|
top: '2px',
|
||||||
@ -127,6 +133,7 @@ const CellComponent = ({
|
|||||||
isEditable,
|
isEditable,
|
||||||
isUpdating,
|
isUpdating,
|
||||||
onCellNumberClick,
|
onCellNumberClick,
|
||||||
|
isInvalid,
|
||||||
}) => {
|
}) => {
|
||||||
const { lockedCells } = useRealtime();
|
const { lockedCells } = useRealtime();
|
||||||
const cellKey = `${row.id}_${column.id}`;
|
const cellKey = `${row.id}_${column.id}`;
|
||||||
@ -162,7 +169,8 @@ const CellComponent = ({
|
|||||||
backgroundColor: isLocked ? '#f5f5f5' : backgroundColor,
|
backgroundColor: isLocked ? '#f5f5f5' : backgroundColor,
|
||||||
color: textColor,
|
color: textColor,
|
||||||
...(isLocked ? LOCKED_STYLES : {}),
|
...(isLocked ? LOCKED_STYLES : {}),
|
||||||
}), [backgroundColor, isLocked, textColor]);
|
...(isInvalid ? INVALID_STYLES : {}),
|
||||||
|
}), [backgroundColor, isInvalid, isLocked, textColor]);
|
||||||
|
|
||||||
const lockBadge = useMemo(() => {
|
const lockBadge = useMemo(() => {
|
||||||
if (!isLocked) return null;
|
if (!isLocked) return null;
|
||||||
@ -196,6 +204,7 @@ const Cell = React.memo(CellComponent, (prevProps, nextProps) => {
|
|||||||
prevProps.globalFilter === nextProps.globalFilter &&
|
prevProps.globalFilter === nextProps.globalFilter &&
|
||||||
prevProps.columnFilter === nextProps.columnFilter &&
|
prevProps.columnFilter === nextProps.columnFilter &&
|
||||||
prevProps.backgroundColor === nextProps.backgroundColor &&
|
prevProps.backgroundColor === nextProps.backgroundColor &&
|
||||||
|
prevProps.isInvalid === nextProps.isInvalid &&
|
||||||
prevProps.isEditable === nextProps.isEditable &&
|
prevProps.isEditable === nextProps.isEditable &&
|
||||||
prevProps.isUpdating === nextProps.isUpdating &&
|
prevProps.isUpdating === nextProps.isUpdating &&
|
||||||
prevProps.onCellNumberClick === nextProps.onCellNumberClick
|
prevProps.onCellNumberClick === nextProps.onCellNumberClick
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import { ColumnSelectionOverlay } from './ColumnSelectionOverlay/ColumnSelection
|
|||||||
import { useColumnSettings } from './hooks/useColumnSettings';
|
import { useColumnSettings } from './hooks/useColumnSettings';
|
||||||
import { useTableScale } from './hooks/useTableScale';
|
import { useTableScale } from './hooks/useTableScale';
|
||||||
import { useHeaderPortal } from './hooks/useHeaderPortal';
|
import { useHeaderPortal } from './hooks/useHeaderPortal';
|
||||||
|
import { useValidationRules } from './hooks/useValidationRules';
|
||||||
import {
|
import {
|
||||||
BASE_TABLE_CONFIG,
|
BASE_TABLE_CONFIG,
|
||||||
getTableBodyCellProps,
|
getTableBodyCellProps,
|
||||||
@ -41,6 +42,11 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
const [isLoadingData, setIsLoadingData] = useState(false);
|
const [isLoadingData, setIsLoadingData] = useState(false);
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
const [editingCell, setEditingCell] = useState(null);
|
const [editingCell, setEditingCell] = useState(null);
|
||||||
|
const [columnsConfig, setColumnsConfig] = useState(null);
|
||||||
|
const { errors: validationErrors, isCellInvalid } = useValidationRules(
|
||||||
|
data,
|
||||||
|
columnsConfig?.columns,
|
||||||
|
);
|
||||||
|
|
||||||
const [isOpenModalSelectVsp, setIsOpenModalSelectVsp] = useState(false);
|
const [isOpenModalSelectVsp, setIsOpenModalSelectVsp] = useState(false);
|
||||||
const [isOpenModalSelectExpenseItem, setIsOpenModalSelectExpenseItem] = useState(false);
|
const [isOpenModalSelectExpenseItem, setIsOpenModalSelectExpenseItem] = useState(false);
|
||||||
@ -105,8 +111,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
rowVirtualizerRef.current?.measure?.();
|
rowVirtualizerRef.current?.measure?.();
|
||||||
}, [sizeMult]);
|
}, [sizeMult]);
|
||||||
|
|
||||||
const [columnsConfig, setColumnsConfig] = useState(null);
|
|
||||||
|
|
||||||
const configCache = useRef(new Map());
|
const configCache = useRef(new Map());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -155,6 +159,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
const handleDocumentClick = (e) => {
|
const handleDocumentClick = (e) => {
|
||||||
if (e.target.closest('[data-col-select-id]')) return;
|
if (e.target.closest('[data-col-select-id]')) return;
|
||||||
if (e.target.closest('[data-pin-panel]')) return;
|
if (e.target.closest('[data-pin-panel]')) return;
|
||||||
|
if (e.target.closest('[data-validation-errors-menu]')) return;
|
||||||
|
|
||||||
setSelectedColumnId(undefined);
|
setSelectedColumnId(undefined);
|
||||||
};
|
};
|
||||||
@ -178,12 +183,19 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
onCellUpdate: handleUpdateCell,
|
onCellUpdate: handleUpdateCell,
|
||||||
onCellNumberClick: handleClickRowCell,
|
onCellNumberClick: handleClickRowCell,
|
||||||
onCellUpdateError: handleCellUpdateError,
|
onCellUpdateError: handleCellUpdateError,
|
||||||
|
isCellInvalid,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating columns:', error);
|
console.error('Error creating columns:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}, [columnsConfig]);
|
}, [
|
||||||
|
columnsConfig,
|
||||||
|
handleCellUpdateError,
|
||||||
|
handleClickRowCell,
|
||||||
|
handleUpdateCell,
|
||||||
|
isCellInvalid,
|
||||||
|
]);
|
||||||
|
|
||||||
const selectedColumnIdRef = useRef(selectedColumnId);
|
const selectedColumnIdRef = useRef(selectedColumnId);
|
||||||
|
|
||||||
@ -334,6 +346,33 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
|
|
||||||
const table = useMaterialReactTable(tableConfig);
|
const table = useMaterialReactTable(tableConfig);
|
||||||
|
|
||||||
|
const handleNavigateToColumn = useCallback((columnId) => {
|
||||||
|
if (!columnId) return;
|
||||||
|
|
||||||
|
setSelectedColumnId(columnId);
|
||||||
|
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const pinnedIds = new Set(columnPinning?.left || []);
|
||||||
|
if (pinnedIds.has(columnId)) return;
|
||||||
|
|
||||||
|
const centerColumns = table
|
||||||
|
.getVisibleLeafColumns()
|
||||||
|
.filter((column) => !pinnedIds.has(column.id));
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
for (const column of centerColumns) {
|
||||||
|
if (column.id === columnId) break;
|
||||||
|
offset += column.getSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
container.scrollTo({
|
||||||
|
left: Math.max(0, offset - 40),
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}, [columnPinning, containerRef, table]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dataEditingCells?.line_id) {
|
if (!dataEditingCells?.line_id) {
|
||||||
setEditingCell(null);
|
setEditingCell(null);
|
||||||
@ -503,6 +542,8 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
year={year}
|
year={year}
|
||||||
isProject={formType == 'PROJECT'}
|
isProject={formType == 'PROJECT'}
|
||||||
formType={formType}
|
formType={formType}
|
||||||
|
validationErrors={validationErrors}
|
||||||
|
onNavigateToColumn={handleNavigateToColumn}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div style={tableWrapperStyle}>
|
<div style={tableWrapperStyle}>
|
||||||
|
|||||||
@ -2,12 +2,9 @@ import { useEffect, useState } from 'react';
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { Panel } from './SettingPanel.style';
|
import { Panel } from './SettingPanel.style';
|
||||||
import { Divider, IconButton, Stack, Tooltip } from '@mui/material';
|
import { Divider, IconButton, Stack, Tooltip } from '@mui/material';
|
||||||
|
|
||||||
import { GroupByObject } from './GroupByObject/GroupByObject';
|
import { GroupByObject } from './GroupByObject/GroupByObject';
|
||||||
import { ColorPickerButton } from './ColorPicker/ColorPickerButton';
|
|
||||||
import {
|
import {
|
||||||
AddLine,
|
AddLine,
|
||||||
FormulaSettingPanel,
|
|
||||||
Minus,
|
Minus,
|
||||||
Pin,
|
Pin,
|
||||||
Plus,
|
Plus,
|
||||||
@ -22,6 +19,7 @@ import SearchComponent from '../../common/SearchComponent';
|
|||||||
import { ExportDefaultButton } from '../../common/Buttons/ButtonsActions';
|
import { ExportDefaultButton } from '../../common/Buttons/ButtonsActions';
|
||||||
import { exportSheet, exportSheetProject } from '../../../utils/exportFile';
|
import { exportSheet, exportSheetProject } from '../../../utils/exportFile';
|
||||||
import { FORM_TYPE_OPTIONS } from '../../../constants/constants';
|
import { FORM_TYPE_OPTIONS } from '../../../constants/constants';
|
||||||
|
import ValidationErrorsAlert from './ValidationErrorsAlert';
|
||||||
|
|
||||||
const SettingPanel = ({
|
const SettingPanel = ({
|
||||||
selectedColumnId,
|
selectedColumnId,
|
||||||
@ -47,6 +45,8 @@ const SettingPanel = ({
|
|||||||
year,
|
year,
|
||||||
isProject,
|
isProject,
|
||||||
formType,
|
formType,
|
||||||
|
validationErrors = [],
|
||||||
|
onNavigateToColumn,
|
||||||
}) => {
|
}) => {
|
||||||
const [isPinned, setIsPinned] = useState(false);
|
const [isPinned, setIsPinned] = useState(false);
|
||||||
const [isExporting, setIsExporting] = useState(false);
|
const [isExporting, setIsExporting] = useState(false);
|
||||||
@ -198,6 +198,10 @@ const SettingPanel = ({
|
|||||||
/>
|
/>
|
||||||
</GroupByObject>
|
</GroupByObject>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<ValidationErrorsAlert
|
||||||
|
errors={validationErrors}
|
||||||
|
onNavigateToColumn={onNavigateToColumn}
|
||||||
|
/>
|
||||||
</Panel>
|
</Panel>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,128 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Button, Menu, MenuItem, Stack } from '@mui/material';
|
||||||
|
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||||
|
import WarningAmberRoundedIcon from '@mui/icons-material/WarningAmberRounded';
|
||||||
|
|
||||||
|
const warningColors = {
|
||||||
|
bg: '#fff7e6',
|
||||||
|
text: '#b54708',
|
||||||
|
border: '#FCD99C',
|
||||||
|
hover: '#fff1d6',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ValidationErrorsAlert = ({ errors = [], onNavigateToColumn }) => {
|
||||||
|
const [anchorEl, setAnchorEl] = useState(null);
|
||||||
|
const isOpen = Boolean(anchorEl);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (errors.length === 0) {
|
||||||
|
setAnchorEl(null);
|
||||||
|
}
|
||||||
|
}, [errors.length]);
|
||||||
|
|
||||||
|
if (errors.length === 0) return null;
|
||||||
|
|
||||||
|
const handleNavigate = (columnId) => {
|
||||||
|
onNavigateToColumn?.(columnId);
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={(e) => setAnchorEl(e.currentTarget)}
|
||||||
|
sx={{
|
||||||
|
ml: 2,
|
||||||
|
maxWidth: 360,
|
||||||
|
height: '2.5rem',
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
bgcolor: warningColors.bg,
|
||||||
|
color: warningColors.text,
|
||||||
|
borderColor: warningColors.border,
|
||||||
|
textTransform: 'none',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
px: 1.5,
|
||||||
|
gap: 1,
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: warningColors.hover,
|
||||||
|
borderColor: warningColors.border,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
sx={{ alignItems: 'center', gap: 1, minWidth: 0, flex: 1 }}
|
||||||
|
>
|
||||||
|
<WarningAmberRoundedIcon sx={{ fontSize: '1.1rem', flexShrink: 0 }} />
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
textAlign: 'left',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Контроль ошибок
|
||||||
|
</span>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
sx={{ alignItems: 'center', gap: 0.25, flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
<span>Подробнее</span>
|
||||||
|
<KeyboardArrowDownIcon
|
||||||
|
sx={{
|
||||||
|
fontSize: '1.25rem',
|
||||||
|
transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)',
|
||||||
|
transition: 'transform 0.2s ease',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Menu
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={isOpen}
|
||||||
|
onClose={() => setAnchorEl(null)}
|
||||||
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||||
|
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
|
||||||
|
slotProps={{
|
||||||
|
paper: {
|
||||||
|
'data-validation-errors-menu': true,
|
||||||
|
sx: {
|
||||||
|
mt: 0.5,
|
||||||
|
bgcolor: warningColors.bg,
|
||||||
|
color: warningColors.text,
|
||||||
|
border: `1px solid ${warningColors.border}`,
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
boxShadow: 'none',
|
||||||
|
minWidth: anchorEl?.offsetWidth,
|
||||||
|
maxWidth: 420,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{errors.map((error) => (
|
||||||
|
<MenuItem
|
||||||
|
key={error.id}
|
||||||
|
onClick={() => handleNavigate(error.columnId)}
|
||||||
|
sx={{
|
||||||
|
whiteSpace: 'normal',
|
||||||
|
color: warningColors.text,
|
||||||
|
cursor: 'pointer',
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: warningColors.hover,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{error.message}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ValidationErrorsAlert;
|
||||||
41
web/src/components/RealtimeTable/constants/validation.js
Normal file
41
web/src/components/RealtimeTable/constants/validation.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// Требование №1. Контроль превышения бронирования над сметой.
|
||||||
|
const remainingBookingKeys = [
|
||||||
|
"data.q1.rem_booking",
|
||||||
|
"data.q2.rem_booking",
|
||||||
|
"data.q3.rem_booking",
|
||||||
|
"data.q4.rem_booking"
|
||||||
|
];
|
||||||
|
|
||||||
|
// Требование №2. Контроль столбца «Текущие корректировки = 0».
|
||||||
|
const zeroValueRequirementKeys = [
|
||||||
|
"data.q1.adj_current",
|
||||||
|
"data.q2.adj_current",
|
||||||
|
"data.q3.adj_current",
|
||||||
|
"data.q4.adj_current"
|
||||||
|
];
|
||||||
|
|
||||||
|
export const validationRules = [
|
||||||
|
{
|
||||||
|
id: "remaining-booking",
|
||||||
|
columnKeys: remainingBookingKeys,
|
||||||
|
message: "Сумма бронирования превышает сумму, предусмотренную сметой",
|
||||||
|
isInvalid: (value) =>
|
||||||
|
value !== null &&
|
||||||
|
value !== undefined &&
|
||||||
|
value !== "" &&
|
||||||
|
Number.isFinite(Number(value)) &&
|
||||||
|
Number(value) < 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "current-adjustments-zero",
|
||||||
|
columnKeys: zeroValueRequirementKeys,
|
||||||
|
message: "Текущие корректировки должны быть равны 0",
|
||||||
|
appliesToRow: (row) => row?.depth === 0 || row?.row_type === "ROOT",
|
||||||
|
isInvalid: (value) =>
|
||||||
|
value !== null &&
|
||||||
|
value !== undefined &&
|
||||||
|
value !== "" &&
|
||||||
|
Number.isFinite(Number(value)) &&
|
||||||
|
Number(value) !== 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
37
web/src/components/RealtimeTable/hooks/useValidationRules.js
Normal file
37
web/src/components/RealtimeTable/hooks/useValidationRules.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
import { validationRules } from "../constants/validation";
|
||||||
|
import {
|
||||||
|
buildColumnHeaderPaths,
|
||||||
|
collectInvalidErrors,
|
||||||
|
} from "../utils/validationUtils";
|
||||||
|
|
||||||
|
export const useValidationRules = (data = [], columns = []) => {
|
||||||
|
const headerPaths = useMemo(
|
||||||
|
() => buildColumnHeaderPaths(columns),
|
||||||
|
[columns],
|
||||||
|
);
|
||||||
|
|
||||||
|
const errors = useMemo(() => {
|
||||||
|
const result = [];
|
||||||
|
const seen = new Set();
|
||||||
|
|
||||||
|
for (const rule of validationRules) {
|
||||||
|
collectInvalidErrors(data, rule, headerPaths, result, seen);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, [data, headerPaths]);
|
||||||
|
|
||||||
|
const isCellInvalid = useCallback(
|
||||||
|
(columnId, value, row) =>
|
||||||
|
validationRules.some(
|
||||||
|
(rule) =>
|
||||||
|
rule.columnKeys.includes(columnId) &&
|
||||||
|
(!rule.appliesToRow || rule.appliesToRow(row)) &&
|
||||||
|
rule.isInvalid(value),
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { errors, isCellInvalid };
|
||||||
|
};
|
||||||
@ -83,6 +83,7 @@ export const getTableColumns = ({
|
|||||||
columnsConfig,
|
columnsConfig,
|
||||||
onCellUpdateError,
|
onCellUpdateError,
|
||||||
onCellNumberClick,
|
onCellNumberClick,
|
||||||
|
isCellInvalid,
|
||||||
}) => {
|
}) => {
|
||||||
const columnColors = { ...columnsConfig.colors };
|
const columnColors = { ...columnsConfig.colors };
|
||||||
const columns = structuredClone(columnsConfig.columns);
|
const columns = structuredClone(columnsConfig.columns);
|
||||||
@ -92,6 +93,8 @@ export const getTableColumns = ({
|
|||||||
|
|
||||||
const getCachedCellProps = (row, column, table) => {
|
const getCachedCellProps = (row, column, table) => {
|
||||||
const key = `${row.id}_${column.id}`;
|
const key = `${row.id}_${column.id}`;
|
||||||
|
const value = row.getValue(column.id);
|
||||||
|
const isInvalid = isCellInvalid?.(column.id, value, row.original) || false;
|
||||||
|
|
||||||
if (!cellPropsCache.has(row)) {
|
if (!cellPropsCache.has(row)) {
|
||||||
cellPropsCache.set(row, {});
|
cellPropsCache.set(row, {});
|
||||||
@ -104,7 +107,9 @@ export const getTableColumns = ({
|
|||||||
const currentColumnFilter = table.getState().columnFilters?.find(f => f.id === column.id)?.value;
|
const currentColumnFilter = table.getState().columnFilters?.find(f => f.id === column.id)?.value;
|
||||||
|
|
||||||
if (cached.globalFilter === currentGlobalFilter &&
|
if (cached.globalFilter === currentGlobalFilter &&
|
||||||
cached.columnFilter === currentColumnFilter) {
|
cached.columnFilter === currentColumnFilter &&
|
||||||
|
cached.value === value &&
|
||||||
|
cached.isInvalid === isInvalid) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -118,7 +123,9 @@ export const getTableColumns = ({
|
|||||||
isEditable: row.original?.row_type === 'INPUT' || false,
|
isEditable: row.original?.row_type === 'INPUT' || false,
|
||||||
backgroundColor: columnColors[column.id]?.color_type?.[row.original?.row_type || row.row_type],
|
backgroundColor: columnColors[column.id]?.color_type?.[row.original?.row_type || row.row_type],
|
||||||
isUpdating: table.options.meta?.updatingCells?.[`${row.id}_${column.id}`],
|
isUpdating: table.options.meta?.updatingCells?.[`${row.id}_${column.id}`],
|
||||||
_hash: `${globalFilter}_${columnFilter}_${row.id}_${column.id}`,
|
isInvalid,
|
||||||
|
value,
|
||||||
|
_hash: `${globalFilter}_${columnFilter}_${row.id}_${column.id}_${value}_${isInvalid}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
rowCache[key] = props;
|
rowCache[key] = props;
|
||||||
|
|||||||
48
web/src/components/RealtimeTable/utils/validationUtils.js
Normal file
48
web/src/components/RealtimeTable/utils/validationUtils.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// Достаёт вложенное поле из объекта строки таблицы
|
||||||
|
export const getValueByPath = (row, path) =>
|
||||||
|
path.split(".").reduce((value, key) => value?.[key], row);
|
||||||
|
|
||||||
|
// Строит пути заголовков колонок
|
||||||
|
export const buildColumnHeaderPaths = (columns = [], path = [], map = {}) => {
|
||||||
|
for (const column of columns) {
|
||||||
|
const nextPath = column.header ? [...path, column.header] : path;
|
||||||
|
|
||||||
|
if (column.columns?.length) {
|
||||||
|
buildColumnHeaderPaths(column.columns, nextPath, map);
|
||||||
|
} else if (column.accessorKey) {
|
||||||
|
map[column.accessorKey] = nextPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обходит строки и собирает ошибки по правилу валидации
|
||||||
|
export const collectInvalidErrors = (rows, rule, headerPaths, errors, seen) => {
|
||||||
|
for (const row of rows) {
|
||||||
|
if (!rule.appliesToRow || rule.appliesToRow(row)) {
|
||||||
|
for (const columnKey of rule.columnKeys) {
|
||||||
|
if (!rule.isInvalid(getValueByPath(row, columnKey))) continue;
|
||||||
|
|
||||||
|
const errorId = `${rule.id}:${columnKey}`;
|
||||||
|
if (seen.has(errorId)) continue;
|
||||||
|
seen.add(errorId);
|
||||||
|
|
||||||
|
const columnPath = headerPaths[columnKey];
|
||||||
|
const columnMessage = columnPath?.length
|
||||||
|
? `Ошибка в колонке: ${columnPath.join(" => ")}`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
errors.push({
|
||||||
|
id: errorId,
|
||||||
|
columnId: columnKey,
|
||||||
|
message: columnMessage
|
||||||
|
? `${rule.message}. ${columnMessage}`
|
||||||
|
: rule.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
collectInvalidErrors(row.subRows || [], rule, headerPaths, errors, seen);
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user