mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 22:04:56 +00:00
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:
218
frontends/nextjs/src/components/editors/lua/blocks/BlockItem.tsx
Normal file
218
frontends/nextjs/src/components/editors/lua/blocks/BlockItem.tsx
Normal 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>
|
||||
)
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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]))
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user