compute the terminal state of a step and apply it on newly created children

maxime 1 year ago
parent 5cc25772ff
commit 293d21c162

@ -1,26 +1,25 @@
import "../../style/steps_tree.css" import "../../style/steps_tree.css"
import { StepInfoNode } from "../../model/tactic/Tactic" import { StepInfoNode } from "../../model/tactic/Tactic"
import BendableArrow from "../arrows/BendableArrow" import BendableArrow from "../arrows/BendableArrow"
import { useRef, useState } from "react" import { useRef } from "react"
import AddSvg from "../../assets/icon/add.svg?react" import AddSvg from "../../assets/icon/add.svg?react"
import RemoveSvg from "../../assets/icon/remove.svg?react" import RemoveSvg from "../../assets/icon/remove.svg?react"
export interface StepsTreeProps { export interface StepsTreeProps {
root: StepInfoNode root: StepInfoNode
selectedStepId: number
onAddChildren: (parent: StepInfoNode) => void onAddChildren: (parent: StepInfoNode) => void
onRemoveNode: (node: StepInfoNode) => void onRemoveNode: (node: StepInfoNode) => void
onStepSelected: (node: StepInfoNode) => void onStepSelected: (node: StepInfoNode) => void
} }
export default function StepsTree({ export default function StepsTree({
root, root,
onAddChildren, selectedStepId,
onRemoveNode, onAddChildren,
onStepSelected, onRemoveNode,
}: StepsTreeProps) { onStepSelected,
}: StepsTreeProps) {
const [selectedStepId, setSelectedStepId] = useState(root.id)
return ( return (
<div className="steps-tree"> <div className="steps-tree">
<StepsTreeNode <StepsTreeNode
@ -29,10 +28,7 @@ export default function StepsTree({
selectedStepId={selectedStepId} selectedStepId={selectedStepId}
onAddChildren={onAddChildren} onAddChildren={onAddChildren}
onRemoveNode={onRemoveNode} onRemoveNode={onRemoveNode}
onStepSelected={(step) => { onStepSelected={onStepSelected}
setSelectedStepId(step.id)
onStepSelected(step)
}}
/> />
</div> </div>
) )
@ -41,34 +37,31 @@ export default function StepsTree({
interface StepsTreeContentProps { interface StepsTreeContentProps {
node: StepInfoNode node: StepInfoNode
isNodeRoot: boolean isNodeRoot: boolean
selectedStepId: number, selectedStepId: number
onAddChildren: (parent: StepInfoNode) => void onAddChildren: (parent: StepInfoNode) => void
onRemoveNode: (node: StepInfoNode) => void onRemoveNode: (node: StepInfoNode) => void
onStepSelected: (node: StepInfoNode) => void onStepSelected: (node: StepInfoNode) => void
} }
function StepsTreeNode({ function StepsTreeNode({
node, node,
isNodeRoot, isNodeRoot,
selectedStepId, selectedStepId,
onAddChildren, onAddChildren,
onRemoveNode, onRemoveNode,
onStepSelected, onStepSelected,
}: StepsTreeContentProps) { }: StepsTreeContentProps) {
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
return ( return (
<div ref={ref} <div ref={ref} className={"step-group"}>
className={"step-group"}>
{node.children.map((child) => ( {node.children.map((child) => (
<BendableArrow <BendableArrow
key={child.id} key={child.id}
area={ref} area={ref}
startPos={"step-piece-" + node.id} startPos={"step-piece-" + node.id}
segments={[{next: "step-piece-" + child.id}]} segments={[{ next: "step-piece-" + child.id }]}
onSegmentsChanges={() => { onSegmentsChanges={() => {}}
}}
forceStraight={true} forceStraight={true}
wavy={false} wavy={false}
//TODO remove magic constants //TODO remove magic constants
@ -80,7 +73,9 @@ function StepsTreeNode({
id={node.id} id={node.id}
isSelected={selectedStepId === node.id} isSelected={selectedStepId === node.id}
onAddButtonClicked={() => onAddChildren(node)} onAddButtonClicked={() => onAddChildren(node)}
onRemoveButtonClicked={isNodeRoot ? undefined : () => onRemoveNode(node)} onRemoveButtonClicked={
isNodeRoot ? undefined : () => onRemoveNode(node)
}
onSelected={() => onStepSelected(node)} onSelected={() => onStepSelected(node)}
/> />
<div className={"step-children"}> <div className={"step-children"}>
@ -109,17 +104,19 @@ interface StepPieceProps {
} }
function StepPiece({ function StepPiece({
id, id,
isSelected, isSelected,
onAddButtonClicked, onAddButtonClicked,
onRemoveButtonClicked, onRemoveButtonClicked,
onSelected, onSelected,
}: StepPieceProps) { }: StepPieceProps) {
return ( return (
<div <div
id={"step-piece-" + id} id={"step-piece-" + id}
tabIndex={1} tabIndex={1}
className={"step-piece " + (isSelected ? "step-piece-selected" : "")} className={
"step-piece " + (isSelected ? "step-piece-selected" : "")
}
onClick={onSelected}> onClick={onSelected}>
<div className="step-piece-actions"> <div className="step-piece-actions">
{onAddButtonClicked && ( {onAddButtonClicked && (

@ -40,13 +40,16 @@ export function removeStepNode(
* @param root * @param root
*/ */
export function getAvailableId(root: StepInfoNode): number { export function getAvailableId(root: StepInfoNode): number {
const acc = (root: StepInfoNode): number => Math.max(root.id, ...root.children.map(acc)) const acc = (root: StepInfoNode): number =>
Math.max(root.id, ...root.children.map(acc))
return acc(root) + 1 return acc(root) + 1
} }
export function getParent(root: StepInfoNode, node: StepInfoNode): StepInfoNode | null { export function getParent(
if (root.children.find(n => n.id === node.id)) root: StepInfoNode,
return root node: StepInfoNode,
): StepInfoNode | null {
if (root.children.find((n) => n.id === node.id)) return root
for (const child of root.children) { for (const child of root.children) {
const result = getParent(child, node) const result = getParent(child, node)

@ -5,6 +5,7 @@ import {
Player, Player,
PlayerInfo, PlayerInfo,
PlayerLike, PlayerLike,
PlayerPhantom,
PlayerTeam, PlayerTeam,
} from "../model/tactic/Player" } from "../model/tactic/Player"
import { import {
@ -15,13 +16,18 @@ import {
} from "../model/tactic/CourtObjects" } from "../model/tactic/CourtObjects"
import { import {
ComponentId, ComponentId,
TacticComponent,
StepContent, StepContent,
TacticComponent,
} from "../model/tactic/Tactic" } from "../model/tactic/Tactic"
import { overlaps } from "../geo/Box" import { overlaps } from "../geo/Box"
import { RackedCourtObject, RackedPlayer } from "./RackedItems" import { RackedCourtObject, RackedPlayer } from "./RackedItems"
import { changePlayerBallState, getComponent, getOrigin } from "./PlayerDomains" import {
changePlayerBallState,
computePhantomPositioning,
getComponent,
getOrigin,
} from "./PlayerDomains"
import { ActionKind } from "../model/tactic/Action.ts" import { ActionKind } from "../model/tactic/Action.ts"
export function placePlayerAt( export function placePlayerAt(
@ -289,3 +295,85 @@ export function getRackPlayers(
) )
.map((key) => ({ team, key })) .map((key) => ({ team, key }))
} }
/**
* Returns a step content that only contains the terminal state of each components inside the given content
* @param content
* @param courtArea
*/
export function getTerminalState(
content: StepContent,
courtArea: DOMRect,
): StepContent {
const nonPhantomComponents: (Player | CourtObject)[] =
content.components.filter((c) => c.type !== "phantom") as (
| Player
| CourtObject
)[]
const componentsTargetedState = nonPhantomComponents.map((comp) =>
comp.type === "player"
? getPlayerTerminalState(comp, content, courtArea)
: comp,
)
return {
components: componentsTargetedState,
}
}
function getPlayerTerminalState(
player: Player,
content: StepContent,
area: DOMRect,
): Player {
function stateAfter(state: BallState): BallState {
switch (state) {
case BallState.HOLDS_ORIGIN:
return BallState.HOLDS_ORIGIN
case BallState.PASSED_ORIGIN:
case BallState.PASSED:
return BallState.NONE
case BallState.HOLDS_BY_PASS:
return BallState.HOLDS_ORIGIN
case BallState.NONE:
return BallState.NONE
}
}
function getTerminalPos(component: PlayerLike): Pos {
return component.type === "phantom"
? computePhantomPositioning(component, content, area)
: component.pos
}
const phantoms = player.path?.items
if (!phantoms || phantoms.length === 0) {
const pos = getTerminalPos(player)
return {
...player,
ballState: stateAfter(player.ballState),
actions: [],
pos,
}
}
const lastPhantomId = phantoms[phantoms.length - 1]
const lastPhantom = content.components.find(
(c) => c.id === lastPhantomId,
)! as PlayerPhantom
const pos = getTerminalPos(lastPhantom)
return {
type: "player",
path: { items: [] },
role: player.role,
team: player.team,
actions: [],
ballState: stateAfter(lastPhantom.ballState),
id: player.id,
pos,
}
}

@ -42,6 +42,7 @@ import {
dropBallOnComponent, dropBallOnComponent,
getComponentCollided, getComponentCollided,
getRackPlayers, getRackPlayers,
getTerminalState,
moveComponent, moveComponent,
placeBallAt, placeBallAt,
placeObjectAt, placeObjectAt,
@ -79,7 +80,12 @@ import {
import { CourtBall } from "../components/editor/CourtBall" import { CourtBall } from "../components/editor/CourtBall"
import { useNavigate, useParams } from "react-router-dom" import { useNavigate, useParams } from "react-router-dom"
import StepsTree from "../components/editor/StepsTree" import StepsTree from "../components/editor/StepsTree"
import { addStepNode, getAvailableId, getParent, removeStepNode } from "../editor/StepsDomain" import {
addStepNode,
getAvailableId,
getParent,
removeStepNode,
} from "../editor/StepsDomain"
const ERROR_STYLE: CSSProperties = { const ERROR_STYLE: CSSProperties = {
borderColor: "red", borderColor: "red",
@ -96,7 +102,6 @@ interface TacticDto {
id: number id: number
name: string name: string
courtType: CourtType courtType: CourtType
content: { components: TacticComponent[] }
root: StepInfoNode root: StepInfoNode
} }
@ -108,78 +113,124 @@ export default function Editor({ guestMode }: EditorPageProps) {
return <EditorPortal guestMode={guestMode} /> return <EditorPortal guestMode={guestMode} />
} }
function EditorPortal({ guestMode }: EditorPageProps) { function EditorPortal({ guestMode }: EditorPageProps) {
return guestMode ? <GuestModeEditor /> : <UserModeEditor /> return guestMode ? <GuestModeEditor /> : <UserModeEditor />
} }
function GuestModeEditor() { function GuestModeEditor() {
const storageContent = localStorage.getItem(GUEST_MODE_STEP_CONTENT_STORAGE_KEY + "0") const storageContent = localStorage.getItem(
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + "0",
)
const stepInitialContent = ({ const stepInitialContent = {
...(storageContent == null ? { components: [] } : JSON.parse(storageContent)), ...(storageContent == null
? { components: [] }
: JSON.parse(storageContent)),
stepId: 0, stepId: 0,
}) }
// initialize local storage if we launch in guest mode // initialize local storage if we launch in guest mode
if (storageContent == null) { if (storageContent == null) {
localStorage.setItem(GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY, JSON.stringify({ id: 0, children: [] })) localStorage.setItem(
GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,
JSON.stringify({ id: 0, children: [] }),
)
localStorage.setItem( localStorage.setItem(
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + stepInitialContent.stepId, GUEST_MODE_STEP_CONTENT_STORAGE_KEY + stepInitialContent.stepId,
JSON.stringify(stepInitialContent), JSON.stringify(stepInitialContent),
) )
} }
const [stepId, setStepId] = useState(DEFAULT_STEP_ID) const [stepId, setStepId] = useState(DEFAULT_STEP_ID)
const [stepContent, setStepContent, saveState] = useContentState( const [stepContent, setStepContent, saveState] = useContentState(
stepInitialContent, stepInitialContent,
SaveStates.Guest, SaveStates.Guest,
useMemo(() => debounceAsync(async (content: StepContent) => { useMemo(
localStorage.setItem( () =>
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + stepId, debounceAsync(async (content: StepContent) => {
JSON.stringify(content), localStorage.setItem(
) GUEST_MODE_STEP_CONTENT_STORAGE_KEY + stepId,
return SaveStates.Guest JSON.stringify(content),
}, 250), [stepId]), )
return SaveStates.Guest
}, 250),
[stepId],
),
) )
return <EditorPage return (
tactic={({ <EditorPage
id: -1, tactic={{
rootStepNode: JSON.parse(localStorage.getItem(GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY)!), id: -1,
name: localStorage.getItem(GUEST_MODE_TITLE_STORAGE_KEY) ?? "Nouvelle Tactique", rootStepNode: JSON.parse(
courtType: "PLAIN", localStorage.getItem(
})} GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,
currentStepContent={stepContent} )!,
setCurrentStepContent={(content) => setStepContent(content, true)} ),
saveState={saveState} name:
currentStepId={stepId} localStorage.getItem(GUEST_MODE_TITLE_STORAGE_KEY) ??
onNameChange={useCallback(async name => { "Nouvelle Tactique",
localStorage.setItem(GUEST_MODE_TITLE_STORAGE_KEY, name) courtType: "PLAIN",
return true //simulate that the name has been changed }}
}, [])} currentStepContent={stepContent}
selectStep={useCallback(step => { setCurrentStepContent={(content) => setStepContent(content, true)}
setStepId(step) saveState={saveState}
setStepContent({ ...JSON.parse(localStorage.getItem(GUEST_MODE_STEP_CONTENT_STORAGE_KEY + step)!) }, false) currentStepId={stepId}
return onNameChange={useCallback(async (name) => {
}, [setStepContent])} localStorage.setItem(GUEST_MODE_TITLE_STORAGE_KEY, name)
onAddStep={useCallback(async (parent, content) => { return true //simulate that the name has been changed
const root: StepInfoNode = JSON.parse(localStorage.getItem(GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY)!) }, [])}
selectStep={useCallback(
const nodeId = getAvailableId(root) (step) => {
const node = { id: nodeId, children: [] } setStepId(step)
setStepContent(
localStorage.setItem(GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY, JSON.stringify(addStepNode(root, parent, node))) {
localStorage.setItem(GUEST_MODE_STEP_CONTENT_STORAGE_KEY + node.id, JSON.stringify(content)) ...JSON.parse(
return node localStorage.getItem(
}, [])} GUEST_MODE_STEP_CONTENT_STORAGE_KEY + step,
onRemoveStep={useCallback(async step => { )!,
const root: StepInfoNode = JSON.parse(localStorage.getItem(GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY)!) ),
localStorage.setItem(GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY, JSON.stringify(removeStepNode(root, step))) },
return true false,
}, [])} )
/> return
},
[setStepContent],
)}
onAddStep={useCallback(async (parent, content) => {
const root: StepInfoNode = JSON.parse(
localStorage.getItem(
GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,
)!,
)
const nodeId = getAvailableId(root)
const node = { id: nodeId, children: [] }
localStorage.setItem(
GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,
JSON.stringify(addStepNode(root, parent, node)),
)
localStorage.setItem(
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + node.id,
JSON.stringify(content),
)
return node
}, [])}
onRemoveStep={useCallback(async (step) => {
const root: StepInfoNode = JSON.parse(
localStorage.getItem(
GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,
)!,
)
localStorage.setItem(
GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,
JSON.stringify(removeStepNode(root, step)),
)
return true
}, [])}
/>
)
} }
function UserModeEditor() { function UserModeEditor() {
@ -188,12 +239,36 @@ function UserModeEditor() {
const id = parseInt(idStr!) const id = parseInt(idStr!)
const navigation = useNavigate() const navigation = useNavigate()
useEffect(() => { const [stepId, setStepId] = useState(1)
const [stepContent, setStepContent, saveState] =
useContentState<StepContent>(
{ components: [] },
SaveStates.Ok,
useMemo(
() =>
debounceAsync(async (content: StepContent) => {
const response = await fetchAPI(
`tactics/${id}/steps/${stepId}`,
{
content: {
components: content.components,
},
},
"PUT",
)
return response.ok ? SaveStates.Ok : SaveStates.Err
}, 250),
[id, stepId],
),
)
useEffect(() => {
async function initialize() { async function initialize() {
const infoResponsePromise = fetchAPIGet(`tactics/${id}`) const infoResponsePromise = fetchAPIGet(`tactics/${id}`)
const treeResponsePromise = fetchAPIGet(`tactics/${id}/tree`) const treeResponsePromise = fetchAPIGet(`tactics/${id}/tree`)
const contentResponsePromise = fetchAPIGet(`tactics/${id}/steps/${DEFAULT_STEP_ID}`) const contentResponsePromise = fetchAPIGet(
`tactics/${id}/steps/${DEFAULT_STEP_ID}`,
)
const infoResponse = await infoResponsePromise const infoResponse = await infoResponsePromise
const treeResponse = await treeResponsePromise const treeResponse = await treeResponsePromise
@ -212,82 +287,70 @@ function UserModeEditor() {
const content = await contentResponse.json() const content = await contentResponse.json()
const { root } = await treeResponse.json() const { root } = await treeResponse.json()
setTactic({ id, name, courtType, content, root }) setTactic({ id, name, courtType, root })
setStepContent(content, false)
} }
initialize() initialize()
}, [id, idStr, navigation]) }, [id, idStr, navigation])
const onNameChange = useCallback(
(name: string) =>
fetchAPI(`tactics/${id}/name`, { name }, "PUT").then((r) => r.ok),
[id],
)
const [stepId, setStepId] = useState(1) const selectStep = useCallback(
const [stepContent, setStepContent, saveState] = useContentState( async (step: number) => {
tactic?.content ?? { components: [] }, const response = await fetchAPIGet(`tactics/${id}/steps/${step}`)
SaveStates.Ok, if (!response.ok) return
useMemo(() => debounceAsync(async (content: StepContent) => { setStepId(step)
const response = await fetchAPI( setStepContent({ ...(await response.json()) }, false)
`tactics/${id}/steps/${stepId}`, },
{ [id, setStepContent],
content: { )
components: content.components,
}, const onAddStep = useCallback(
}, async (parent: StepInfoNode, content: StepContent) => {
"PUT", const response = await fetchAPI(`tactics/${id}/steps`, {
) parentId: parent.id,
return response.ok ? SaveStates.Ok : SaveStates.Err content,
}, 250), [id, stepId]), })
if (!response.ok) return null
const { stepId } = await response.json()
return { id: stepId, children: [] }
},
[id],
)
const onRemoveStep = useCallback(
(step: StepInfoNode) =>
fetchAPI(`tactics/${id}/steps/${step.id}`, {}, "DELETE").then(
(r) => r.ok,
),
[id],
)
if (!tactic) return <EditorLoadingScreen />
return (
<EditorPage
tactic={{
id,
name: tactic?.name ?? "",
rootStepNode: tactic?.root ?? { id: stepId, children: [] },
courtType: tactic?.courtType,
}}
currentStepId={stepId}
currentStepContent={stepContent}
setCurrentStepContent={(content) => setStepContent(content, true)}
saveState={saveState}
onNameChange={onNameChange}
selectStep={selectStep}
onAddStep={onAddStep}
onRemoveStep={onRemoveStep}
/>
) )
const onNameChange = useCallback((name: string) =>
fetchAPI(`tactics/${id}/name`, { name }, "PUT")
.then((r) => r.ok)
, [id])
const selectStep = useCallback(async (step: number) => {
const response = await fetchAPIGet(`tactics/${id}/steps/${step}`)
if (!response.ok)
return
setStepId(step)
setStepContent({ ...await response.json() }, false)
}, [id, setStepContent])
const onAddStep = useCallback(async (parent: StepInfoNode, content: StepContent) => {
const response = await fetchAPI(`tactics/${id}/steps`, {
parentId: parent.id,
content
})
if (!response.ok)
return null
const { stepId } = await response.json()
return { id: stepId, children: [] }
}, [id])
const onRemoveStep = useCallback((step: StepInfoNode) =>
fetchAPI(
`tactics/${id}/steps/${step.id}`,
{},
"DELETE",
).then(r => r.ok)
, [id])
if (!tactic)
return <EditorLoadingScreen />
return <EditorPage
tactic={({
id,
name: tactic?.name ?? "",
rootStepNode: tactic?.root ?? { id: stepId, children: [] },
courtType: tactic?.courtType,
})}
currentStepId={stepId}
currentStepContent={stepContent}
setCurrentStepContent={(content) => setStepContent(content, true)}
saveState={saveState}
onNameChange={onNameChange}
selectStep={selectStep}
onAddStep={onAddStep}
onRemoveStep={onRemoveStep}
/>
} }
function EditorLoadingScreen() { function EditorLoadingScreen() {
@ -304,26 +367,23 @@ export interface EditorViewProps {
selectStep: (stepId: number) => void selectStep: (stepId: number) => void
onNameChange: (name: string) => Promise<boolean> onNameChange: (name: string) => Promise<boolean>
onRemoveStep: (step: StepInfoNode) => Promise<boolean> onRemoveStep: (step: StepInfoNode) => Promise<boolean>
onAddStep: (parent: StepInfoNode, content: StepContent) => Promise<StepInfoNode | null> onAddStep: (
parent: StepInfoNode,
content: StepContent,
) => Promise<StepInfoNode | null>
} }
function EditorPage({ function EditorPage({
tactic: { tactic: { name, rootStepNode: initialStepsNode, courtType },
name, currentStepId,
rootStepNode: initialStepsNode, setCurrentStepContent: setContent,
courtType, currentStepContent: content,
}, saveState,
setCurrentStepContent: setContent, onNameChange,
currentStepContent: content, selectStep,
saveState, onRemoveStep,
onNameChange, onAddStep,
selectStep, }: EditorViewProps) {
onRemoveStep,
onAddStep,
}: EditorViewProps) {
const [titleStyle, setTitleStyle] = useState<CSSProperties>({}) const [titleStyle, setTitleStyle] = useState<CSSProperties>({})
const [rootStepsNode, setRootStepsNode] = useState(initialStepsNode) const [rootStepsNode, setRootStepsNode] = useState(initialStepsNode)
@ -693,29 +753,35 @@ function EditorPage({
</div> </div>
<EditorStepsTree <EditorStepsTree
isVisible={isStepsTreeVisible} isVisible={isStepsTreeVisible}
selectedStepId={currentStepId}
root={rootStepsNode} root={rootStepsNode}
onAddChildren={useCallback( onAddChildren={useCallback(
async (parent) => { async (parent) => {
const addedNode = await onAddStep(parent, content) const addedNode = await onAddStep(
parent,
getTerminalState(content, courtBounds()),
)
if (addedNode == null) { if (addedNode == null) {
console.error( console.error(
"could not add step : onAddStep returned null node", "could not add step : onAddStep returned null node",
) )
return return
} }
selectStep(addedNode.id)
setRootStepsNode((root) => setRootStepsNode((root) =>
addStepNode(root, parent, addedNode), addStepNode(root, parent, addedNode),
) )
}, },
[content, onAddStep], [content, courtBounds, onAddStep, selectStep],
)} )}
onRemoveNode={useCallback( onRemoveNode={useCallback(
async (removed) => { async (removed) => {
const isOk = await onRemoveStep(removed) const isOk = await onRemoveStep(removed)
selectStep(getParent(rootStepsNode, removed)!.id) selectStep(getParent(rootStepsNode, removed)!.id)
if (isOk) setRootStepsNode( if (isOk)
(root) => removeStepNode(root, removed)!, setRootStepsNode(
) (root) => removeStepNode(root, removed)!,
)
}, },
[rootStepsNode, onRemoveStep, selectStep], [rootStepsNode, onRemoveStep, selectStep],
)} )}
@ -731,6 +797,7 @@ function EditorPage({
interface EditorStepsTreeProps { interface EditorStepsTreeProps {
isVisible: boolean isVisible: boolean
selectedStepId: number
root: StepInfoNode root: StepInfoNode
onAddChildren: (parent: StepInfoNode) => void onAddChildren: (parent: StepInfoNode) => void
onRemoveNode: (node: StepInfoNode) => void onRemoveNode: (node: StepInfoNode) => void
@ -739,6 +806,7 @@ interface EditorStepsTreeProps {
function EditorStepsTree({ function EditorStepsTree({
isVisible, isVisible,
selectedStepId,
root, root,
onAddChildren, onAddChildren,
onRemoveNode, onRemoveNode,
@ -752,6 +820,7 @@ function EditorStepsTree({
}}> }}>
<StepsTree <StepsTree
root={root} root={root}
selectedStepId={selectedStepId}
onStepSelected={onStepSelected} onStepSelected={onStepSelected}
onAddChildren={onAddChildren} onAddChildren={onAddChildren}
onRemoveNode={onRemoveNode} onRemoveNode={onRemoveNode}
@ -869,7 +938,7 @@ function CourtPlayerArrowAction({
})) }))
}} }}
onHeadPicked={(headPos) => { onHeadPicked={(headPos) => {
(document.activeElement as HTMLElement).blur() ;(document.activeElement as HTMLElement).blur()
setPreviewAction({ setPreviewAction({
origin: playerInfo.id, origin: playerInfo.id,

@ -32,7 +32,9 @@
background-color: var(--editor-tree-step-piece-hovered); background-color: var(--editor-tree-step-piece-hovered);
} }
.step-piece-selected .step-piece-actions, .step-piece:hover .step-piece-actions, .step-piece:focus-within .step-piece-actions { .step-piece-selected .step-piece-actions,
.step-piece:hover .step-piece-actions,
.step-piece:focus-within .step-piece-actions {
visibility: visible; visibility: visible;
} }

Loading…
Cancel
Save