Merge pull request #245 from johndoe6345789/codex/create-blockitem-and-grouping-files

refactor: extract lua block item and grouping helpers
This commit is contained in:
2025-12-27 18:42:52 +00:00
committed by GitHub
4 changed files with 273 additions and 187 deletions

View File

@@ -0,0 +1,218 @@
import type { MouseEvent } from 'react'
import {
Box,
Button,
IconButton,
MenuItem,
TextField,
Tooltip,
Typography,
} from '@mui/material'
import {
Add as AddIcon,
ArrowDownward,
ArrowUpward,
ContentCopy,
Delete as DeleteIcon,
} from '@mui/icons-material'
import type { BlockDefinition, BlockSlot, LuaBlock } from '../types'
import styles from '../LuaBlocksEditor.module.scss'
interface BlockItemProps {
block: LuaBlock
definition: BlockDefinition
index: number
total: number
onRequestAddBlock: (
event: MouseEvent<HTMLElement>,
target: { parentId: string | null; slot: BlockSlot }
) => void
onMoveBlock: (blockId: string, direction: 'up' | 'down') => void
onDuplicateBlock: (blockId: string) => void
onRemoveBlock: (blockId: string) => void
onUpdateField: (blockId: string, fieldName: string, value: string) => void
renderNestedList: (blocks?: LuaBlock[]) => JSX.Element
}
interface BlockSectionProps {
title: string
blocks: LuaBlock[] | undefined
parentId: string
slot: BlockSlot
onRequestAddBlock: (
event: MouseEvent<HTMLElement>,
target: { parentId: string | null; slot: BlockSlot }
) => void
renderNestedList: (blocks?: LuaBlock[]) => JSX.Element
}
const BlockSection = ({
title,
blocks,
parentId,
slot,
onRequestAddBlock,
renderNestedList,
}: BlockSectionProps) => (
<Box className={styles.blockSection}>
<Box className={styles.blockSectionHeader}>
<Typography className={styles.blockSectionTitle}>{title}</Typography>
<Button
size="small"
variant="contained"
onClick={(event) => onRequestAddBlock(event, { parentId, slot })}
startIcon={<AddIcon fontSize="small" />}
>
Add block
</Button>
</Box>
<Box className={styles.blockSectionBody}>
{blocks && blocks.length > 0 ? (
renderNestedList(blocks)
) : (
<Box className={styles.blockEmpty}>Drop blocks here to build this section.</Box>
)}
</Box>
</Box>
)
const BlockFields = ({
block,
definition,
onUpdateField,
}: {
block: LuaBlock
definition: BlockDefinition
onUpdateField: (blockId: string, fieldName: string, value: string) => void
}) => {
if (definition.fields.length === 0) return null
return (
<Box className={styles.blockFields}>
{definition.fields.map((field) => (
<Box key={field.name}>
<Typography className={styles.blockFieldLabel}>{field.label}</Typography>
{field.type === 'select' ? (
<TextField
select
size="small"
value={block.fields[field.name]}
onChange={(event) => onUpdateField(block.id, field.name, event.target.value)}
fullWidth
variant="outlined"
InputProps={{
sx: { backgroundColor: 'rgba(255,255,255,0.95)' },
}}
>
{field.options?.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</TextField>
) : (
<TextField
size="small"
value={block.fields[field.name]}
onChange={(event) => onUpdateField(block.id, field.name, event.target.value)}
placeholder={field.placeholder}
fullWidth
variant="outlined"
type={field.type === 'number' ? 'number' : 'text'}
InputProps={{
sx: { backgroundColor: 'rgba(255,255,255,0.95)' },
}}
/>
)}
</Box>
))}
</Box>
)
}
export const BlockItem = ({
block,
definition,
index,
total,
onRequestAddBlock,
onMoveBlock,
onDuplicateBlock,
onRemoveBlock,
onUpdateField,
renderNestedList,
}: BlockItemProps) => (
<Box className={styles.blockCard} data-category={definition.category}>
<Box className={styles.blockHeader}>
<Typography className={styles.blockTitle}>{definition.label}</Typography>
<Box className={styles.blockActions}>
<Tooltip title="Move up">
<span>
<IconButton
size="small"
onClick={() => onMoveBlock(block.id, 'up')}
disabled={index === 0}
sx={{ color: 'rgba(255,255,255,0.85)' }}
>
<ArrowUpward fontSize="inherit" />
</IconButton>
</span>
</Tooltip>
<Tooltip title="Move down">
<span>
<IconButton
size="small"
onClick={() => onMoveBlock(block.id, 'down')}
disabled={index === total - 1}
sx={{ color: 'rgba(255,255,255,0.85)' }}
>
<ArrowDownward fontSize="inherit" />
</IconButton>
</span>
</Tooltip>
<Tooltip title="Duplicate block">
<IconButton
size="small"
onClick={() => onDuplicateBlock(block.id)}
sx={{ color: 'rgba(255,255,255,0.85)' }}
>
<ContentCopy fontSize="inherit" />
</IconButton>
</Tooltip>
<Tooltip title="Delete block">
<IconButton
size="small"
onClick={() => onRemoveBlock(block.id)}
sx={{ color: 'rgba(255,255,255,0.85)' }}
>
<DeleteIcon fontSize="inherit" />
</IconButton>
</Tooltip>
</Box>
</Box>
<BlockFields block={block} definition={definition} onUpdateField={onUpdateField} />
{definition.hasChildren && (
<BlockSection
title="Then"
blocks={block.children}
parentId={block.id}
slot="children"
onRequestAddBlock={onRequestAddBlock}
renderNestedList={renderNestedList}
/>
)}
{definition.hasElseChildren && (
<BlockSection
title="Else"
blocks={block.elseChildren}
parentId={block.id}
slot="elseChildren"
onRequestAddBlock={onRequestAddBlock}
renderNestedList={renderNestedList}
/>
)}
</Box>
)

View File

@@ -1,22 +1,8 @@
import type { MouseEvent } from 'react'
import {
Box,
Button,
IconButton,
MenuItem,
TextField,
Tooltip,
Typography,
} from '@mui/material'
import {
Add as AddIcon,
ArrowDownward,
ArrowUpward,
ContentCopy,
Delete as DeleteIcon,
} from '@mui/icons-material'
import { Box } from '@mui/material'
import type { BlockDefinition, BlockSlot, LuaBlock, LuaBlockType } from '../types'
import styles from '../LuaBlocksEditor.module.scss'
import { BlockItem } from './BlockItem'
interface BlockListProps {
blocks: LuaBlock[]
@@ -31,89 +17,6 @@ interface BlockListProps {
onUpdateField: (blockId: string, fieldName: string, value: string) => void
}
const renderBlockFields = (
block: LuaBlock,
definition: BlockDefinition,
onUpdateField: (blockId: string, fieldName: string, value: string) => void
) => {
if (definition.fields.length === 0) return null
return (
<Box className={styles.blockFields}>
{definition.fields.map((field) => (
<Box key={field.name}>
<Typography className={styles.blockFieldLabel}>{field.label}</Typography>
{field.type === 'select' ? (
<TextField
select
size="small"
value={block.fields[field.name]}
onChange={(event) => onUpdateField(block.id, field.name, event.target.value)}
fullWidth
variant="outlined"
InputProps={{
sx: { backgroundColor: 'rgba(255,255,255,0.95)' },
}}
>
{field.options?.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</TextField>
) : (
<TextField
size="small"
value={block.fields[field.name]}
onChange={(event) => onUpdateField(block.id, field.name, event.target.value)}
placeholder={field.placeholder}
fullWidth
variant="outlined"
type={field.type === 'number' ? 'number' : 'text'}
InputProps={{
sx: { backgroundColor: 'rgba(255,255,255,0.95)' },
}}
/>
)}
</Box>
))}
</Box>
)
}
const renderBlockSection = (
title: string,
blocks: LuaBlock[] | undefined,
parentId: string | null,
slot: BlockSlot,
onRequestAddBlock: (
event: MouseEvent<HTMLElement>,
target: { parentId: string | null; slot: BlockSlot }
) => void,
renderBlockCard: (block: LuaBlock, index: number, total: number) => JSX.Element | null
) => (
<Box className={styles.blockSection}>
<Box className={styles.blockSectionHeader}>
<Typography className={styles.blockSectionTitle}>{title}</Typography>
<Button
size="small"
variant="contained"
onClick={(event) => onRequestAddBlock(event, { parentId, slot })}
startIcon={<AddIcon fontSize="small" />}
>
Add block
</Button>
</Box>
<Box className={styles.blockSectionBody}>
{blocks && blocks.length > 0 ? (
blocks.map((child, index) => renderBlockCard(child, index, blocks.length))
) : (
<Box className={styles.blockEmpty}>Drop blocks here to build this section.</Box>
)}
</Box>
</Box>
)
export const BlockList = ({
blocks,
blockDefinitionMap,
@@ -123,78 +26,40 @@ export const BlockList = ({
onRemoveBlock,
onUpdateField,
}: BlockListProps) => {
const renderBlockCard = (block: LuaBlock, index: number, total: number) => {
const definition = blockDefinitionMap.get(block.type)
if (!definition) return null
return (
<Box key={block.id} className={styles.blockCard} data-category={definition.category}>
<Box className={styles.blockHeader}>
<Typography className={styles.blockTitle}>{definition.label}</Typography>
<Box className={styles.blockActions}>
<Tooltip title="Move up">
<span>
<IconButton
size="small"
onClick={() => onMoveBlock(block.id, 'up')}
disabled={index === 0}
sx={{ color: 'rgba(255,255,255,0.85)' }}
>
<ArrowUpward fontSize="inherit" />
</IconButton>
</span>
</Tooltip>
<Tooltip title="Move down">
<span>
<IconButton
size="small"
onClick={() => onMoveBlock(block.id, 'down')}
disabled={index === total - 1}
sx={{ color: 'rgba(255,255,255,0.85)' }}
>
<ArrowDownward fontSize="inherit" />
</IconButton>
</span>
</Tooltip>
<Tooltip title="Duplicate block">
<IconButton
size="small"
onClick={() => onDuplicateBlock(block.id)}
sx={{ color: 'rgba(255,255,255,0.85)' }}
>
<ContentCopy fontSize="inherit" />
</IconButton>
</Tooltip>
<Tooltip title="Delete block">
<IconButton
size="small"
onClick={() => onRemoveBlock(block.id)}
sx={{ color: 'rgba(255,255,255,0.85)' }}
>
<DeleteIcon fontSize="inherit" />
</IconButton>
</Tooltip>
</Box>
</Box>
{renderBlockFields(block, definition, onUpdateField)}
{definition.hasChildren &&
renderBlockSection('Then', block.children, block.id, 'children', onRequestAddBlock, renderBlockCard)}
{definition.hasElseChildren &&
renderBlockSection(
'Else',
block.elseChildren,
block.id,
'elseChildren',
onRequestAddBlock,
renderBlockCard
)}
</Box>
)
}
const renderNestedList = (childBlocks?: LuaBlock[]) => (
<BlockList
blocks={childBlocks ?? []}
blockDefinitionMap={blockDefinitionMap}
onRequestAddBlock={onRequestAddBlock}
onMoveBlock={onMoveBlock}
onDuplicateBlock={onDuplicateBlock}
onRemoveBlock={onRemoveBlock}
onUpdateField={onUpdateField}
/>
)
return (
<Box className={styles.blockStack}>
{blocks.map((block, index) => renderBlockCard(block, index, blocks.length))}
{blocks.map((block, index) => {
const definition = blockDefinitionMap.get(block.type)
if (!definition) return null
return (
<BlockItem
key={block.id}
block={block}
definition={definition}
index={index}
total={blocks.length}
onRequestAddBlock={onRequestAddBlock}
onMoveBlock={onMoveBlock}
onDuplicateBlock={onDuplicateBlock}
onRemoveBlock={onRemoveBlock}
onUpdateField={onUpdateField}
renderNestedList={renderNestedList}
/>
)
})}
</Box>
)
}

View File

@@ -0,0 +1,20 @@
import type { BlockCategory, BlockDefinition } from '../types'
const createCategoryIndex = (): Record<BlockCategory, BlockDefinition[]> => ({
Basics: [],
Logic: [],
Loops: [],
Data: [],
Functions: [],
})
export const groupBlockDefinitionsByCategory = (definitions: BlockDefinition[]) => {
const categories = createCategoryIndex()
definitions.forEach((definition) => {
categories[definition.category].push(definition)
})
return categories
}
export const buildBlockDefinitionMap = (definitions: BlockDefinition[]) =>
new Map(definitions.map((definition) => [definition.type, definition]))

View File

@@ -1,4 +1,4 @@
import type { BlockCategory, BlockDefinition } from '../types'
import type { BlockDefinition } from '../types'
import { basicBlocks } from './basics'
import { dataBlocks } from './data'
import { functionBlocks } from './functions'
@@ -13,21 +13,4 @@ export const BLOCK_DEFINITIONS: BlockDefinition[] = [
...functionBlocks,
]
const createCategoryIndex = (): Record<BlockCategory, BlockDefinition[]> => ({
Basics: [],
Logic: [],
Loops: [],
Data: [],
Functions: [],
})
export const groupBlockDefinitionsByCategory = (definitions: BlockDefinition[]) => {
const categories = createCategoryIndex()
definitions.forEach((definition) => {
categories[definition.category].push(definition)
})
return categories
}
export const buildBlockDefinitionMap = (definitions: BlockDefinition[]) =>
new Map(definitions.map((definition) => [definition.type, definition]))
export { buildBlockDefinitionMap, groupBlockDefinitionsByCategory } from './grouping'