mirror of
https://github.com/stan-smith/FossFLOW.git
synced 2025-12-24 06:58:48 -05:00
fix: handle missing items gracefully in hooks and components
This resolves console errors when items are deleted or during undo/redo operations. Changes: - Added getItemById utility function that returns null instead of throwing - Updated all item hooks to return null when items don't exist - Added null checks in components that use these hooks - Components now gracefully handle missing items by returning null - Fixed TypeScript export syntax in standaloneExports.ts - Added MUI dependencies as externals in webpack config This fixes the "Item with id not found" errors that were occurring when: - Using undo/redo functionality - Items were deleted but components were still rendering - React was unmounting components that referenced deleted items
This commit is contained in:
@@ -20,6 +20,11 @@ export const ConnectorControls = ({ id }: Props) => {
|
||||
const connector = useConnector(id);
|
||||
const { updateConnector, deleteConnector } = useScene();
|
||||
|
||||
// If connector doesn't exist, return null
|
||||
if (!connector) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ControlsContainer>
|
||||
<Section>
|
||||
|
||||
@@ -35,12 +35,17 @@ export const NodeControls = ({ id }: Props) => {
|
||||
const viewItem = useViewItem(id);
|
||||
const modelItem = useModelItem(id);
|
||||
const { iconCategories } = useIconCategories();
|
||||
const { icon } = useIcon(modelItem.icon);
|
||||
const { icon } = useIcon(modelItem?.icon || '');
|
||||
|
||||
const onSwitchMode = useCallback((newMode: Mode) => {
|
||||
setMode(newMode);
|
||||
}, []);
|
||||
|
||||
// If items don't exist, return null (component will unmount)
|
||||
if (!viewItem || !modelItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ControlsContainer>
|
||||
<Box
|
||||
|
||||
@@ -26,6 +26,10 @@ export const NodeSettings = ({
|
||||
}: Props) => {
|
||||
const modelItem = useModelItem(node.id);
|
||||
|
||||
if (!modelItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section title="Name">
|
||||
|
||||
@@ -19,6 +19,11 @@ export const RectangleControls = ({ id }: Props) => {
|
||||
const rectangle = useRectangle(id);
|
||||
const { updateRectangle, deleteRectangle } = useScene();
|
||||
|
||||
// If rectangle doesn't exist, return null
|
||||
if (!rectangle) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ControlsContainer>
|
||||
<Section>
|
||||
|
||||
@@ -27,6 +27,11 @@ export const TextBoxControls = ({ id }: Props) => {
|
||||
const textBox = useTextBox(id);
|
||||
const { updateTextBox, deleteTextBox } = useScene();
|
||||
|
||||
// If textBox doesn't exist, return null
|
||||
if (!textBox) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ControlsContainer>
|
||||
<Section>
|
||||
|
||||
@@ -23,6 +23,11 @@ export const Connector = ({ connector: _connector, isSelected }: Props) => {
|
||||
const color = useColor(_connector.color);
|
||||
const { currentView } = useScene();
|
||||
const connector = useConnector(_connector.id);
|
||||
|
||||
if (!connector || !color) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { css, pxSize } = useIsoProjection({
|
||||
...connector.path.rectangle
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ interface Props {
|
||||
|
||||
export const Node = ({ node, order }: Props) => {
|
||||
const modelItem = useModelItem(node.id);
|
||||
const { iconComponent } = useIcon(modelItem.icon);
|
||||
const { iconComponent } = useIcon(modelItem?.icon);
|
||||
|
||||
const position = useMemo(() => {
|
||||
return getTilePosition({
|
||||
@@ -30,13 +30,19 @@ export const Node = ({ node, order }: Props) => {
|
||||
|
||||
const description = useMemo(() => {
|
||||
if (
|
||||
!modelItem ||
|
||||
modelItem.description === undefined ||
|
||||
modelItem.description === MARKDOWN_EMPTY_VALUE
|
||||
)
|
||||
return null;
|
||||
|
||||
return modelItem.description;
|
||||
}, [modelItem.description]);
|
||||
}, [modelItem?.description]);
|
||||
|
||||
// If modelItem doesn't exist, don't render the node
|
||||
if (!modelItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -52,7 +58,7 @@ export const Node = ({ node, order }: Props) => {
|
||||
top: position.y
|
||||
}}
|
||||
>
|
||||
{(modelItem.name || description) && (
|
||||
{(modelItem?.name || description) && (
|
||||
<Box
|
||||
sx={{ position: 'absolute' }}
|
||||
style={{ bottom: PROJECTED_TILE_SIZE.height / 2 }}
|
||||
|
||||
@@ -9,6 +9,10 @@ type Props = ReturnType<typeof useScene>['rectangles'][0];
|
||||
export const Rectangle = ({ from, to, color: colorId }: Props) => {
|
||||
const color = useColor(colorId);
|
||||
|
||||
if (!color) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<IsoTileArea
|
||||
from={from}
|
||||
|
||||
@@ -9,5 +9,9 @@ interface Props {
|
||||
export const NodeTransformControls = ({ id }: Props) => {
|
||||
const node = useViewItem(id);
|
||||
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <TransformControls from={node.tile} to={node.tile} />;
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ export const RectangleTransformControls = ({ id }: Props) => {
|
||||
|
||||
const onAnchorMouseDown = useCallback(
|
||||
(key: AnchorPosition) => {
|
||||
if (!rectangle) return;
|
||||
uiStateActions.setMode({
|
||||
type: 'RECTANGLE.TRANSFORM',
|
||||
id: rectangle.id,
|
||||
@@ -23,9 +24,13 @@ export const RectangleTransformControls = ({ id }: Props) => {
|
||||
showCursor: true
|
||||
});
|
||||
},
|
||||
[rectangle.id, uiStateActions]
|
||||
[rectangle?.id, uiStateActions]
|
||||
);
|
||||
|
||||
if (!rectangle) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TransformControls
|
||||
from={rectangle.from}
|
||||
|
||||
@@ -11,8 +11,13 @@ export const TextBoxTransformControls = ({ id }: Props) => {
|
||||
const textBox = useTextBox(id);
|
||||
|
||||
const to = useMemo(() => {
|
||||
if (!textBox) return { x: 0, y: 0 };
|
||||
return getTextBoxEndTile(textBox, textBox.size);
|
||||
}, [textBox]);
|
||||
|
||||
if (!textBox) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <TransformControls from={textBox.tile} to={to} />;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { getItemByIdOrThrow } from 'src/utils';
|
||||
import { getItemById } from 'src/utils';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
|
||||
export const useColor = (colorId?: string) => {
|
||||
@@ -7,14 +7,11 @@ export const useColor = (colorId?: string) => {
|
||||
|
||||
const color = useMemo(() => {
|
||||
if (colorId === undefined) {
|
||||
if (colors.length > 0) {
|
||||
return colors[0];
|
||||
}
|
||||
|
||||
throw new Error('No colors available.');
|
||||
return colors.length > 0 ? colors[0] : null;
|
||||
}
|
||||
|
||||
return getItemByIdOrThrow(colors, colorId).value;
|
||||
const item = getItemById(colors, colorId);
|
||||
return item ? item.value : null;
|
||||
}, [colorId, colors]);
|
||||
|
||||
return color;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useMemo } from 'react';
|
||||
import { getItemByIdOrThrow } from 'src/utils';
|
||||
import { getItemById } from 'src/utils';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
|
||||
export const useConnector = (id: string) => {
|
||||
const { connectors } = useScene();
|
||||
|
||||
const connector = useMemo(() => {
|
||||
return getItemByIdOrThrow(connectors, id).value;
|
||||
const item = getItemById(connectors, id);
|
||||
return item ? item.value : null;
|
||||
}, [connectors, id]);
|
||||
|
||||
return connector;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useMemo, useEffect } from 'react';
|
||||
import { useModelStore } from 'src/stores/modelStore';
|
||||
import { getItemByIdOrThrow } from 'src/utils';
|
||||
import { getItemById } from 'src/utils';
|
||||
import { IsometricIcon } from 'src/components/SceneLayers/Nodes/Node/IconTypes/IsometricIcon';
|
||||
import { NonIsometricIcon } from 'src/components/SceneLayers/Nodes/Node/IconTypes/NonIsometricIcon';
|
||||
import { DEFAULT_ICON } from 'src/config';
|
||||
@@ -14,7 +14,8 @@ export const useIcon = (id: string | undefined) => {
|
||||
const icon = useMemo(() => {
|
||||
if (!id) return DEFAULT_ICON;
|
||||
|
||||
return getItemByIdOrThrow(icons, id).value;
|
||||
const item = getItemById(icons, id);
|
||||
return item ? item.value : DEFAULT_ICON;
|
||||
}, [icons, id]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { useMemo } from 'react';
|
||||
import { ModelItem } from 'src/types';
|
||||
import { useModelStore } from 'src/stores/modelStore';
|
||||
import { getItemByIdOrThrow } from 'src/utils';
|
||||
import { getItemById } from 'src/utils';
|
||||
|
||||
export const useModelItem = (id: string): ModelItem => {
|
||||
export const useModelItem = (id: string): ModelItem | null => {
|
||||
const model = useModelStore((state) => {
|
||||
return state;
|
||||
});
|
||||
|
||||
const modelItem = useMemo(() => {
|
||||
return getItemByIdOrThrow(model.items, id).value;
|
||||
const item = getItemById(model.items, id);
|
||||
return item ? item.value : null;
|
||||
}, [id, model.items]);
|
||||
|
||||
return modelItem;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useMemo } from 'react';
|
||||
import { getItemByIdOrThrow } from 'src/utils';
|
||||
import { getItemById } from 'src/utils';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
|
||||
export const useRectangle = (id: string) => {
|
||||
const { rectangles } = useScene();
|
||||
|
||||
const rectangle = useMemo(() => {
|
||||
return getItemByIdOrThrow(rectangles, id).value;
|
||||
const item = getItemById(rectangles, id);
|
||||
return item ? item.value : null;
|
||||
}, [rectangles, id]);
|
||||
|
||||
return rectangle;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useMemo } from 'react';
|
||||
import { getItemByIdOrThrow } from 'src/utils';
|
||||
import { getItemById } from 'src/utils';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
|
||||
export const useTextBox = (id: string) => {
|
||||
const { textBoxes } = useScene();
|
||||
|
||||
const textBox = useMemo(() => {
|
||||
return getItemByIdOrThrow(textBoxes, id).value;
|
||||
const item = getItemById(textBoxes, id);
|
||||
return item ? item.value : null;
|
||||
}, [textBoxes, id]);
|
||||
|
||||
return textBox;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useMemo } from 'react';
|
||||
import { getItemByIdOrThrow } from 'src/utils';
|
||||
import { getItemById } from 'src/utils';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
|
||||
export const useViewItem = (id: string) => {
|
||||
const { items } = useScene();
|
||||
|
||||
const viewItem = useMemo(() => {
|
||||
return getItemByIdOrThrow(items, id).value;
|
||||
const item = getItemById(items, id);
|
||||
return item ? item.value : null;
|
||||
}, [items, id]);
|
||||
|
||||
return viewItem;
|
||||
|
||||
@@ -5,4 +5,4 @@ export * as reducers from 'src/stores/reducers';
|
||||
export { INITIAL_DATA, INITIAL_SCENE_STATE } from 'src/config';
|
||||
export * from 'src/schemas';
|
||||
export type { IsoflowProps, InitialData } from 'src/types';
|
||||
export type * from 'src/types/model';
|
||||
export * from 'src/types/model';
|
||||
|
||||
@@ -94,6 +94,21 @@ export function getItemByIdOrThrow<T extends { id: string }>(
|
||||
return { value: values[index], index };
|
||||
}
|
||||
|
||||
export function getItemById<T extends { id: string }>(
|
||||
values: T[],
|
||||
id: string
|
||||
): { value: T; index: number } | null {
|
||||
const index = values.findIndex((val) => {
|
||||
return val.id === id;
|
||||
});
|
||||
|
||||
if (index === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { value: values[index], index };
|
||||
}
|
||||
|
||||
export function getItemByIndexOrThrow<T>(items: T[], index: number): T {
|
||||
const item = items[index];
|
||||
|
||||
|
||||
@@ -26,7 +26,11 @@ module.exports = {
|
||||
commonjs2: 'react-dom',
|
||||
amd: 'ReactDOM',
|
||||
root: 'ReactDOM'
|
||||
}
|
||||
},
|
||||
'@mui/material': '@mui/material',
|
||||
'@mui/icons-material': '@mui/icons-material',
|
||||
'@emotion/react': '@emotion/react',
|
||||
'@emotion/styled': '@emotion/styled'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
|
||||
Reference in New Issue
Block a user