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

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

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

@ -0,0 +1,76 @@
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>
)
}

@ -26,7 +26,7 @@ export function Rack<E extends { key: string | number }>({
canDetach,
onElementDetached,
render,
}: RackProps<E>) {
}: RackProps<E>) {
return (
<div
id={id}
@ -44,8 +44,7 @@ export function Rack<E extends { key: string | number }>({
const index = objects.findIndex(
(o) => o.key === element.key,
)
if (onChange)
onChange(objects.toSpliced(index, 1))
if (onChange) onChange(objects.toSpliced(index, 1))
onElementDetached(ref, element)
}}
@ -59,7 +58,7 @@ function RackItem<E extends { key: string | number }>({
item,
onTryDetach,
render,
}: RackItemProps<E>) {
}: RackItemProps<E>) {
const divRef = useRef<HTMLDivElement>(null)
return (

@ -389,6 +389,7 @@ export default function BendableArrow({
useEffect(() => {
const observer = new MutationObserver(update)
const config = { attributes: true }
if (typeof startPos == "string") {
observer.observe(document.getElementById(startPos)!, config)
}
@ -402,6 +403,14 @@ export default function BendableArrow({
return () => observer.disconnect()
}, [startPos, segments, update])
useEffect(() => {
const observer = new ResizeObserver(update)
observer.observe(area.current!, {})
return () => observer.disconnect()
})
// Adds a selection handler
// Also force an update when the window is resized
useEffect(() => {

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

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

@ -399,8 +399,6 @@ export function drainTerminalStateOnChildContent(
parentTerminalState: StepContent,
childContent: StepContent,
): StepContent | null {
let gotUpdated = false
for (const parentComponent of parentTerminalState.components) {
@ -435,11 +433,17 @@ export function drainTerminalStateOnChildContent(
if (newContentResult) {
gotUpdated = true
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
// 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
childContent = updateComponent(
{
@ -460,7 +464,7 @@ export function drainTerminalStateOnChildContent(
(comp) =>
comp.type === "phantom" ||
!comp.frozen ||
tryGetComponent(comp.id, parentTerminalState.components)
tryGetComponent(comp.id, parentTerminalState.components),
),
}

@ -92,6 +92,7 @@ import {
getStepNode,
removeStepNode,
} from "../editor/StepsDomain"
import CurtainLayout from "../components/CurtainLayout"
const ERROR_STYLE: CSSProperties = {
borderColor: "red",
@ -360,10 +361,7 @@ function UserModeEditor() {
const { name, courtType } = await infoResponse.json()
const { root } = await treeResponse.json()
if (
infoResponse.status == 401 ||
treeResponse.status == 401
) {
if (infoResponse.status == 401 || treeResponse.status == 401) {
navigation("/login")
return
}
@ -372,7 +370,6 @@ function UserModeEditor() {
`tactics/${tacticId}/steps/${root.id}`,
)
const contentResponse = await contentResponsePromise
if (contentResponse.status == 401) {
@ -519,6 +516,9 @@ function EditorPage({
[courtRef],
)
const [editorContentCurtainWidth, setEditorContentCurtainWidth] =
useState(80)
const relativePositions = useMemo(() => {
const courtBounds = courtRef.current?.getBoundingClientRect()
return courtBounds
@ -700,7 +700,15 @@ function EditorPage({
/>
)
},
[courtRef, content, relativePositions, courtBounds, renderAvailablePlayerActions, validatePlayerPosition, doRemovePlayer],
[
courtRef,
content,
relativePositions,
courtBounds,
renderAvailablePlayerActions,
validatePlayerPosition,
doRemovePlayer,
],
)
const doDeleteAction = useCallback(
@ -774,36 +782,10 @@ function EditorPage({
/>
)
}),
[courtRef, doDeleteAction, doUpdateAction],
[courtRef, doDeleteAction, doUpdateAction, editorContentCurtainWidth],
)
return (
<div id="main-div">
<div id="topbar-div">
<div id="topbar-left">
<SavingState state={saveState} />
</div>
<div id="title-input-div">
<TitleInput
style={titleStyle}
default_value={name}
onValidated={useCallback(
(new_name) => {
onNameChange(new_name).then((success) => {
setTitleStyle(success ? {} : ERROR_STYLE)
})
},
[onNameChange],
)}
/>
</div>
<div id="topbar-right">
<button onClick={() => setStepsTreeVisible((b) => !b)}>
ETAPES
</button>
</div>
</div>
<div id="editor-div">
const contentNode = (
<div id="content-div">
<div id="racks">
<PlayerRack
@ -860,18 +842,17 @@ function EditorPage({
</div>
</div>
</div>
)
const stepsTreeNode = (
<EditorStepsTree
isVisible={isStepsTreeVisible}
selectedStepId={currentStepId}
root={rootStepsNode}
onAddChildren={useCallback(
async (parent) => {
const addedNode = await onAddStep(
parent,
computeTerminalState(
content,
relativePositions,
),
computeTerminalState(content, relativePositions),
)
if (addedNode == null) {
console.error(
@ -902,13 +883,53 @@ function EditorPage({
[selectStep],
)}
/>
)
return (
<div id="main-div">
<div id="topbar-div">
<div id="topbar-left">
<SavingState state={saveState} />
</div>
<div id="title-input-div">
<TitleInput
style={titleStyle}
default_value={name}
onValidated={useCallback(
(new_name) => {
onNameChange(new_name).then((success) => {
setTitleStyle(success ? {} : ERROR_STYLE)
})
},
[onNameChange],
)}
/>
</div>
<div id="topbar-right">
<button
id={"show-steps-button"}
onClick={() => setStepsTreeVisible((b) => !b)}>
ETAPES
</button>
</div>
</div>
<div id="editor-div">
{isStepsTreeVisible ? (
<CurtainLayout
rightWidth={editorContentCurtainWidth}
onRightWidthChange={setEditorContentCurtainWidth}>
{contentNode}
{stepsTreeNode}
</CurtainLayout>
) : (
contentNode
)}
</div>
</div>
)
}
interface EditorStepsTreeProps {
isVisible: boolean
selectedStepId: number
root: StepInfoNode
onAddChildren: (parent: StepInfoNode) => void
@ -917,7 +938,6 @@ interface EditorStepsTreeProps {
}
function EditorStepsTree({
isVisible,
selectedStepId,
root,
onAddChildren,
@ -925,11 +945,7 @@ function EditorStepsTree({
onStepSelected,
}: EditorStepsTreeProps) {
return (
<div
id="steps-div"
style={{
transform: isVisible ? "translateX(0)" : "translateX(100%)",
}}>
<div id="steps-div">
<StepsTree
root={root}
selectedStepId={selectedStepId}

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

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

@ -3,7 +3,10 @@ import { fetchAPI } from "../../src/Fetcher"
import { saveSession } from "../../src/api/session"
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)
const { token, expirationDate } = await response.json()
saveSession({ auth: { token, expirationDate: Date.parse(expirationDate) } })
@ -13,18 +16,26 @@ beforeAll(login)
test("create tactic", async () => {
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)
})
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)
const { id } = await createTacticResponse.json()
const tasks = Array.from({length: 200})
.map(async () => {
const response = await fetchAPI(`tactics/${id}/steps`, { parentId: 1, content: { components: [] } })
const tasks = Array.from({ length: 200 }).map(async () => {
const response = await fetchAPI(`tactics/${id}/steps`, {
parentId: 1,
content: { components: [] },
})
expect(response.status).toBe(200)
const { stepId } = await response.json()
return stepId
@ -35,6 +46,6 @@ test("spam step creation test", async () => {
steps.push(await task)
}
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)
})

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

Loading…
Cancel
Save