Merge pull request 'ui/scene: mock create machine modal for testing' (#4404) from scene-progress into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4404
This commit is contained in:
@@ -3,6 +3,7 @@ import { Dialog as KDialog } from "@kobalte/core/dialog";
|
|||||||
import "./Modal.css";
|
import "./Modal.css";
|
||||||
import { Typography } from "../Typography/Typography";
|
import { Typography } from "../Typography/Typography";
|
||||||
import Icon from "../Icon/Icon";
|
import Icon from "../Icon/Icon";
|
||||||
|
import cx from "classnames";
|
||||||
|
|
||||||
export interface ModalContext {
|
export interface ModalContext {
|
||||||
close(): void;
|
close(): void;
|
||||||
@@ -13,6 +14,8 @@ export interface ModalProps {
|
|||||||
title: string;
|
title: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
children: (ctx: ModalContext) => JSX.Element;
|
children: (ctx: ModalContext) => JSX.Element;
|
||||||
|
mount?: Node;
|
||||||
|
class?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Modal = (props: ModalProps) => {
|
export const Modal = (props: ModalProps) => {
|
||||||
@@ -20,18 +23,28 @@ export const Modal = (props: ModalProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<KDialog id={props.id} open={open()} modal={true}>
|
<KDialog id={props.id} open={open()} modal={true}>
|
||||||
<KDialog.Portal>
|
<KDialog.Portal mount={props.mount}>
|
||||||
<KDialog.Content class="modal-content">
|
<KDialog.Content class={cx("modal-content", props.class)}>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<Typography class="title" hierarchy="label" family="mono" size="xs">
|
<Typography class="title" hierarchy="label" family="mono" size="xs">
|
||||||
{props.title}
|
{props.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<KDialog.CloseButton onClick={() => setOpen(false)}>
|
<KDialog.CloseButton
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false);
|
||||||
|
props.onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Icon icon="Close" size="0.75rem" />
|
<Icon icon="Close" size="0.75rem" />
|
||||||
</KDialog.CloseButton>
|
</KDialog.CloseButton>
|
||||||
</div>
|
</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
{props.children({ close: () => setOpen(false) })}
|
{props.children({
|
||||||
|
close: () => {
|
||||||
|
setOpen(false);
|
||||||
|
props.onClose();
|
||||||
|
},
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</KDialog.Content>
|
</KDialog.Content>
|
||||||
</KDialog.Portal>
|
</KDialog.Portal>
|
||||||
|
|||||||
@@ -2,3 +2,12 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.5s ease;
|
transition: opacity 0.5s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.create-backdrop {
|
||||||
|
@apply absolute top-0 left-0 w-full h-svh flex justify-center items-center backdrop-blur-0 z-50;
|
||||||
|
-webkit-backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-modal {
|
||||||
|
@apply min-w-96;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { RouteSectionProps } from "@solidjs/router";
|
import { RouteSectionProps } from "@solidjs/router";
|
||||||
import { Component, JSX } from "solid-js";
|
import { Component, JSX, Show, createSignal } from "solid-js";
|
||||||
import { useClanURI } from "@/src/hooks/clan";
|
import { useClanURI } from "@/src/hooks/clan";
|
||||||
import { CubeScene } from "@/src/scene/cubes";
|
import { CubeScene } from "@/src/scene/cubes";
|
||||||
import { MachinesQueryResult, useMachinesQuery } from "@/src/queries/queries";
|
import { MachinesQueryResult, useMachinesQuery } from "@/src/queries/queries";
|
||||||
@@ -10,6 +10,9 @@ import { Button } from "@/src/components/Button/Button";
|
|||||||
import { Splash } from "@/src/scene/splash";
|
import { Splash } from "@/src/scene/splash";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import "./Clan.css";
|
import "./Clan.css";
|
||||||
|
import { Modal } from "@/src/components/Modal/Modal";
|
||||||
|
import { TextInput } from "@/src/components/Form/TextInput";
|
||||||
|
import { createForm, FieldValues, reset } from "@modular-forms/solid";
|
||||||
|
|
||||||
export const Clan: Component<RouteSectionProps> = (props) => {
|
export const Clan: Component<RouteSectionProps> = (props) => {
|
||||||
return (
|
return (
|
||||||
@@ -27,17 +30,88 @@ export const Clan: Component<RouteSectionProps> = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface CreateFormValues extends FieldValues {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
interface MockProps {
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (formValues: CreateFormValues) => void;
|
||||||
|
}
|
||||||
|
const MockCreateMachine = (props: MockProps) => {
|
||||||
|
let container: Node;
|
||||||
|
|
||||||
|
const [form, { Form, Field, FieldArray }] = createForm<CreateFormValues>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={(el) => (container = el)} class="create-backdrop">
|
||||||
|
<Modal
|
||||||
|
mount={container!}
|
||||||
|
onClose={() => {
|
||||||
|
reset(form);
|
||||||
|
props.onClose();
|
||||||
|
}}
|
||||||
|
class="create-modal"
|
||||||
|
title="Create Machine"
|
||||||
|
>
|
||||||
|
{() => (
|
||||||
|
<Form class="flex flex-col" onSubmit={props.onSubmit}>
|
||||||
|
<Field name="name">
|
||||||
|
{(field, props) => (
|
||||||
|
<>
|
||||||
|
<TextInput
|
||||||
|
{...field}
|
||||||
|
label="Name"
|
||||||
|
size="s"
|
||||||
|
required={true}
|
||||||
|
input={{ ...props, placeholder: "name" }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<div class="flex w-full items-center justify-end gap-4">
|
||||||
|
<Button size="s" hierarchy="secondary" onClick={props.onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="s"
|
||||||
|
type="submit"
|
||||||
|
hierarchy="primary"
|
||||||
|
onClick={close}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const ClanSceneController = () => {
|
const ClanSceneController = () => {
|
||||||
const clanURI = useClanURI({ force: true });
|
const clanURI = useClanURI({ force: true });
|
||||||
|
|
||||||
const onCreate = async (id: string) => {
|
const [dialogHandlers, setDialogHandlers] = createSignal<{
|
||||||
|
resolve: ({ id }: { id: string }) => void;
|
||||||
|
reject: (err: unknown) => void;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const onCreate = async (): Promise<{ id: string }> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
setShowModal(true);
|
||||||
|
setDialogHandlers({ resolve, reject });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendCreate = async (values: CreateFormValues) => {
|
||||||
const api = callApi("create_machine", {
|
const api = callApi("create_machine", {
|
||||||
opts: {
|
opts: {
|
||||||
clan_dir: {
|
clan_dir: {
|
||||||
identifier: clanURI,
|
identifier: clanURI,
|
||||||
},
|
},
|
||||||
machine: {
|
machine: {
|
||||||
name: id,
|
name: values.name,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -49,14 +123,34 @@ const ClanSceneController = () => {
|
|||||||
// Important: rejects the promise
|
// Important: rejects the promise
|
||||||
throw new Error(res.errors[0].message);
|
throw new Error(res.errors[0].message);
|
||||||
}
|
}
|
||||||
return;
|
return { id: values.name };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [showModal, setShowModal] = createSignal(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SceneDataProvider clanURI={clanURI}>
|
<SceneDataProvider clanURI={clanURI}>
|
||||||
{({ query }) => {
|
{({ query }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Show when={showModal()}>
|
||||||
|
<MockCreateMachine
|
||||||
|
onClose={() => {
|
||||||
|
setShowModal(false);
|
||||||
|
dialogHandlers()?.reject(new Error("User cancelled"));
|
||||||
|
}}
|
||||||
|
onSubmit={async (values) => {
|
||||||
|
try {
|
||||||
|
const result = await sendCreate(values);
|
||||||
|
dialogHandlers()?.resolve(result);
|
||||||
|
setShowModal(false);
|
||||||
|
} catch (err) {
|
||||||
|
dialogHandlers()?.reject(err);
|
||||||
|
setShowModal(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
<div
|
<div
|
||||||
class="flex flex-row"
|
class="flex flex-row"
|
||||||
style={{ position: "absolute", top: "10px", left: "10px" }}
|
style={{ position: "absolute", top: "10px", left: "10px" }}
|
||||||
@@ -68,6 +162,7 @@ const ClanSceneController = () => {
|
|||||||
produce((s) => {
|
produce((s) => {
|
||||||
for (const machineId in s.sceneData[clanURI]) {
|
for (const machineId in s.sceneData[clanURI]) {
|
||||||
// Reset the position of each machine to [0, 0]
|
// Reset the position of each machine to [0, 0]
|
||||||
|
s.sceneData[clanURI] = {}; // Clear the entire object
|
||||||
// delete s.sceneData[clanURI][machineId];
|
// delete s.sceneData[clanURI][machineId];
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -90,6 +185,7 @@ const ClanSceneController = () => {
|
|||||||
<div class={cx({ "fade-out": !query.isLoading })}>
|
<div class={cx({ "fade-out": !query.isLoading })}>
|
||||||
<Splash />
|
<Splash />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CubeScene
|
<CubeScene
|
||||||
isLoading={query.isLoading}
|
isLoading={query.isLoading}
|
||||||
cubesQuery={query}
|
cubesQuery={query}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ function keyFromPos(pos: [number, number]): string {
|
|||||||
|
|
||||||
export function CubeScene(props: {
|
export function CubeScene(props: {
|
||||||
cubesQuery: MachinesQueryResult;
|
cubesQuery: MachinesQueryResult;
|
||||||
onCreate?: (id: string) => Promise<void>;
|
onCreate: () => Promise<{ id: string }>;
|
||||||
sceneStore: Accessor<SceneData>;
|
sceneStore: Accessor<SceneData>;
|
||||||
setMachinePos: (machineId: string, pos: [number, number]) => void;
|
setMachinePos: (machineId: string, pos: [number, number]) => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
@@ -251,7 +251,10 @@ export function CubeScene(props: {
|
|||||||
// Reactive cubes memo - this recalculates whenever data changes
|
// Reactive cubes memo - this recalculates whenever data changes
|
||||||
const cubes = createMemo(() => {
|
const cubes = createMemo(() => {
|
||||||
console.log("Calculating cubes...");
|
console.log("Calculating cubes...");
|
||||||
const currentIds = Object.keys(unwrap(props.sceneStore()));
|
const sceneData = props.sceneStore(); // keep it reactive
|
||||||
|
if (!sceneData) return [];
|
||||||
|
|
||||||
|
const currentIds = Object.keys(sceneData);
|
||||||
console.log("Current IDs:", currentIds);
|
console.log("Current IDs:", currentIds);
|
||||||
|
|
||||||
let cameraTarget = [0, 0, 0] as [number, number, number];
|
let cameraTarget = [0, 0, 0] as [number, number, number];
|
||||||
@@ -302,6 +305,7 @@ export function CubeScene(props: {
|
|||||||
|
|
||||||
if (progress < 1) {
|
if (progress < 1) {
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
|
requestRenderIfNotRequested();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -599,6 +603,7 @@ export function CubeScene(props: {
|
|||||||
CREATE_BASE_COLOR,
|
CREATE_BASE_COLOR,
|
||||||
CREATE_BASE_EMISSIVE,
|
CREATE_BASE_EMISSIVE,
|
||||||
);
|
);
|
||||||
|
initBase.visible = false;
|
||||||
|
|
||||||
scene.add(initBase);
|
scene.add(initBase);
|
||||||
|
|
||||||
@@ -631,14 +636,25 @@ export function CubeScene(props: {
|
|||||||
// - Creates a new cube in "create" mode
|
// - Creates a new cube in "create" mode
|
||||||
const onClick = (event: MouseEvent) => {
|
const onClick = (event: MouseEvent) => {
|
||||||
if (worldMode() === "create") {
|
if (worldMode() === "create") {
|
||||||
setWorldMode("view");
|
props
|
||||||
|
.onCreate()
|
||||||
|
.then(({ id }) => {
|
||||||
|
//Successfully created machine
|
||||||
|
const pos = cursorPosition();
|
||||||
|
if (!pos) {
|
||||||
|
console.warn("No position set for new cube");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
props.setMachinePos(id, pos);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error creating cube:", error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (initBase) initBase.visible = false;
|
||||||
|
|
||||||
// res.result.then(() => {
|
setWorldMode("view");
|
||||||
// props.cubesQuery.refetch();
|
});
|
||||||
|
|
||||||
// positionMap.set("sara", pos);
|
|
||||||
// addCube("sara");
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const rect = renderer.domElement.getBoundingClientRect();
|
const rect = renderer.domElement.getBoundingClientRect();
|
||||||
@@ -828,6 +844,8 @@ export function CubeScene(props: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
requestRenderIfNotRequested();
|
||||||
});
|
});
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
@@ -856,8 +874,10 @@ export function CubeScene(props: {
|
|||||||
const pos = nextGridPos();
|
const pos = nextGridPos();
|
||||||
if (!initBase) return;
|
if (!initBase) return;
|
||||||
|
|
||||||
initBase.position.set(pos[0], BASE_HEIGHT / 2, pos[1]);
|
if (initBase.visible === false && inside) {
|
||||||
|
initBase.position.set(pos[0], BASE_HEIGHT / 2, pos[1]);
|
||||||
|
initBase.visible = true;
|
||||||
|
}
|
||||||
requestRenderIfNotRequested();
|
requestRenderIfNotRequested();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -869,6 +889,8 @@ export function CubeScene(props: {
|
|||||||
if (worldMode() !== "create") return;
|
if (worldMode() !== "create") return;
|
||||||
if (!initBase) return;
|
if (!initBase) return;
|
||||||
|
|
||||||
|
initBase.visible = true;
|
||||||
|
|
||||||
const rect = renderer.domElement.getBoundingClientRect();
|
const rect = renderer.domElement.getBoundingClientRect();
|
||||||
const mouse = new THREE.Vector2(
|
const mouse = new THREE.Vector2(
|
||||||
((event.clientX - rect.left) / rect.width) * 2 - 1,
|
((event.clientX - rect.left) / rect.width) * 2 - 1,
|
||||||
@@ -923,8 +945,10 @@ export function CubeScene(props: {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (positionMode() === "grid") {
|
if (positionMode() === "grid") {
|
||||||
setPositionMode("circle");
|
setPositionMode("circle");
|
||||||
|
grid.visible = false;
|
||||||
} else {
|
} else {
|
||||||
setPositionMode("grid");
|
setPositionMode("grid");
|
||||||
|
grid.visible = true;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user