the tree is now shown with a resizeable curtain-slide view
continuous-integration/drone/push Build was killed Details

maxime.batista 1 year ago committed by maxime
parent 4fe1ddfbd2
commit e936aadb76

@ -1,5 +1,4 @@
set -e #!/usr/bin/env bash
set -xeu set -xeu
export OUTPUT=$1 export OUTPUT=$1

@ -0,0 +1,72 @@
import { ReactNode, useCallback, useEffect, useRef, useState } from "react"
export interface SlideLayoutProps {
children: [ReactNode, ReactNode]
rightWidth: number,
onRightWidthChange: (w: number) => void
}
export default function CurtainLayout({ children, rightWidth, onRightWidthChange }: SlideLayoutProps) {
const curtainRef = useRef<HTMLDivElement>(null)
const sliderRef = useRef<HTMLDivElement>(null)
const resize = useCallback(
(e: MouseEvent) => {
const sliderPosX = e.clientX
const curtainWidth =
curtainRef.current!.getBoundingClientRect().width
onRightWidthChange((sliderPosX / curtainWidth) * 100)
},
[curtainRef, onRightWidthChange],
)
const [resizing, setResizing] = useState(false)
useEffect(() => {
const curtain = curtainRef.current!
const slider = sliderRef.current!
if (resizing) {
const handleMouseUp = () => setResizing(false)
curtain.addEventListener("mousemove", resize)
curtain.addEventListener("mouseup", handleMouseUp)
return () => {
curtain.removeEventListener("mousemove", resize)
curtain.removeEventListener("mouseup", handleMouseUp)
}
}
const handleMouseDown = () => setResizing(true)
slider.addEventListener("mousedown", handleMouseDown)
return () => {
slider.removeEventListener("mousedown", handleMouseDown)
}
}, [sliderRef, curtainRef, resizing, setResizing, resize])
return (
<div className={"curtain"} ref={curtainRef} style={{ display: "flex" }}>
<div className={"curtain-left"} style={{ width: `${rightWidth}%` }}>
{children[0]}
</div>
<div
ref={sliderRef}
style={{
width: 4,
height: "100%",
backgroundColor: "grey",
cursor: "col-resize",
userSelect: "none",
}}></div>
<div
className={"curtain-right"}
style={{ width: `${100 - rightWidth}%` }}>
{children[1]}
</div>
</div>
)
}

@ -20,13 +20,13 @@ interface RackItemProps<E extends { key: string | number }> {
* A container of draggable objects * A container of draggable objects
* */ * */
export function Rack<E extends { key: string | number }>({ export function Rack<E extends { key: string | number }>({
id, id,
objects, objects,
onChange, onChange,
canDetach, canDetach,
onElementDetached, onElementDetached,
render, render,
}: RackProps<E>) { }: RackProps<E>) {
return ( return (
<div <div
id={id} id={id}
@ -44,8 +44,7 @@ export function Rack<E extends { key: string | number }>({
const index = objects.findIndex( const index = objects.findIndex(
(o) => o.key === element.key, (o) => o.key === element.key,
) )
if (onChange) if (onChange) onChange(objects.toSpliced(index, 1))
onChange(objects.toSpliced(index, 1))
onElementDetached(ref, element) onElementDetached(ref, element)
}} }}
@ -56,10 +55,10 @@ export function Rack<E extends { key: string | number }>({
} }
function RackItem<E extends { key: string | number }>({ function RackItem<E extends { key: string | number }>({
item, item,
onTryDetach, onTryDetach,
render, render,
}: RackItemProps<E>) { }: RackItemProps<E>) {
const divRef = useRef<HTMLDivElement>(null) const divRef = useRef<HTMLDivElement>(null)
return ( return (

@ -39,6 +39,11 @@ export interface BendableArrowProps {
startRadius?: number startRadius?: number
endRadius?: number endRadius?: number
/**
* A way to force the arrow to render.
*/
renderDependency?: object | string | number
onDeleteRequested?: () => void onDeleteRequested?: () => void
style?: ArrowStyle style?: ArrowStyle
@ -92,6 +97,7 @@ function constraintInCircle(center: Pos, reference: Pos, radius: number): Pos {
* @param onSegmentsChanges * @param onSegmentsChanges
* @param wavy * @param wavy
* @param forceStraight * @param forceStraight
* @param renderDependency
* @param style * @param style
* @param startRadius * @param startRadius
* @param endRadius * @param endRadius
@ -99,20 +105,22 @@ function constraintInCircle(center: Pos, reference: Pos, radius: number): Pos {
* @constructor * @constructor
*/ */
export default function BendableArrow({ export default function BendableArrow({
area, area,
startPos, startPos,
segments,
onSegmentsChanges,
segments, forceStraight,
onSegmentsChanges, wavy,
forceStraight, renderDependency,
wavy,
style, style,
startRadius = 0, startRadius = 0,
endRadius = 0, endRadius = 0,
onDeleteRequested, onDeleteRequested,
}: BendableArrowProps) { }: BendableArrowProps) {
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const svgRef = useRef<SVGSVGElement>(null) const svgRef = useRef<SVGSVGElement>(null)
const pathRef = useRef<SVGPathElement>(null) const pathRef = useRef<SVGPathElement>(null)
@ -266,8 +274,8 @@ export default function BendableArrow({
const endPrevious = forceStraight const endPrevious = forceStraight
? startRelative ? startRelative
: lastSegment.controlPoint : lastSegment.controlPoint
? posWithinBase(lastSegment.controlPoint, parentBase) ? posWithinBase(lastSegment.controlPoint, parentBase)
: getPosWithinBase(lastSegment.start, parentBase) : getPosWithinBase(lastSegment.start, parentBase)
const tailPos = constraintInCircle( const tailPos = constraintInCircle(
startRelative, startRelative,
@ -305,12 +313,12 @@ export default function BendableArrow({
const segmentsRelatives = ( const segmentsRelatives = (
forceStraight forceStraight
? [ ? [
{ {
start: startPos, start: startPos,
controlPoint: undefined, controlPoint: undefined,
end: lastSegment.end, end: lastSegment.end,
}, },
] ]
: internalSegments : internalSegments
).map(({ start, controlPoint, end }) => { ).map(({ start, controlPoint, end }) => {
const svgPosRelativeToBase = { x: left, y: top } const svgPosRelativeToBase = { x: left, y: top }
@ -326,9 +334,9 @@ export default function BendableArrow({
const controlPointRelative = const controlPointRelative =
controlPoint && !forceStraight controlPoint && !forceStraight
? relativeTo( ? relativeTo(
posWithinBase(controlPoint, parentBase), posWithinBase(controlPoint, parentBase),
svgPosRelativeToBase, svgPosRelativeToBase,
) )
: middle(startRelative, nextRelative) : middle(startRelative, nextRelative)
return { return {
@ -384,11 +392,12 @@ export default function BendableArrow({
]) ])
// Will update the arrow when the props change // Will update the arrow when the props change
useEffect(update, [update]) useEffect(update, [update, renderDependency])
useEffect(() => { useEffect(() => {
const observer = new MutationObserver(update) const observer = new MutationObserver(update)
const config = { attributes: true } const config = { attributes: true }
if (typeof startPos == "string") { if (typeof startPos == "string") {
observer.observe(document.getElementById(startPos)!, config) observer.observe(document.getElementById(startPos)!, config)
} }
@ -454,13 +463,13 @@ export default function BendableArrow({
const smoothCp = beforeSegment const smoothCp = beforeSegment
? add( ? add(
currentPos, currentPos,
minus( minus(
currentPos, currentPos,
beforeSegment.controlPoint ?? beforeSegment.controlPoint ??
middle(beforeSegmentPos, currentPos), middle(beforeSegmentPos, currentPos),
), ),
) )
: segmentCp : segmentCp
const result = searchOnSegment( const result = searchOnSegment(
@ -639,7 +648,7 @@ function wavyBezier(
// 3 : down to middle // 3 : down to middle
let phase = 0 let phase = 0
for (let t = step; t <= 1; ) { for (let t = step; t <= 1;) {
const pos = cubicBeziers(start, cp1, cp2, end, t) const pos = cubicBeziers(start, cp1, cp2, end, t)
const amplification = getVerticalDerivativeProjectionAmplification(t) const amplification = getVerticalDerivativeProjectionAmplification(t)
@ -757,14 +766,14 @@ function searchOnSegment(
* @constructor * @constructor
*/ */
function ArrowPoint({ function ArrowPoint({
className, className,
posRatio, posRatio,
parentBase, parentBase,
onMoves, onMoves,
onPosValidated, onPosValidated,
onRemove, onRemove,
radius = 7, radius = 7,
}: ControlPointProps) { }: ControlPointProps) {
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
const pos = posWithinBase(posRatio, parentBase) const pos = posWithinBase(posRatio, parentBase)

@ -1,4 +1,4 @@
import { ReactElement, ReactNode, RefObject } from "react" import { ReactElement, ReactNode, RefObject, useEffect } from "react"
import { Action } from "../../model/tactic/Action" import { Action } from "../../model/tactic/Action"
import { CourtAction } from "./CourtAction" import { CourtAction } from "./CourtAction"

@ -11,6 +11,8 @@ export interface CourtActionProps {
onActionDeleted: () => void onActionDeleted: () => void
courtRef: RefObject<HTMLElement> courtRef: RefObject<HTMLElement>
isInvalid: boolean isInvalid: boolean
renderDependency?: number
} }
export function CourtAction({ export function CourtAction({
@ -20,6 +22,7 @@ export function CourtAction({
onActionDeleted, onActionDeleted,
courtRef, courtRef,
isInvalid, isInvalid,
renderDependency
}: CourtActionProps) { }: CourtActionProps) {
const color = isInvalid ? "red" : "black" const color = isInvalid ? "red" : "black"
@ -56,6 +59,7 @@ export function CourtAction({
endRadius={action.target ? 26 : 17} endRadius={action.target ? 26 : 17}
startRadius={10} startRadius={10}
onDeleteRequested={onActionDeleted} onDeleteRequested={onActionDeleted}
renderDependency={renderDependency}
style={{ style={{
head, head,
dashArray, dashArray,

@ -63,7 +63,12 @@ function StepsTreeNode({
key={child.id} key={child.id}
area={ref} area={ref}
startPos={"step-piece-" + stepId} startPos={"step-piece-" + stepId}
segments={[{ next: "step-piece-" + getStepName(rootNode, child.id)}]} segments={[
{
next:
"step-piece-" + getStepName(rootNode, child.id),
},
]}
onSegmentsChanges={() => {}} onSegmentsChanges={() => {}}
forceStraight={true} forceStraight={true}
wavy={false} wavy={false}
@ -77,7 +82,9 @@ function StepsTreeNode({
isSelected={selectedStepId === node.id} isSelected={selectedStepId === node.id}
onAddButtonClicked={() => onAddChildren(node)} onAddButtonClicked={() => onAddChildren(node)}
onRemoveButtonClicked={ onRemoveButtonClicked={
rootNode.id === node.id ? undefined : () => onRemoveNode(node) rootNode.id === node.id
? undefined
: () => onRemoveNode(node)
} }
onSelected={() => onStepSelected(node)} onSelected={() => onStepSelected(node)}
/> />

@ -19,14 +19,12 @@ export function addStepNode(
} }
export function getStepName(root: StepInfoNode, step: number): string { export function getStepName(root: StepInfoNode, step: number): string {
let ord = 1 let ord = 1
const nodes = [root] const nodes = [root]
while (nodes.length > 0) { while (nodes.length > 0) {
const node = nodes.pop()! const node = nodes.pop()!
if (node.id === step) if (node.id === step) break
break
ord++ ord++
nodes.push(...[...node.children].reverse()) nodes.push(...[...node.children].reverse())

@ -200,9 +200,9 @@ export function moveComponent(
phantomIdx == 0 phantomIdx == 0
? origin ? origin
: getComponent( : getComponent(
originPathItems[phantomIdx - 1], originPathItems[phantomIdx - 1],
content.components, content.components,
) )
// detach the action from the screen target and transform it to a regular move action to the phantom. // detach the action from the screen target and transform it to a regular move action to the phantom.
content = updateComponent( content = updateComponent(
{ {
@ -210,18 +210,18 @@ export function moveComponent(
actions: playerBeforePhantom.actions.map((a) => actions: playerBeforePhantom.actions.map((a) =>
a.target === referent a.target === referent
? { ? {
...a, ...a,
segments: a.segments.toSpliced( segments: a.segments.toSpliced(
a.segments.length - 2, a.segments.length - 2,
1, 1,
{ {
...a.segments[a.segments.length - 1], ...a.segments[a.segments.length - 1],
next: component.id, next: component.id,
}, },
), ),
target: component.id, target: component.id,
type: ActionKind.MOVE, type: ActionKind.MOVE,
} }
: a, : a,
), ),
}, },
@ -234,9 +234,9 @@ export function moveComponent(
...component, ...component,
pos: isPhantom pos: isPhantom
? { ? {
type: "fixed", type: "fixed",
...newPos, ...newPos,
} }
: newPos, : newPos,
}, },
content, content,
@ -315,15 +315,15 @@ export function computeTerminalState(
content.components.filter((c) => c.type !== "phantom") as ( content.components.filter((c) => c.type !== "phantom") as (
| Player | Player
| CourtObject | CourtObject
)[] )[]
const componentsTargetedState = nonPhantomComponents.map((comp) => const componentsTargetedState = nonPhantomComponents.map((comp) =>
comp.type === "player" comp.type === "player"
? getPlayerTerminalState(comp, content, computedPositions) ? getPlayerTerminalState(comp, content, computedPositions)
: { : {
...comp, ...comp,
frozen: true, frozen: true,
}, },
) )
return { return {
@ -399,8 +399,6 @@ export function drainTerminalStateOnChildContent(
parentTerminalState: StepContent, parentTerminalState: StepContent,
childContent: StepContent, childContent: StepContent,
): StepContent | null { ): StepContent | null {
let gotUpdated = false let gotUpdated = false
for (const parentComponent of parentTerminalState.components) { for (const parentComponent of parentTerminalState.components) {
@ -435,11 +433,17 @@ export function drainTerminalStateOnChildContent(
if (newContentResult) { if (newContentResult) {
gotUpdated = true gotUpdated = true
childContent = newContentResult childContent = newContentResult
childComponent = getComponent<Player>(childComponent.id, newContentResult?.components) childComponent = getComponent<Player>(
childComponent.id,
newContentResult?.components,
)
} }
// update the position of the player if it has been moved // update the position of the player if it has been moved
// also force update if the child component is not frozen (the component was introduced previously by the child step but the parent added it afterward) // also force update if the child component is not frozen (the component was introduced previously by the child step but the parent added it afterward)
if (!childComponent.frozen || !equals(childComponent.pos, parentComponent.pos)) { if (
!childComponent.frozen ||
!equals(childComponent.pos, parentComponent.pos)
) {
gotUpdated = true gotUpdated = true
childContent = updateComponent( childContent = updateComponent(
{ {
@ -460,7 +464,7 @@ export function drainTerminalStateOnChildContent(
(comp) => (comp) =>
comp.type === "phantom" || comp.type === "phantom" ||
!comp.frozen || !comp.frozen ||
tryGetComponent(comp.id, parentTerminalState.components) tryGetComponent(comp.id, parentTerminalState.components),
), ),
} }

@ -92,6 +92,7 @@ import {
getStepNode, getStepNode,
removeStepNode, removeStepNode,
} from "../editor/StepsDomain" } from "../editor/StepsDomain"
import CurtainLayout from "../components/CurtainLayout"
const ERROR_STYLE: CSSProperties = { const ERROR_STYLE: CSSProperties = {
borderColor: "red", borderColor: "red",
@ -186,7 +187,7 @@ function GuestModeEditor() {
const content = JSON.parse( const content = JSON.parse(
localStorage.getItem( localStorage.getItem(
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + GUEST_MODE_STEP_CONTENT_STORAGE_KEY +
stepId, stepId,
)!, )!,
) )
const courtBounds = const courtBounds =
@ -360,10 +361,7 @@ function UserModeEditor() {
const { name, courtType } = await infoResponse.json() const { name, courtType } = await infoResponse.json()
const { root } = await treeResponse.json() const { root } = await treeResponse.json()
if ( if (infoResponse.status == 401 || treeResponse.status == 401) {
infoResponse.status == 401 ||
treeResponse.status == 401
) {
navigation("/login") navigation("/login")
return return
} }
@ -372,7 +370,6 @@ function UserModeEditor() {
`tactics/${tacticId}/steps/${root.id}`, `tactics/${tacticId}/steps/${root.id}`,
) )
const contentResponse = await contentResponsePromise const contentResponse = await contentResponsePromise
if (contentResponse.status == 401) { if (contentResponse.status == 401) {
@ -485,18 +482,18 @@ export interface EditorViewProps {
} }
function EditorPage({ function EditorPage({
tactic: { name, rootStepNode: initialStepsNode, courtType }, tactic: { name, rootStepNode: initialStepsNode, courtType },
currentStepId, currentStepId,
setCurrentStepContent: setContent, setCurrentStepContent: setContent,
currentStepContent: content, currentStepContent: content,
saveState, saveState,
onNameChange, onNameChange,
selectStep, selectStep,
onRemoveStep, onRemoveStep,
onAddStep, onAddStep,
courtRef, courtRef,
}: EditorViewProps) { }: EditorViewProps) {
const [titleStyle, setTitleStyle] = useState<CSSProperties>({}) const [titleStyle, setTitleStyle] = useState<CSSProperties>({})
const [rootStepsNode, setRootStepsNode] = useState(initialStepsNode) const [rootStepsNode, setRootStepsNode] = useState(initialStepsNode)
@ -519,6 +516,8 @@ function EditorPage({
[courtRef], [courtRef],
) )
const [editorContentCurtainWidth, setEditorContentCurtainWidth] = useState(80)
const relativePositions = useMemo(() => { const relativePositions = useMemo(() => {
const courtBounds = courtRef.current?.getBoundingClientRect() const courtBounds = courtRef.current?.getBoundingClientRect()
return courtBounds return courtBounds
@ -635,15 +634,15 @@ function EditorPage({
/> />
), ),
!isFrozen && !isFrozen &&
(info.ballState === BallState.HOLDS_ORIGIN || (info.ballState === BallState.HOLDS_ORIGIN ||
info.ballState === BallState.PASSED_ORIGIN) && ( info.ballState === BallState.PASSED_ORIGIN) && (
<BallAction <BallAction
key={2} key={2}
onDrop={(ballBounds) => { onDrop={(ballBounds) => {
doMoveBall(ballBounds, player) doMoveBall(ballBounds, player)
}} }}
/> />
), ),
] ]
}, },
[content, courtRef, doMoveBall, previewAction?.isInvalid, setContent], [content, courtRef, doMoveBall, previewAction?.isInvalid, setContent],
@ -700,7 +699,15 @@ function EditorPage({
/> />
) )
}, },
[courtRef, content, relativePositions, courtBounds, renderAvailablePlayerActions, validatePlayerPosition, doRemovePlayer], [
courtRef,
content,
relativePositions,
courtBounds,
renderAvailablePlayerActions,
validatePlayerPosition,
doRemovePlayer,
],
) )
const doDeleteAction = useCallback( const doDeleteAction = useCallback(
@ -771,10 +778,111 @@ function EditorPage({
onActionChanges={(action) => onActionChanges={(action) =>
doUpdateAction(component, action, i) doUpdateAction(component, action, i)
} }
renderDependency={editorContentCurtainWidth}
/> />
) )
}), }),
[courtRef, doDeleteAction, doUpdateAction], [courtRef, doDeleteAction, doUpdateAction, editorContentCurtainWidth],
)
const contentNode = (
<div id="content-div">
<div id="racks">
<PlayerRack
id={"allies"}
objects={allies}
setComponents={setComponents}
courtRef={courtRef}
/>
<Rack
id={"objects"}
objects={objects}
onChange={setObjects}
canDetach={useCallback(
(div) =>
overlaps(
courtBounds(),
div.getBoundingClientRect(),
),
[courtBounds],
)}
onElementDetached={useCallback(
(r, e: RackedCourtObject) =>
setContent((content) =>
placeObjectAt(
r.getBoundingClientRect(),
courtBounds(),
e,
content,
),
),
[courtBounds, setContent],
)}
render={renderCourtObject}
/>
<PlayerRack
id={"opponents"}
objects={opponents}
setComponents={setComponents}
courtRef={courtRef}
/>
</div>
<div id="court-div">
<div id="court-div-bounds">
<BasketCourt
components={content.components}
courtImage={<Court courtType={courtType} />}
courtRef={courtRef}
previewAction={previewAction}
renderComponent={renderComponent}
renderActions={renderActions}
/>
</div>
</div>
</div>
)
const stepsTreeNode = (
<EditorStepsTree
selectedStepId={currentStepId}
root={rootStepsNode}
onAddChildren={useCallback(
async (parent) => {
const addedNode = await onAddStep(
parent,
computeTerminalState(content, relativePositions),
)
if (addedNode == null) {
console.error(
"could not add step : onAddStep returned null node",
)
return
}
selectStep(addedNode.id)
setRootStepsNode((root) =>
addStepNode(root, parent, addedNode),
)
},
[content, onAddStep, selectStep, relativePositions],
)}
onRemoveNode={useCallback(
async (removed) => {
const isOk = await onRemoveStep(removed)
selectStep(getParent(rootStepsNode, removed)!.id)
if (isOk)
setRootStepsNode(
(root) => removeStepNode(root, removed)!,
)
},
[rootStepsNode, onRemoveStep, selectStep],
)}
onStepSelected={useCallback(
(node) => selectStep(node.id),
[selectStep],
)}
/>
) )
return ( return (
@ -798,117 +906,31 @@ function EditorPage({
/> />
</div> </div>
<div id="topbar-right"> <div id="topbar-right">
<button onClick={() => setStepsTreeVisible((b) => !b)}> <button
id={"show-steps-button"}
onClick={() => setStepsTreeVisible((b) => !b)}>
ETAPES ETAPES
</button> </button>
</div> </div>
</div> </div>
<div id="editor-div"> <div id="editor-div">
<div id="content-div"> {isStepsTreeVisible ? (
<div id="racks"> <CurtainLayout
<PlayerRack rightWidth={editorContentCurtainWidth}
id={"allies"} onRightWidthChange={setEditorContentCurtainWidth}
objects={allies} >
setComponents={setComponents} {contentNode}
courtRef={courtRef} {stepsTreeNode}
/> </CurtainLayout>
) : (
<Rack contentNode
id={"objects"} )}
objects={objects}
onChange={setObjects}
canDetach={useCallback(
(div) =>
overlaps(
courtBounds(),
div.getBoundingClientRect(),
),
[courtBounds],
)}
onElementDetached={useCallback(
(r, e: RackedCourtObject) =>
setContent((content) =>
placeObjectAt(
r.getBoundingClientRect(),
courtBounds(),
e,
content,
),
),
[courtBounds, setContent],
)}
render={renderCourtObject}
/>
<PlayerRack
id={"opponents"}
objects={opponents}
setComponents={setComponents}
courtRef={courtRef}
/>
</div>
<div id="court-div">
<div id="court-div-bounds">
<BasketCourt
components={content.components}
courtImage={<Court courtType={courtType} />}
courtRef={courtRef}
previewAction={previewAction}
renderComponent={renderComponent}
renderActions={renderActions}
/>
</div>
</div>
</div>
<EditorStepsTree
isVisible={isStepsTreeVisible}
selectedStepId={currentStepId}
root={rootStepsNode}
onAddChildren={useCallback(
async (parent) => {
const addedNode = await onAddStep(
parent,
computeTerminalState(
content,
relativePositions,
),
)
if (addedNode == null) {
console.error(
"could not add step : onAddStep returned null node",
)
return
}
selectStep(addedNode.id)
setRootStepsNode((root) =>
addStepNode(root, parent, addedNode),
)
},
[content, onAddStep, selectStep, relativePositions],
)}
onRemoveNode={useCallback(
async (removed) => {
const isOk = await onRemoveStep(removed)
selectStep(getParent(rootStepsNode, removed)!.id)
if (isOk)
setRootStepsNode(
(root) => removeStepNode(root, removed)!,
)
},
[rootStepsNode, onRemoveStep, selectStep],
)}
onStepSelected={useCallback(
(node) => selectStep(node.id),
[selectStep],
)}
/>
</div> </div>
</div> </div>
) )
} }
interface EditorStepsTreeProps { interface EditorStepsTreeProps {
isVisible: boolean
selectedStepId: number selectedStepId: number
root: StepInfoNode root: StepInfoNode
onAddChildren: (parent: StepInfoNode) => void onAddChildren: (parent: StepInfoNode) => void
@ -917,19 +939,14 @@ interface EditorStepsTreeProps {
} }
function EditorStepsTree({ function EditorStepsTree({
isVisible, selectedStepId,
selectedStepId, root,
root, onAddChildren,
onAddChildren, onRemoveNode,
onRemoveNode, onStepSelected,
onStepSelected, }: EditorStepsTreeProps) {
}: EditorStepsTreeProps) {
return ( return (
<div <div id="steps-div">
id="steps-div"
style={{
transform: isVisible ? "translateX(0)" : "translateX(100%)",
}}>
<StepsTree <StepsTree
root={root} root={root}
selectedStepId={selectedStepId} selectedStepId={selectedStepId}
@ -952,12 +969,12 @@ interface PlayerRackProps {
} }
function PlayerRack({ function PlayerRack({
id, id,
objects, objects,
setObjects, setObjects,
courtRef, courtRef,
setComponents, setComponents,
}: PlayerRackProps) { }: PlayerRackProps) {
const courtBounds = useCallback( const courtBounds = useCallback(
() => courtRef.current!.getBoundingClientRect(), () => courtRef.current!.getBoundingClientRect(),
[courtRef], [courtRef],
@ -1011,15 +1028,15 @@ interface CourtPlayerArrowActionProps {
} }
function CourtPlayerArrowAction({ function CourtPlayerArrowAction({
playerInfo, playerInfo,
player, player,
isInvalid, isInvalid,
content, content,
setContent, setContent,
setPreviewAction, setPreviewAction,
courtRef, courtRef,
}: CourtPlayerArrowActionProps) { }: CourtPlayerArrowActionProps) {
const courtBounds = useCallback( const courtBounds = useCallback(
() => courtRef.current!.getBoundingClientRect(), () => courtRef.current!.getBoundingClientRect(),
[courtRef], [courtRef],

@ -50,28 +50,25 @@
} }
#content-div, #content-div,
#editor-div { #editor-div,
#steps-div {
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
#content-div { #content-div {
overflow: hidden;
}
.curtain {
width: 100%; width: 100%;
} }
#steps-div { #steps-div {
background-color: var(--editor-tree-background); background-color: var(--editor-tree-background);
width: 20%;
transform: translateX(100%);
transition: transform 500ms;
overflow: scroll; overflow: scroll;
} }
#steps-div::-webkit-scrollbar {
display: none;
}
#allies-rack, #allies-rack,
#opponent-rack { #opponent-rack {
width: 125px; width: 125px;
@ -154,6 +151,13 @@
font-family: monospace; font-family: monospace;
} }
.save-state,
#show-steps-button {
user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.save-state-error { .save-state-error {
color: red; color: red;
} }

@ -75,6 +75,7 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
top: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }

@ -3,7 +3,10 @@ import { fetchAPI } from "../../src/Fetcher"
import { saveSession } from "../../src/api/session" import { saveSession } from "../../src/api/session"
async function login() { async function login() {
const response = await fetchAPI("auth/token/", { email: "maxime@mail.com", password: "123456" }) const response = await fetchAPI("auth/token/", {
email: "maxime@mail.com",
password: "123456",
})
expect(response.status).toBe(200) expect(response.status).toBe(200)
const { token, expirationDate } = await response.json() const { token, expirationDate } = await response.json()
saveSession({ auth: { token, expirationDate: Date.parse(expirationDate) } }) saveSession({ auth: { token, expirationDate: Date.parse(expirationDate) } })
@ -13,28 +16,36 @@ beforeAll(login)
test("create tactic", async () => { test("create tactic", async () => {
await login() await login()
const response = await fetchAPI("tactics", { courtType: "PLAIN", name: "test tactic" }) const response = await fetchAPI("tactics", {
courtType: "PLAIN",
name: "test tactic",
})
expect(response.status).toBe(200) expect(response.status).toBe(200)
}) })
test("spam step creation test", async () => { test("spam step creation test", async () => {
const createTacticResponse = await fetchAPI("tactics", { courtType: "PLAIN", name: "test tactic" }) const createTacticResponse = await fetchAPI("tactics", {
courtType: "PLAIN",
name: "test tactic",
})
expect(createTacticResponse.status).toBe(200) expect(createTacticResponse.status).toBe(200)
const { id } = await createTacticResponse.json() const { id } = await createTacticResponse.json()
const tasks = Array.from({length: 200}) const tasks = Array.from({ length: 200 }).map(async () => {
.map(async () => { const response = await fetchAPI(`tactics/${id}/steps`, {
const response = await fetchAPI(`tactics/${id}/steps`, { parentId: 1, content: { components: [] } }) parentId: 1,
expect(response.status).toBe(200) content: { components: [] },
const { stepId } = await response.json()
return stepId
}) })
expect(response.status).toBe(200)
const { stepId } = await response.json()
return stepId
})
const steps = [] const steps = []
for (const task of tasks) { for (const task of tasks) {
steps.push(await task) steps.push(await task)
} }
steps.sort((a, b) => a - b) steps.sort((a, b) => a - b)
const expected = Array.from({length: 200}, (_, i) => i + 2) const expected = Array.from({ length: 200 }, (_, i) => i + 2)
expect(steps).toEqual(expected) expect(steps).toEqual(expected)
}) })

@ -9,7 +9,7 @@ export default defineConfig({
target: "es2021", target: "es2021",
}, },
test: { test: {
environment: "jsdom" environment: "jsdom",
}, },
plugins: [ plugins: [
react(), react(),

Loading…
Cancel
Save