/
vshmidt
/
label-studio
Обзор
Документация
Войти
/
vshmidt
/
label-studio
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
web/apps/labelstudio/src/pages/CreateProject/Config/useConfigResizer.js
226 строк
9 KB
Ricardo Cabral
fix: FIT-1427: Panels twitch when resizing in Labeling Interface on small screens (#9496)
28 фев 2026, 00:57
Не верифицирован
28 фев 2026, 00:57
70b0a6c
Код
Авторство
О чём код?
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; // Gap between columns (1rem / --spacing-base = 16px) const COLUMN_GAP = 16; // Minimum widths in pixels // These values ensure editor and preview remain usable at minimum sizes const MIN_EDITOR_WIDTH = 400; // Minimum width to display code editor comfortably const MIN_PREVIEW_WIDTH = 500; // Minimum width to display labeling interface preview effectively // Left side menu width (sidebar navigation) const LEFT_MENU_WIDTH = 240; // Default editor width (left side) // Comfortable default width for the editor column const DEFAULT_EDITOR_WIDTH = 500; export const useConfigResizer = ({ projectId, containerWidth }) => { // Generate storage key based on project ID // This allows separate storage for different projects const storageKey = useMemo( () => (projectId ? `config-editor-width:${projectId}` : "config-editor-width"), [projectId], ); // Initialize state from localStorage or use default // This runs once on mount and handles initial localStorage read const [editorWidthPixels, setEditorWidthPixelsInternal] = useState(() => { // Calculate storage key inline (can't use useMemo value in initializer) const initialStorageKey = projectId ? `config-editor-width:${projectId}` : "config-editor-width"; try { const item = window.localStorage.getItem(initialStorageKey); if (item) { const storedWidth = JSON.parse(item); // Check if stored width + left menu width exceeds screen width const leftSideExceedsScreen = storedWidth + LEFT_MENU_WIDTH > window.innerWidth; // Check if right column would be too small (only if containerWidth is available) const rightColumnTooSmall = containerWidth !== undefined && containerWidth - storedWidth - COLUMN_GAP < MIN_PREVIEW_WIDTH; // Reset to default if either condition is true if (leftSideExceedsScreen || rightColumnTooSmall) { return DEFAULT_EDITOR_WIDTH; } return storedWidth; } } catch { // If error reading from localStorage, use default } return DEFAULT_EDITOR_WIDTH; }); // Track previous storage key to prevent writes during key switches // This is necessary because when the storage key changes (e.g., switching projects), // we need to load the new value from localStorage without writing the old value to the new key const prevStorageKeyRef = useRef(storageKey); // Read from localStorage when storage key changes (project switch) // Separate effect for reading ensures we don't write stale values when switching contexts useEffect(() => { const currentKey = storageKey; const prevKey = prevStorageKeyRef.current; // Only read if storage key changed (project switch) if (prevKey !== currentKey) { try { const item = window.localStorage.getItem(currentKey); if (item) { const parsedValue = JSON.parse(item); // Clamp to valid range instead of resetting to default so user preference (e.g. smaller editor) is preserved const minEditor = MIN_EDITOR_WIDTH; const maxEditor = containerWidth !== undefined ? Math.max(minEditor, containerWidth - MIN_PREVIEW_WIDTH - COLUMN_GAP) : DEFAULT_EDITOR_WIDTH * 2; const maxAllowed = Math.min(maxEditor, window.innerWidth - LEFT_MENU_WIDTH); const clamped = Math.max(minEditor, Math.min(maxAllowed, parsedValue)); setEditorWidthPixelsInternal(clamped); } else { setEditorWidthPixelsInternal(DEFAULT_EDITOR_WIDTH); } } catch { setEditorWidthPixelsInternal(DEFAULT_EDITOR_WIDTH); } prevStorageKeyRef.current = currentKey; } }, [storageKey, containerWidth]); // Write to localStorage when width changes (but not during key switches) // This effect persists user's width preference, but skips writing when switching // between projects to avoid overwriting the new project's stored value useEffect(() => { const currentKey = storageKey; const prevKey = prevStorageKeyRef.current; // Only write if storage key hasn't changed (to avoid writing during key switch) // When key changes, the read effect handles loading the new value if (prevKey === currentKey) { try { window.localStorage.setItem(currentKey, JSON.stringify(editorWidthPixels)); } catch { // Ignore write errors (e.g., quota exceeded) } } }, [storageKey, editorWidthPixels]); // Calculate min/max constraints based on container width const constraints = useMemo(() => { if (!containerWidth) { return { minEditorWidth: DEFAULT_EDITOR_WIDTH, maxEditorWidth: DEFAULT_EDITOR_WIDTH * 2, }; } // Minimum editor width const minEditorWidth = MIN_EDITOR_WIDTH; // Maximum editor width ensures preview column has minimum width const maxEditorWidth = containerWidth - MIN_PREVIEW_WIDTH - COLUMN_GAP; return { minEditorWidth, maxEditorWidth: Math.max(minEditorWidth, maxEditorWidth), }; }, [containerWidth]); // Track previous container width to only clamp when container actually resizes const prevContainerWidthRef = useRef(containerWidth); // Clamp width when container resizes // This ensures the editor width stays within valid bounds when container size changes // We only clamp on actual container resize, not on project switches, because project switches // load values from localStorage which are already validated, and clamping would override // the user's saved preference unnecessarily useEffect(() => { if (!constraints.minEditorWidth || !constraints.maxEditorWidth) { prevContainerWidthRef.current = containerWidth; return; } // Only clamp if container width actually changed (not just constraints) // This distinction is important: project switches change storage keys but shouldn't trigger clamping, // while actual window/container resizes should clamp to ensure preview remains usable const containerWidthChanged = prevContainerWidthRef.current !== undefined && prevContainerWidthRef.current !== containerWidth; if (containerWidthChanged) { // Check if current width is out of bounds and clamp if needed const clampedWidth = Math.max( constraints.minEditorWidth, Math.min(constraints.maxEditorWidth, editorWidthPixels), ); // Only update if clamping is needed if (clampedWidth !== editorWidthPixels) { setEditorWidthPixelsInternal(clampedWidth); } } prevContainerWidthRef.current = containerWidth; }, [containerWidth, constraints.minEditorWidth, constraints.maxEditorWidth, editorWidthPixels]); // Clamp width when current value is out of bounds (window resize or container change) // We clamp to the valid range instead of resetting to default so the user can still // make the editor column smaller by dragging when the screen is small. useEffect(() => { const checkAndClamp = () => { if ( constraints.minEditorWidth === undefined || constraints.maxEditorWidth === undefined || containerWidth === undefined ) { return; } const maxAllowed = Math.min(constraints.maxEditorWidth, window.innerWidth - LEFT_MENU_WIDTH); const clampedWidth = Math.max(constraints.minEditorWidth, Math.min(maxAllowed, editorWidthPixels)); if (clampedWidth !== editorWidthPixels) { setEditorWidthPixelsInternal(clampedWidth); } }; checkAndClamp(); window.addEventListener("resize", checkAndClamp); return () => { window.removeEventListener("resize", checkAndClamp); }; }, [editorWidthPixels, containerWidth, constraints.minEditorWidth, constraints.maxEditorWidth]); // Wrapped setter that automatically clamps values to valid constraints // This ensures all width updates respect min/max bounds const setEditorWidthPixels = useCallback( (value) => { setEditorWidthPixelsInternal((prev) => { const newValue = typeof value === "function" ? value(prev) : value; // Clamp to constraints if available if (constraints.minEditorWidth !== undefined && constraints.maxEditorWidth !== undefined) { return Math.max(constraints.minEditorWidth, Math.min(constraints.maxEditorWidth, newValue)); } return newValue; }); }, [constraints.minEditorWidth, constraints.maxEditorWidth], ); // Calculate preview width from editor width const previewWidthPixels = useMemo(() => { if (!containerWidth) return 0; return Math.max(MIN_PREVIEW_WIDTH, containerWidth - editorWidthPixels - COLUMN_GAP); }, [containerWidth, editorWidthPixels]); return { editorWidthPixels, setEditorWidthPixels, previewWidthPixels, constraints: { minEditorWidth: constraints.minEditorWidth, maxEditorWidth: constraints.maxEditorWidth, }, }; };