From c724197682d3d1365c362ec2224c78b5459c865e Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 18:09:11 +0400 Subject: [PATCH 1/2] =?UTF-8?q?perf(=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86?= =?UTF-8?q?=D0=B0):=20=D0=B2=D1=8B=D0=BD=D0=B5=D1=81=D1=82=D0=B8=20=D1=81?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D0=B5=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=B4=D0=B0=D0=BB=D0=BE=D0=BA=20=D0=BB=D0=B8=D1=81=D1=82=D0=B0?= =?UTF-8?q?=20=D0=B8=D0=B7=20RealtimeTable=20=D0=B2=20SheetModals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Открытие модалки больше не перерисовывает RealtimeTable: состояние живёт в memo-компоненте SheetModals, наружу открывается через ref.open(kind). Проверки выделенной строки остаются в таблице. Редьюсер вынесен в utils/sheetModalsState.js с node-тестом. Co-Authored-By: Claude Opus 5.5 --- .../RealtimeTable/Modals/SheetModals.jsx | 42 +++++++++++++++++ .../RealtimeTable/RealtimeTable.jsx | 46 ++++++------------- .../RealtimeTable/utils/sheetModalsState.js | 21 +++++++++ .../utils/sheetModalsState.test.js | 29 ++++++++++++ 4 files changed, 105 insertions(+), 33 deletions(-) create mode 100644 web/src/components/RealtimeTable/Modals/SheetModals.jsx create mode 100644 web/src/components/RealtimeTable/utils/sheetModalsState.js create mode 100644 web/src/components/RealtimeTable/utils/sheetModalsState.test.js diff --git a/web/src/components/RealtimeTable/Modals/SheetModals.jsx b/web/src/components/RealtimeTable/Modals/SheetModals.jsx new file mode 100644 index 0000000..24536c0 --- /dev/null +++ b/web/src/components/RealtimeTable/Modals/SheetModals.jsx @@ -0,0 +1,42 @@ +import { forwardRef, memo, useCallback, useImperativeHandle, useReducer } from 'react'; + +import { SHEET_MODALS_INITIAL_STATE, getCreateModalConfig, sheetModalsReducer } from '../utils/sheetModalsState'; +import { CreateProgramModal } from './CreateProgramProjectModal'; +import { SelectExpenseItemModal } from './SelectExpenseItemModal'; +import { SelectVspModal } from './SelectVspModal'; + +const SheetModalsComponent = forwardRef(function SheetModals( + { formId, sheetName, onAddVsp, onAddExpenseItem, onAddProgram, onAddProject }, + ref, +) { + const [state, dispatch] = useReducer(sheetModalsReducer, SHEET_MODALS_INITIAL_STATE); + + useImperativeHandle(ref, () => ({ open: (kind) => dispatch({ type: 'open', kind }) }), []); + + const close = useCallback(() => dispatch({ type: 'close' }), []); + + const createConfig = getCreateModalConfig(state.kind); + const createHandlers = { onAddProgram, onAddProject }; + + return ( + <> + + + + + ); +}); + +export const SheetModals = memo(SheetModalsComponent); diff --git a/web/src/components/RealtimeTable/RealtimeTable.jsx b/web/src/components/RealtimeTable/RealtimeTable.jsx index 539e4b3..7e3a0dd 100644 --- a/web/src/components/RealtimeTable/RealtimeTable.jsx +++ b/web/src/components/RealtimeTable/RealtimeTable.jsx @@ -12,9 +12,7 @@ import { getTableColumns } from './tableColumns'; import { CircularProgress } from '@mui/material'; import { debounce } from '@mui/material'; import { toast } from 'react-toastify'; -import { CreateProgramModal } from './Modals/CreateProgramProjectModal'; -import { SelectExpenseItemModal } from './Modals/SelectExpenseItemModal'; -import { SelectVspModal } from './Modals/SelectVspModal'; +import { SheetModals } from './Modals/SheetModals'; import { additionExpenseRowTable, additionVspRowTable } from './constants/addingRowConfig'; import { BASE_TABLE_CONFIG, TABLE_ROW_HEIGHT, getTableBodyCellProps, getTablePaperStyles } from './constants/tableConfig'; import { useRealtimeActions, useVspOptions } from './contexts/RealtimeContext'; @@ -103,9 +101,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { const [expanded, setExpanded] = useState(true); const [expenseItemsVisibilityAction, setExpenseItemsVisibilityAction] = useState('hide'); - const [isOpenModalSelectVsp, setIsOpenModalSelectVsp] = useState(false); - const [isOpenModalSelectExpenseItem, setIsOpenModalSelectExpenseItem] = useState(false); - const [createModalType, setCreateModalType] = useState(null); + const modalsRef = useRef(null); const isFormNoCanAddRow = useMemo(() => FormsNoCanAddRow[formType]?.includes(sheetName) ?? false, [formType, sheetName]); const canModifyRows = userRoleId === ROLES_NAME_ID.admin || columnsCurStage.length > 0; @@ -650,7 +646,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { ); const handleOpenModalSelectVsp = useCallback(() => { - setIsOpenModalSelectVsp(true); + modalsRef.current?.open('vsp'); }, []); const handleOpenModalSelectExpenseItem = useCallback(() => { @@ -663,7 +659,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { toast.error('Выделите строку проекта'); return; } - setIsOpenModalSelectExpenseItem(true); + modalsRef.current?.open('expense'); }, [getSelectedRow]); const handleDeleteRow = useCallback(() => { @@ -699,7 +695,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { toast.error('Добавление строк недоступно: нет доступных для редактирования столбцов'); return; } - setCreateModalType('program'); + modalsRef.current?.open('program'); }, [canModifyRows]); const addProject = useCallback(() => { @@ -716,13 +712,9 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { toast.error('Выделите строку программы'); return; } - setCreateModalType('project'); + modalsRef.current?.open('project'); }, [canModifyRows, getSelectedRow]); - const closeSelectVspModal = useCallback(() => setIsOpenModalSelectVsp(false), []); - const closeSelectExpenseItemModal = useCallback(() => setIsOpenModalSelectExpenseItem(false), []); - const closeCreateModal = useCallback(() => setCreateModalType(null), []); - useEffect(() => { return () => { rowVirtualizerRef.current = null; @@ -848,26 +840,14 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { - - - ); diff --git a/web/src/components/RealtimeTable/utils/sheetModalsState.js b/web/src/components/RealtimeTable/utils/sheetModalsState.js new file mode 100644 index 0000000..97623b1 --- /dev/null +++ b/web/src/components/RealtimeTable/utils/sheetModalsState.js @@ -0,0 +1,21 @@ +export const SHEET_MODAL_KINDS = ['vsp', 'expense', 'program', 'project']; + +export const SHEET_MODALS_INITIAL_STATE = { kind: null }; + +export const sheetModalsReducer = (state, action) => { + switch (action?.type) { + case 'open': + return SHEET_MODAL_KINDS.includes(action.kind) ? { kind: action.kind } : state; + case 'close': + return state.kind === null ? state : SHEET_MODALS_INITIAL_STATE; + default: + return state; + } +}; + +const CREATE_MODAL_CONFIG = { + program: { title: 'Создание программы', handlerKey: 'onAddProgram' }, + project: { title: 'Создание проекта', handlerKey: 'onAddProject' }, +}; + +export const getCreateModalConfig = (kind) => CREATE_MODAL_CONFIG[kind] ?? null; diff --git a/web/src/components/RealtimeTable/utils/sheetModalsState.test.js b/web/src/components/RealtimeTable/utils/sheetModalsState.test.js new file mode 100644 index 0000000..a4c7281 --- /dev/null +++ b/web/src/components/RealtimeTable/utils/sheetModalsState.test.js @@ -0,0 +1,29 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { SHEET_MODALS_INITIAL_STATE, getCreateModalConfig, sheetModalsReducer } from './sheetModalsState.js'; + +test('open sets the kind and close resets it', () => { + for (const kind of ['vsp', 'expense', 'program', 'project']) { + const opened = sheetModalsReducer(SHEET_MODALS_INITIAL_STATE, { type: 'open', kind }); + assert.deepEqual(opened, { kind }); + assert.deepEqual(sheetModalsReducer(opened, { type: 'close' }), SHEET_MODALS_INITIAL_STATE); + } +}); + +test('opening another kind replaces the current one', () => { + const state = sheetModalsReducer({ kind: 'vsp' }, { type: 'open', kind: 'expense' }); + assert.deepEqual(state, { kind: 'expense' }); +}); + +test('unknown kind or action keeps the same state object; closing a closed state too', () => { + assert.equal(sheetModalsReducer(SHEET_MODALS_INITIAL_STATE, { type: 'open', kind: 'nope' }), SHEET_MODALS_INITIAL_STATE); + assert.equal(sheetModalsReducer(SHEET_MODALS_INITIAL_STATE, { type: 'close' }), SHEET_MODALS_INITIAL_STATE); + assert.equal(sheetModalsReducer(SHEET_MODALS_INITIAL_STATE, { type: 'x' }), SHEET_MODALS_INITIAL_STATE); +}); + +test('create modal config differs for program and project, null for others', () => { + assert.deepEqual(getCreateModalConfig('program'), { title: 'Создание программы', handlerKey: 'onAddProgram' }); + assert.deepEqual(getCreateModalConfig('project'), { title: 'Создание проекта', handlerKey: 'onAddProject' }); + assert.equal(getCreateModalConfig('vsp'), null); + assert.equal(getCreateModalConfig(null), null); +}); -- 2.47.2 From 365becc55ac675581c147b4e46cb00d4f8cb5475 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 18:09:26 +0400 Subject: [PATCH 2/2] =?UTF-8?q?perf(=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86?= =?UTF-8?q?=D0=B0):=20=D1=81=D1=82=D0=B0=D0=B1=D0=B8=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=BA=D0=BE=D0=BB=D0=B1=D1=8D=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B8=D1=81=D0=BA=D0=B0=20=D0=B4=D0=BB=D1=8F=20Set?= =?UTF-8?q?tingsPanel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit onSearchNext/onSearchPrevious были стрелками в JSX и ломали memo панели. Co-Authored-By: Claude Opus 5.5 --- web/src/components/RealtimeTable/RealtimeTable.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/web/src/components/RealtimeTable/RealtimeTable.jsx b/web/src/components/RealtimeTable/RealtimeTable.jsx index 7e3a0dd..17a9d3b 100644 --- a/web/src/components/RealtimeTable/RealtimeTable.jsx +++ b/web/src/components/RealtimeTable/RealtimeTable.jsx @@ -550,6 +550,9 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { [handleNavigateToColumn, searchMatches], ); + const handleSearchNext = useCallback(() => navigateToSearchMatch(1), [navigateToSearchMatch]); + const handleSearchPrevious = useCallback(() => navigateToSearchMatch(-1), [navigateToSearchMatch]); + const handleSearchKeyDown = useCallback( (event) => { if (event.key !== 'Enter') return; @@ -775,8 +778,8 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { onGlobalFilterChange={handleGlobalFilterChange} globalFilter={globalFilter} onSearchKeyDown={handleSearchKeyDown} - onSearchNext={() => navigateToSearchMatch(1)} - onSearchPrevious={() => navigateToSearchMatch(-1)} + onSearchNext={handleSearchNext} + onSearchPrevious={handleSearchPrevious} searchMatchCount={searchMatches.length} searchMatchIndex={searchMatchIndex} sizeMult={sizeMult} -- 2.47.2