Merge pull request 'feat(ui): introduce a top-level Clan context' (#4860) from feat/handle-clan-switch-errors into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4860
This commit is contained in:
@@ -5,15 +5,20 @@ import { Typography } from "@/src/components/Typography/Typography";
|
|||||||
import { Button } from "@/src/components/Button/Button";
|
import { Button } from "@/src/components/Button/Button";
|
||||||
import { navigateToClan, navigateToOnboarding } from "@/src/hooks/clan";
|
import { navigateToClan, navigateToOnboarding } from "@/src/hooks/clan";
|
||||||
import { useNavigate } from "@solidjs/router";
|
import { useNavigate } from "@solidjs/router";
|
||||||
import { For } from "solid-js";
|
import { For, Show } from "solid-js";
|
||||||
import { activeClanURI, clanURIs, setActiveClanURI } from "@/src/stores/clan";
|
import { activeClanURI, clanURIs, setActiveClanURI } from "@/src/stores/clan";
|
||||||
import { useClanListQuery } from "@/src/hooks/queries";
|
import { useClanListQuery } from "@/src/hooks/queries";
|
||||||
|
import { Alert } from "@/src/components/Alert/Alert";
|
||||||
|
|
||||||
export interface ClansModalProps {
|
export interface ListClansModalProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
error?: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ListClansModal = (props: ClansModalProps) => {
|
export const ListClansModal = (props: ListClansModalProps) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const query = useClanListQuery(clanURIs());
|
const query = useClanListQuery(clanURIs());
|
||||||
@@ -38,6 +43,14 @@ export const ListClansModal = (props: ClansModalProps) => {
|
|||||||
class={cx(styles.modal)}
|
class={cx(styles.modal)}
|
||||||
>
|
>
|
||||||
<div class={cx(styles.content)}>
|
<div class={cx(styles.content)}>
|
||||||
|
<Show when={props.error}>
|
||||||
|
<Alert
|
||||||
|
type="error"
|
||||||
|
title={props.error?.title || ""}
|
||||||
|
description={props.error?.description}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<div class={cx(styles.header)}>
|
<div class={cx(styles.header)}>
|
||||||
<Typography
|
<Typography
|
||||||
hierarchy="label"
|
hierarchy="label"
|
||||||
@@ -3,11 +3,12 @@ import { A } from "@solidjs/router";
|
|||||||
import { Accordion } from "@kobalte/core/accordion";
|
import { Accordion } from "@kobalte/core/accordion";
|
||||||
import Icon from "../Icon/Icon";
|
import Icon from "../Icon/Icon";
|
||||||
import { Typography } from "@/src/components/Typography/Typography";
|
import { Typography } from "@/src/components/Typography/Typography";
|
||||||
import { For } from "solid-js";
|
import { For, useContext } from "solid-js";
|
||||||
import { MachineStatus } from "@/src/components/MachineStatus/MachineStatus";
|
import { MachineStatus } from "@/src/components/MachineStatus/MachineStatus";
|
||||||
import { buildMachinePath, useClanURI } from "@/src/hooks/clan";
|
import { buildMachinePath, useClanURI } from "@/src/hooks/clan";
|
||||||
import { useMachinesQuery, useMachineStateQuery } from "@/src/hooks/queries";
|
import { useMachineStateQuery } from "@/src/hooks/queries";
|
||||||
import { SidebarProps } from "./Sidebar";
|
import { SidebarProps } from "./Sidebar";
|
||||||
|
import { ClanContext } from "@/src/routes/Clan/Clan";
|
||||||
|
|
||||||
interface MachineProps {
|
interface MachineProps {
|
||||||
clanURI: string;
|
clanURI: string;
|
||||||
@@ -56,7 +57,11 @@ const MachineRoute = (props: MachineProps) => {
|
|||||||
|
|
||||||
export const SidebarBody = (props: SidebarProps) => {
|
export const SidebarBody = (props: SidebarProps) => {
|
||||||
const clanURI = useClanURI();
|
const clanURI = useClanURI();
|
||||||
const machineList = useMachinesQuery(clanURI);
|
|
||||||
|
const ctx = useContext(ClanContext);
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error("ClanContext not found");
|
||||||
|
}
|
||||||
|
|
||||||
const sectionLabels = (props.staticSections || []).map(
|
const sectionLabels = (props.staticSections || []).map(
|
||||||
(section) => section.title,
|
(section) => section.title,
|
||||||
@@ -96,7 +101,7 @@ export const SidebarBody = (props: SidebarProps) => {
|
|||||||
</Accordion.Header>
|
</Accordion.Header>
|
||||||
<Accordion.Content class="content">
|
<Accordion.Content class="content">
|
||||||
<nav>
|
<nav>
|
||||||
<For each={Object.entries(machineList.data || {})}>
|
<For each={Object.entries(ctx.machinesQuery.data || {})}>
|
||||||
{([id, machine]) => (
|
{([id, machine]) => (
|
||||||
<MachineRoute
|
<MachineRoute
|
||||||
clanURI={clanURI}
|
clanURI={clanURI}
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import Icon from "@/src/components/Icon/Icon";
|
|||||||
import { DropdownMenu } from "@kobalte/core/dropdown-menu";
|
import { DropdownMenu } from "@kobalte/core/dropdown-menu";
|
||||||
import { useNavigate } from "@solidjs/router";
|
import { useNavigate } from "@solidjs/router";
|
||||||
import { Typography } from "../Typography/Typography";
|
import { Typography } from "../Typography/Typography";
|
||||||
import { createSignal, For, Suspense } from "solid-js";
|
import { createSignal, For, Suspense, useContext } from "solid-js";
|
||||||
import { useClanListQuery } from "@/src/hooks/queries";
|
|
||||||
import {
|
import {
|
||||||
navigateToClan,
|
navigateToClan,
|
||||||
navigateToOnboarding,
|
navigateToOnboarding,
|
||||||
useClanURI,
|
useClanURI,
|
||||||
} from "@/src/hooks/clan";
|
} from "@/src/hooks/clan";
|
||||||
import { clanURIs, setActiveClanURI } from "@/src/stores/clan";
|
import { setActiveClanURI } from "@/src/stores/clan";
|
||||||
import { Button } from "../Button/Button";
|
import { Button } from "../Button/Button";
|
||||||
|
import { ClanContext } from "@/src/routes/Clan/Clan";
|
||||||
|
|
||||||
export const SidebarHeader = () => {
|
export const SidebarHeader = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -19,10 +19,19 @@ export const SidebarHeader = () => {
|
|||||||
const [open, setOpen] = createSignal(false);
|
const [open, setOpen] = createSignal(false);
|
||||||
|
|
||||||
// get information about the current active clan
|
// get information about the current active clan
|
||||||
const clanURI = useClanURI();
|
const ctx = useContext(ClanContext);
|
||||||
const allClans = useClanListQuery(clanURIs());
|
|
||||||
|
|
||||||
const activeClan = () => allClans.find(({ data }) => data?.uri === clanURI);
|
if (!ctx) {
|
||||||
|
throw new Error("SidebarContext not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const clanURI = useClanURI();
|
||||||
|
|
||||||
|
const clanChar = () =>
|
||||||
|
ctx?.activeClanQuery?.data?.name.charAt(0).toUpperCase();
|
||||||
|
const clanName = () => ctx?.activeClanQuery?.data?.name;
|
||||||
|
|
||||||
|
const clans = () => ctx.otherClanQueries.filter((clan) => !clan.isError);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
@@ -37,7 +46,7 @@ export const SidebarHeader = () => {
|
|||||||
weight="bold"
|
weight="bold"
|
||||||
inverted={true}
|
inverted={true}
|
||||||
>
|
>
|
||||||
{activeClan()?.data?.name.charAt(0).toUpperCase()}
|
{clanChar()}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<Typography
|
<Typography
|
||||||
@@ -46,7 +55,7 @@ export const SidebarHeader = () => {
|
|||||||
weight="bold"
|
weight="bold"
|
||||||
inverted={!open()}
|
inverted={!open()}
|
||||||
>
|
>
|
||||||
{activeClan()?.data?.name}
|
{clanName()}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenu.Icon>
|
<DropdownMenu.Icon>
|
||||||
@@ -91,7 +100,7 @@ export const SidebarHeader = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenu.GroupLabel>
|
</DropdownMenu.GroupLabel>
|
||||||
<div class="dropdown-group-items">
|
<div class="dropdown-group-items">
|
||||||
<For each={allClans}>
|
<For each={clans()}>
|
||||||
{(clan) => (
|
{(clan) => (
|
||||||
<Suspense fallback={"Loading..."}>
|
<Suspense fallback={"Loading..."}>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ export const useMachineDetailsQuery = (
|
|||||||
|
|
||||||
export const useClanDetailsQuery = (clanURI: string) => {
|
export const useClanDetailsQuery = (clanURI: string) => {
|
||||||
const client = useApiClient();
|
const client = useApiClient();
|
||||||
return useQuery<ClanDetails>(() => ({
|
return useQuery<ClanDetailsWithURI>(() => ({
|
||||||
queryKey: ["clans", encodeBase64(clanURI), "details"],
|
queryKey: ["clans", encodeBase64(clanURI), "details"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const call = client.fetch("get_clan_details", {
|
const call = client.fetch("get_clan_details", {
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { RouteSectionProps } from "@solidjs/router";
|
import { RouteSectionProps, useNavigate } from "@solidjs/router";
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
JSX,
|
createContext,
|
||||||
Show,
|
|
||||||
createEffect,
|
createEffect,
|
||||||
createMemo,
|
createMemo,
|
||||||
createSignal,
|
createSignal,
|
||||||
on,
|
on,
|
||||||
onMount,
|
onMount,
|
||||||
|
Show,
|
||||||
|
useContext,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import {
|
import {
|
||||||
buildMachinePath,
|
buildMachinePath,
|
||||||
@@ -16,13 +17,14 @@ import {
|
|||||||
} from "@/src/hooks/clan";
|
} from "@/src/hooks/clan";
|
||||||
import { CubeScene } from "@/src/scene/cubes";
|
import { CubeScene } from "@/src/scene/cubes";
|
||||||
import {
|
import {
|
||||||
ClanListQueryResult,
|
ClanDetailsWithURI,
|
||||||
MachinesQueryResult,
|
MachinesQueryResult,
|
||||||
|
useClanDetailsQuery,
|
||||||
useClanListQuery,
|
useClanListQuery,
|
||||||
useMachinesQuery,
|
useMachinesQuery,
|
||||||
} from "@/src/hooks/queries";
|
} from "@/src/hooks/queries";
|
||||||
import { callApi } from "@/src/hooks/api";
|
import { callApi } from "@/src/hooks/api";
|
||||||
import { store, setStore, clanURIs } from "@/src/stores/clan";
|
import { clanURIs, setStore, store } from "@/src/stores/clan";
|
||||||
import { produce } from "solid-js/store";
|
import { produce } from "solid-js/store";
|
||||||
import { Button } from "@/src/components/Button/Button";
|
import { Button } from "@/src/components/Button/Button";
|
||||||
import { Splash } from "@/src/scene/splash";
|
import { Splash } from "@/src/scene/splash";
|
||||||
@@ -32,15 +34,36 @@ import { Modal } from "@/src/components/Modal/Modal";
|
|||||||
import { TextInput } from "@/src/components/Form/TextInput";
|
import { TextInput } from "@/src/components/Form/TextInput";
|
||||||
import { createForm, FieldValues, reset } from "@modular-forms/solid";
|
import { createForm, FieldValues, reset } from "@modular-forms/solid";
|
||||||
import { Sidebar } from "@/src/components/Sidebar/Sidebar";
|
import { Sidebar } from "@/src/components/Sidebar/Sidebar";
|
||||||
import { useNavigate } from "@solidjs/router";
|
import { UseQueryResult } from "@tanstack/solid-query";
|
||||||
|
|
||||||
|
export const ClanContext = createContext<{
|
||||||
|
machinesQuery: MachinesQueryResult;
|
||||||
|
activeClanQuery: UseQueryResult<ClanDetailsWithURI>;
|
||||||
|
otherClanQueries: UseQueryResult<ClanDetailsWithURI>[];
|
||||||
|
}>();
|
||||||
|
|
||||||
export const Clan: Component<RouteSectionProps> = (props) => {
|
export const Clan: Component<RouteSectionProps> = (props) => {
|
||||||
|
const clanURI = useClanURI();
|
||||||
|
const activeClanQuery = useClanDetailsQuery(clanURI);
|
||||||
|
|
||||||
|
const otherClanQueries = useClanListQuery(
|
||||||
|
clanURIs().filter((uri) => uri !== clanURI),
|
||||||
|
);
|
||||||
|
|
||||||
|
const machinesQuery = useMachinesQuery(clanURI);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ClanContext.Provider
|
||||||
|
value={{
|
||||||
|
machinesQuery,
|
||||||
|
activeClanQuery,
|
||||||
|
otherClanQueries,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Sidebar class={cx(styles.sidebar)} />
|
<Sidebar class={cx(styles.sidebar)} />
|
||||||
{props.children}
|
{props.children}
|
||||||
<ClanSceneController {...props} />
|
<ClanSceneController {...props} />
|
||||||
</>
|
</ClanContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,7 +121,10 @@ const ClanSceneController = (props: RouteSectionProps) => {
|
|||||||
const clanURI = useClanURI();
|
const clanURI = useClanURI();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const machinesQuery = useMachinesQuery(clanURI);
|
const ctx = useContext(ClanContext);
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error("ClanContext not found");
|
||||||
|
}
|
||||||
|
|
||||||
const [dialogHandlers, setDialogHandlers] = createSignal<{
|
const [dialogHandlers, setDialogHandlers] = createSignal<{
|
||||||
resolve: ({ id }: { id: string }) => void;
|
resolve: ({ id }: { id: string }) => void;
|
||||||
@@ -133,7 +159,7 @@ const ClanSceneController = (props: RouteSectionProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// trigger a refetch of the machines query
|
// trigger a refetch of the machines query
|
||||||
machinesQuery.refetch();
|
ctx.machinesQuery.refetch();
|
||||||
|
|
||||||
return { id: values.name };
|
return { id: values.name };
|
||||||
};
|
};
|
||||||
@@ -177,102 +203,87 @@ const ClanSceneController = (props: RouteSectionProps) => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// a combination of the individual clan details query status and the machines query status
|
||||||
|
// the cube scene needs the machines query, the sidebar needs the clans query and machines query results
|
||||||
|
// so we wait on both before removing the loader to avoid any loading artefacts
|
||||||
|
const isLoading = (): boolean => {
|
||||||
|
// check if the active clan query is still loading
|
||||||
|
if (ctx.activeClanQuery.isLoading) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check the machines query first
|
||||||
|
if (ctx.machinesQuery.isLoading) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise iterate the clans query and return early if we find a queries that is still loading
|
||||||
|
for (const query of ctx.otherClanQueries) {
|
||||||
|
if (query.isLoading) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SceneDataProvider clanURI={clanURI}>
|
<>
|
||||||
{({ clansQuery, machinesQuery }) => {
|
<Show when={showModal()}>
|
||||||
// a combination of the individual clan details query status and the machines query status
|
<MockCreateMachine
|
||||||
// the cube scene needs the machines query, the sidebar needs the clans query and machines query results
|
onClose={() => {
|
||||||
// so we wait on both before removing the loader to avoid any loading artefacts
|
setShowModal(false);
|
||||||
const isLoading = (): boolean => {
|
dialogHandlers()?.reject(new Error("User cancelled"));
|
||||||
// check the machines query first
|
}}
|
||||||
if (machinesQuery.isLoading) {
|
onSubmit={async (values) => {
|
||||||
return true;
|
try {
|
||||||
}
|
const result = await sendCreate(values);
|
||||||
|
dialogHandlers()?.resolve(result);
|
||||||
// otherwise iterate the clans query and return early if we find a queries that is still loading
|
setShowModal(false);
|
||||||
for (const query of clansQuery) {
|
} catch (err) {
|
||||||
if (query.isLoading) {
|
dialogHandlers()?.reject(err);
|
||||||
return true;
|
setShowModal(false);
|
||||||
}
|
}
|
||||||
}
|
}}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
<div
|
||||||
|
class={cx({
|
||||||
|
[styles.fadeOut]: !ctx.machinesQuery.isLoading && loadingCooldown(),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Splash />
|
||||||
|
</div>
|
||||||
|
|
||||||
return false;
|
<CubeScene
|
||||||
};
|
selectedIds={selectedIds}
|
||||||
|
onSelect={onMachineSelect}
|
||||||
return (
|
isLoading={isLoading()}
|
||||||
<>
|
cubesQuery={ctx.machinesQuery}
|
||||||
<Show when={showModal()}>
|
onCreate={onCreate}
|
||||||
<MockCreateMachine
|
sceneStore={() => {
|
||||||
onClose={() => {
|
const clanURI = useClanURI();
|
||||||
setShowModal(false);
|
return store.sceneData?.[clanURI];
|
||||||
dialogHandlers()?.reject(new Error("User cancelled"));
|
}}
|
||||||
}}
|
setMachinePos={(machineId: string, pos: [number, number]) => {
|
||||||
onSubmit={async (values) => {
|
console.log("calling setStore", machineId, pos);
|
||||||
try {
|
setStore(
|
||||||
const result = await sendCreate(values);
|
produce((s) => {
|
||||||
dialogHandlers()?.resolve(result);
|
if (!s.sceneData) {
|
||||||
setShowModal(false);
|
s.sceneData = {};
|
||||||
} catch (err) {
|
}
|
||||||
dialogHandlers()?.reject(err);
|
if (!s.sceneData[clanURI]) {
|
||||||
setShowModal(false);
|
s.sceneData[clanURI] = {};
|
||||||
}
|
}
|
||||||
}}
|
if (!s.sceneData[clanURI][machineId]) {
|
||||||
/>
|
s.sceneData[clanURI][machineId] = { position: pos };
|
||||||
</Show>
|
} else {
|
||||||
<div
|
s.sceneData[clanURI][machineId].position = pos;
|
||||||
class={cx({
|
}
|
||||||
[styles.fadeOut]: !machinesQuery.isLoading && loadingCooldown(),
|
}),
|
||||||
})}
|
);
|
||||||
>
|
}}
|
||||||
<Splash />
|
/>
|
||||||
</div>
|
</>
|
||||||
|
|
||||||
<CubeScene
|
|
||||||
selectedIds={selectedIds}
|
|
||||||
onSelect={onMachineSelect}
|
|
||||||
isLoading={isLoading()}
|
|
||||||
cubesQuery={machinesQuery}
|
|
||||||
onCreate={onCreate}
|
|
||||||
sceneStore={() => {
|
|
||||||
const clanURI = useClanURI();
|
|
||||||
return store.sceneData?.[clanURI];
|
|
||||||
}}
|
|
||||||
setMachinePos={(machineId: string, pos: [number, number]) => {
|
|
||||||
console.log("calling setStore", machineId, pos);
|
|
||||||
setStore(
|
|
||||||
produce((s) => {
|
|
||||||
if (!s.sceneData) {
|
|
||||||
s.sceneData = {};
|
|
||||||
}
|
|
||||||
if (!s.sceneData[clanURI]) {
|
|
||||||
s.sceneData[clanURI] = {};
|
|
||||||
}
|
|
||||||
if (!s.sceneData[clanURI][machineId]) {
|
|
||||||
s.sceneData[clanURI][machineId] = { position: pos };
|
|
||||||
} else {
|
|
||||||
s.sceneData[clanURI][machineId].position = pos;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</SceneDataProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const SceneDataProvider = (props: {
|
|
||||||
clanURI: string;
|
|
||||||
children: (sceneData: {
|
|
||||||
clansQuery: ClanListQueryResult;
|
|
||||||
machinesQuery: MachinesQueryResult;
|
|
||||||
}) => JSX.Element;
|
|
||||||
}) => {
|
|
||||||
const clansQuery = useClanListQuery(clanURIs());
|
|
||||||
const machinesQuery = useMachinesQuery(props.clanURI);
|
|
||||||
|
|
||||||
// This component can be used to provide scene data or context if needed
|
|
||||||
return props.children({ clansQuery, machinesQuery });
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import { HostFileInput } from "@/src/components/Form/HostFileInput";
|
|||||||
import { callApi } from "@/src/hooks/api";
|
import { callApi } from "@/src/hooks/api";
|
||||||
import { Creating } from "./Creating";
|
import { Creating } from "./Creating";
|
||||||
import { useApiClient } from "@/src/hooks/ApiClient";
|
import { useApiClient } from "@/src/hooks/ApiClient";
|
||||||
import { ListClansModal } from "@/src/routes/Onboarding/ListClansModal";
|
import { ListClansModal } from "@/src/components/ListClansModal/ListClansModal";
|
||||||
|
|
||||||
type State = "welcome" | "setup" | "creating";
|
type State = "welcome" | "setup" | "creating";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user