Merge pull request 'Handle error cases when switching clan' (#4879) from feat/handle-clan-switch-errors into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4879
This commit is contained in:
brianmcgee
2025-08-22 10:47:35 +00:00
10 changed files with 439 additions and 120 deletions

View File

@@ -15,9 +15,10 @@
"@modular-forms/solid": "^0.25.1",
"@solid-primitives/storage": "^4.3.2",
"@solidjs/router": "^0.15.3",
"@tanstack/eslint-plugin-query": "^5.51.12",
"@tanstack/solid-query": "^5.76.0",
"@tanstack/solid-query-devtools": "^5.83.0",
"@tanstack/eslint-plugin-query": "^5.83.1",
"@tanstack/solid-query": "^5.85.5",
"@tanstack/solid-query-devtools": "^5.85.5",
"@tanstack/solid-query-persist-client": "^5.85.5",
"solid-js": "^1.9.7",
"solid-toast": "^0.5.0",
"three": "^0.176.0",
@@ -2487,12 +2488,12 @@
}
},
"node_modules/@tanstack/eslint-plugin-query": {
"version": "5.81.2",
"resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.81.2.tgz",
"integrity": "sha512-h4k6P6fm5VhKP5NkK+0TTVpGGyKQdx6tk7NYYG7J7PkSu7ClpLgBihw7yzK8N3n5zPaF3IMyErxfoNiXWH/3/A==",
"version": "5.83.1",
"resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.83.1.tgz",
"integrity": "sha512-tdkpPFfzkTksN9BIlT/qjixSAtKrsW6PUVRwdKWaOcag7DrD1vpki3UzzdfMQGDRGeg1Ue1Dg+rcl5FJGembNg==",
"license": "MIT",
"dependencies": {
"@typescript-eslint/utils": "^8.18.1"
"@typescript-eslint/utils": "^8.37.0"
},
"funding": {
"type": "github",
@@ -2502,10 +2503,169 @@
"eslint": "^8.57.0 || ^9.0.0"
}
},
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/project-service": {
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz",
"integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==",
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.40.0",
"@typescript-eslint/types": "^8.40.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/scope-manager": {
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz",
"integrity": "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==",
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.40.0",
"@typescript-eslint/visitor-keys": "8.40.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz",
"integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==",
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/types": {
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz",
"integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==",
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz",
"integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==",
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.40.0",
"@typescript-eslint/tsconfig-utils": "8.40.0",
"@typescript-eslint/types": "8.40.0",
"@typescript-eslint/visitor-keys": "8.40.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/utils": {
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.40.0.tgz",
"integrity": "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==",
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.40.0",
"@typescript-eslint/types": "8.40.0",
"@typescript-eslint/typescript-estree": "8.40.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz",
"integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==",
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.40.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@tanstack/eslint-plugin-query/node_modules/eslint-visitor-keys": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@tanstack/eslint-plugin-query/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@tanstack/query-core": {
"version": "5.83.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz",
"integrity": "sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==",
"version": "5.85.5",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.85.5.tgz",
"integrity": "sha512-KO0WTob4JEApv69iYp1eGvfMSUkgw//IpMnq+//cORBzXf0smyRwPLrUvEe5qtAEGjwZTXrjxg+oJNP/C00t6w==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2513,22 +2673,35 @@
}
},
"node_modules/@tanstack/query-devtools": {
"version": "5.81.2",
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.81.2.tgz",
"integrity": "sha512-jCeJcDCwKfoyyBXjXe9+Lo8aTkavygHHsUHAlxQKKaDeyT0qyQNLKl7+UyqYH2dDF6UN/14873IPBHchcsU+Zg==",
"version": "5.84.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.84.0.tgz",
"integrity": "sha512-fbF3n+z1rqhvd9EoGp5knHkv3p5B2Zml1yNRjh7sNXklngYI5RVIWUrUjZ1RIcEoscarUb0+bOvIs5x9dwzOXQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/solid-query": {
"version": "5.83.0",
"resolved": "https://registry.npmjs.org/@tanstack/solid-query/-/solid-query-5.83.0.tgz",
"integrity": "sha512-RF8Tv9+6+Kmzj+EafbTzvzzPq+J5SzHtc1Tz3D2MZ/EvlZTH+GL5q4HNnWK3emg7CB6WzyGnTuERmmWJaZs8/w==",
"node_modules/@tanstack/query-persist-client-core": {
"version": "5.85.5",
"resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.85.5.tgz",
"integrity": "sha512-2JQiyiTVaaUu8pwPqOp6tjNa64ZN+0T9eZ3lfksV4le1VuG99fTcAYmZFIydvzwWlSM7GEF/1kpl5bwW2Y1qfQ==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.83.0"
"@tanstack/query-core": "5.85.5"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/solid-query": {
"version": "5.85.5",
"resolved": "https://registry.npmjs.org/@tanstack/solid-query/-/solid-query-5.85.5.tgz",
"integrity": "sha512-0o0Ibk9wqydm4JatbIjmvDu1+MofeZ1bU9BKwAbpt7HYjrLVCeddpW6zGmp41nN7t/mHJyR+ctW9oiNumCkEfg==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.85.5"
},
"funding": {
"type": "github",
@@ -2539,19 +2712,36 @@
}
},
"node_modules/@tanstack/solid-query-devtools": {
"version": "5.83.0",
"resolved": "https://registry.npmjs.org/@tanstack/solid-query-devtools/-/solid-query-devtools-5.83.0.tgz",
"integrity": "sha512-Z0wQlAWXz/U2bJ/paMRBTDhMoPnB9Te6GmA21sXnI+nDnAAPZRcPxFBiCgYJS3eFsvbkdRGJwoUSQrdIgy0shg==",
"version": "5.85.5",
"resolved": "https://registry.npmjs.org/@tanstack/solid-query-devtools/-/solid-query-devtools-5.85.5.tgz",
"integrity": "sha512-9rC22wILlV9Lcsi4xKPmzRkNio1NOxNT36diIS+HjpOmhsEP/aI8XkNKQa/KPhhaSN2naYaTCJamh7eBAQ0Ymg==",
"license": "MIT",
"dependencies": {
"@tanstack/query-devtools": "5.81.2"
"@tanstack/query-devtools": "5.84.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"@tanstack/solid-query": "^5.83.0",
"@tanstack/solid-query": "^5.85.5",
"solid-js": "^1.6.0"
}
},
"node_modules/@tanstack/solid-query-persist-client": {
"version": "5.85.5",
"resolved": "https://registry.npmjs.org/@tanstack/solid-query-persist-client/-/solid-query-persist-client-5.85.5.tgz",
"integrity": "sha512-2aG7UnLZlfE3R4XKqYuIeXVKjJOghjsjq4EU2Ifp915FTBZcZo61sEw1zRqRlrDjEFYAs4kJUZwqViDSJYyX2g==",
"license": "MIT",
"dependencies": {
"@tanstack/query-persist-client-core": "5.85.5"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"@tanstack/solid-query": "^5.85.5",
"solid-js": "^1.6.0"
}
},
@@ -2894,6 +3084,7 @@
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.36.0.tgz",
"integrity": "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.36.0",
@@ -2915,6 +3106,7 @@
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz",
"integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.36.0",
@@ -2932,6 +3124,7 @@
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz",
"integrity": "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2972,6 +3165,7 @@
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz",
"integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2985,6 +3179,7 @@
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz",
"integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.36.0",
@@ -3013,6 +3208,7 @@
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -3025,6 +3221,7 @@
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.36.0.tgz",
"integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
@@ -3048,6 +3245,7 @@
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz",
"integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.36.0",
@@ -3065,6 +3263,7 @@
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"

View File

@@ -72,9 +72,10 @@
"@modular-forms/solid": "^0.25.1",
"@solid-primitives/storage": "^4.3.2",
"@solidjs/router": "^0.15.3",
"@tanstack/eslint-plugin-query": "^5.51.12",
"@tanstack/solid-query": "^5.76.0",
"@tanstack/solid-query-devtools": "^5.83.0",
"@tanstack/eslint-plugin-query": "^5.83.1",
"@tanstack/solid-query": "^5.85.5",
"@tanstack/solid-query-devtools": "^5.85.5",
"@tanstack/solid-query-persist-client": "^5.85.5",
"solid-js": "^1.9.7",
"solid-toast": "^0.5.0",
"three": "^0.176.0",

View File

@@ -11,7 +11,7 @@ import { useClanListQuery } from "@/src/hooks/queries";
import { Alert } from "@/src/components/Alert/Alert";
export interface ListClansModalProps {
onClose: () => void;
onClose?: () => void;
error?: {
title: string;
description: string;
@@ -68,7 +68,7 @@ export const ListClansModal = (props: ListClansModalProps) => {
size="s"
startIcon="Plus"
onClick={() => {
props.onClose();
props.onClose?.();
navigateToOnboarding(navigate, true);
}}
>

View File

@@ -30,7 +30,7 @@ export const useModalContext = () => {
export interface ModalProps {
id?: string;
title: string;
onClose: () => void;
onClose?: () => void;
children: JSX.Element;
mount?: Node;
class?: string;
@@ -57,13 +57,11 @@ export const Modal = (props: ModalProps) => {
>
{props.title}
</Typography>
<KDialog.CloseButton
onClick={() => {
props.onClose();
}}
>
<Icon icon="Close" size="0.75rem" />
</KDialog.CloseButton>
<Show when={props.onClose}>
<KDialog.CloseButton onClick={props.onClose}>
<Icon icon="Close" size="0.75rem" />
</KDialog.CloseButton>
</Show>
</div>
<Show when={props.metaHeader}>
{(metaHeader) => (

View File

@@ -31,7 +31,11 @@ export const SidebarHeader = () => {
ctx?.activeClanQuery?.data?.name.charAt(0).toUpperCase();
const clanName = () => ctx?.activeClanQuery?.data?.name;
const clans = () => ctx.otherClanQueries.filter((clan) => !clan.isError);
const clanList = () =>
ctx.allClansQueries
.filter((it) => it.isSuccess)
.map((it) => it.data!)
.sort((a, b) => a.name.localeCompare(b.name));
return (
<div class="sidebar-header">
@@ -100,13 +104,13 @@ export const SidebarHeader = () => {
</Button>
</DropdownMenu.GroupLabel>
<div class="dropdown-group-items">
<For each={clans()}>
<For each={clanList()}>
{(clan) => (
<Suspense fallback={"Loading..."}>
<DropdownMenu.Item
class="dropdown-item"
onSelect={() => {
setActiveClanURI(clan.data!.uri);
setActiveClanURI(clan.uri);
}}
>
<Typography
@@ -114,7 +118,7 @@ export const SidebarHeader = () => {
size="xs"
weight="medium"
>
{clan.data?.name}
{clan.name}
</Typography>
</DropdownMenu.Item>
</Suspense>

View File

@@ -1,7 +1,14 @@
import { useQueries, useQuery, UseQueryResult } from "@tanstack/solid-query";
import {
QueryClient,
useQueries,
useQuery,
UseQueryResult,
} from "@tanstack/solid-query";
import { SuccessData } from "../hooks/api";
import { encodeBase64 } from "@/src/hooks/clan";
import { useApiClient } from "./ApiClient";
import { experimental_createQueryPersister } from "@tanstack/solid-query-persist-client";
import { ClanDetailsStore } from "@/src/stores/clanDetails";
export type ClanDetails = SuccessData<"get_clan_details">;
export type ClanDetailsWithURI = ClanDetails & { uri: string };
@@ -24,6 +31,14 @@ export interface MachineDetail {
export type MachinesQueryResult = UseQueryResult<ListMachines>;
export type ClanListQueryResult = UseQueryResult<ClanDetailsWithURI>[];
export const DefaultQueryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
export const useMachinesQuery = (clanURI: string) => {
const client = useApiClient();
@@ -155,10 +170,15 @@ export const useMachineDetailsQuery = (
}));
};
export const ClanDetailsPersister = experimental_createQueryPersister({
storage: ClanDetailsStore,
});
export const useClanDetailsQuery = (clanURI: string) => {
const client = useApiClient();
return useQuery<ClanDetailsWithURI>(() => ({
queryKey: ["clans", encodeBase64(clanURI), "details"],
persister: ClanDetailsPersister.persisterFn,
queryFn: async () => {
const call = client.fetch("get_clan_details", {
flake: {
@@ -169,7 +189,7 @@ export const useClanDetailsQuery = (clanURI: string) => {
if (result.status === "error") {
// todo should we create some specific error types?
console.error("Error fetching clan details:", result.errors);
console.error("Error fetching clan details", clanURI, result.errors);
throw new Error(result.errors[0].message);
}
@@ -181,32 +201,54 @@ export const useClanDetailsQuery = (clanURI: string) => {
}));
};
export const useClanListQuery = (clanURIs: string[]): ClanListQueryResult => {
export const useClanListQuery = (
clanURIs: string[],
activeClanURI?: string,
): ClanListQueryResult => {
const client = useApiClient();
return useQueries(() => ({
queries: clanURIs.map((clanURI) => ({
queryKey: ["clans", encodeBase64(clanURI), "details"],
enabled: !!clanURI,
queryFn: async () => {
const call = client.fetch("get_clan_details", {
flake: {
identifier: clanURI,
},
});
const result = await call.result;
queries: clanURIs.map((clanURI) => {
const queryKey = ["clans", encodeBase64(clanURI), "details"];
if (result.status === "error") {
// todo should we create some specific error types?
console.error("Error fetching clan details:", result.errors);
throw new Error(result.errors[0].message);
}
return {
// eslint-disable-next-line @tanstack/query/exhaustive-deps
queryKey,
persister: ClanDetailsPersister.persisterFn,
queryFn: async () => {
// we only perform a request for the active clan
// for all others we load the cached query state
// this is due to how expensive it currently is to evaluate a flake for clan details
// it also helps when a clan folder has been moved/renamed
if (clanURI != activeClanURI) {
const cached = DefaultQueryClient.getQueryCache().find({
queryKey,
});
return {
uri: clanURI,
...result.data,
};
},
})),
if (cached?.state?.data) {
return cached.state.data;
}
}
const call = client.fetch("get_clan_details", {
flake: {
identifier: clanURI,
},
});
const result = await call.result;
if (result.status === "error") {
// todo should we create some specific error types?
throw new Error(result.errors[0].message);
}
return {
uri: clanURI,
...result.data,
};
},
};
}),
}));
};

View File

@@ -2,15 +2,14 @@
import { render } from "solid-js/web";
import "./index.css";
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
import { QueryClientProvider } from "@tanstack/solid-query";
import { Routes } from "@/src/routes";
import { Router } from "@solidjs/router";
import { Layout } from "@/src/routes/Layout";
import { SolidQueryDevtools } from "@tanstack/solid-query-devtools";
import { ApiClientProvider } from "./hooks/ApiClient";
import { callApi } from "./hooks/api";
export const client = new QueryClient();
import { DefaultQueryClient } from "@/src/hooks/queries";
const root = document.getElementById("app");
@@ -26,7 +25,7 @@ if (import.meta.env.DEV) {
render(
() => (
<ApiClientProvider client={{ fetch: callApi }}>
<QueryClientProvider client={client}>
<QueryClientProvider client={DefaultQueryClient}>
{import.meta.env.DEV && <SolidQueryDevtools initialIsOpen={true} />}
<Router root={Layout}>{Routes}</Router>
</QueryClientProvider>

View File

@@ -35,30 +35,84 @@ import { TextInput } from "@/src/components/Form/TextInput";
import { createForm, FieldValues, reset } from "@modular-forms/solid";
import { Sidebar } from "@/src/components/Sidebar/Sidebar";
import { UseQueryResult } from "@tanstack/solid-query";
import { ListClansModal } from "@/src/components/ListClansModal/ListClansModal";
export const ClanContext = createContext<{
interface ClanContextProps {
clanURI: string;
machinesQuery: MachinesQueryResult;
activeClanQuery: UseQueryResult<ClanDetailsWithURI>;
otherClanQueries: UseQueryResult<ClanDetailsWithURI>[];
}>();
allClansQueries: UseQueryResult<ClanDetailsWithURI>[];
isLoading(): boolean;
isError(): boolean;
}
class DefaultClanContext implements ClanContextProps {
public readonly clanURI: string;
public readonly activeClanQuery: UseQueryResult<ClanDetailsWithURI>;
public readonly otherClanQueries: UseQueryResult<ClanDetailsWithURI>[];
public readonly allClansQueries: UseQueryResult<ClanDetailsWithURI>[];
public readonly machinesQuery: MachinesQueryResult;
allQueries: UseQueryResult[];
constructor(
clanURI: string,
machinesQuery: MachinesQueryResult,
activeClanQuery: UseQueryResult<ClanDetailsWithURI>,
otherClanQueries: UseQueryResult<ClanDetailsWithURI>[],
) {
this.clanURI = clanURI;
this.machinesQuery = machinesQuery;
this.activeClanQuery = activeClanQuery;
this.otherClanQueries = otherClanQueries;
this.allClansQueries = [activeClanQuery, ...otherClanQueries];
this.allQueries = [machinesQuery, activeClanQuery, ...otherClanQueries];
}
isLoading(): boolean {
return this.allQueries.some((q) => q.isLoading);
}
isError(): boolean {
return this.activeClanQuery.isError;
}
}
export const ClanContext = createContext<ClanContextProps>();
export const Clan: Component<RouteSectionProps> = (props) => {
const clanURI = useClanURI();
const activeClanQuery = useClanDetailsQuery(clanURI);
createEffect(() => {
if (activeClanQuery.isError) {
console.error("Error loading active clan", activeClanQuery.error);
}
});
const otherClanQueries = useClanListQuery(
clanURIs().filter((uri) => uri !== clanURI),
clanURIs().filter((uri) => uri != clanURI),
clanURI,
);
const machinesQuery = useMachinesQuery(clanURI);
return (
<ClanContext.Provider
value={{
machinesQuery,
activeClanQuery,
otherClanQueries,
}}
value={
new DefaultClanContext(
clanURI,
machinesQuery,
activeClanQuery,
otherClanQueries,
)
}
>
<Sidebar class={cx(styles.sidebar)} />
{props.children}
@@ -118,14 +172,13 @@ const MockCreateMachine = (props: MockProps) => {
};
const ClanSceneController = (props: RouteSectionProps) => {
const clanURI = useClanURI();
const navigate = useNavigate();
const ctx = useContext(ClanContext);
if (!ctx) {
throw new Error("ClanContext not found");
}
const navigate = useNavigate();
const [dialogHandlers, setDialogHandlers] = createSignal<{
resolve: ({ id }: { id: string }) => void;
reject: (err: unknown) => void;
@@ -142,7 +195,7 @@ const ClanSceneController = (props: RouteSectionProps) => {
const api = callApi("create_machine", {
opts: {
clan_dir: {
identifier: clanURI,
identifier: ctx.clanURI,
},
machine: {
name: values.name,
@@ -166,29 +219,38 @@ const ClanSceneController = (props: RouteSectionProps) => {
const [showModal, setShowModal] = createSignal(false);
const [loadingError, setLoadingError] = createSignal<
{ title: string; description: string } | undefined
>();
const [loadingCooldown, setLoadingCooldown] = createSignal(false);
onMount(() => {
setTimeout(() => {
setLoadingCooldown(true);
}, 1500);
});
createEffect(() => {
if (ctx.activeClanQuery.isError) {
setLoadingError({
title: "Error loading clan",
description: ctx.activeClanQuery.error.message,
});
}
});
const [selectedIds, setSelectedIds] = createSignal<Set<string>>(new Set());
const onMachineSelect = (ids: Set<string>) => {
// Get the first selected ID and navigate to its machine details
const selected = ids.values().next().value;
if (selected) {
navigate(buildMachinePath(clanURI, selected));
navigate(buildMachinePath(ctx.clanURI, selected));
}
};
const machine = createMemo(() => maybeUseMachineName());
createEffect(() => {
console.log("Selected clan:", clanURI);
});
createEffect(
on(machine, (machineId) => {
if (machineId) {
@@ -203,32 +265,11 @@ 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 (
<>
<Show when={loadingError()}>
<ListClansModal error={loadingError()} />
</Show>
<Show when={showModal()}>
<MockCreateMachine
onClose={() => {
@@ -249,7 +290,7 @@ const ClanSceneController = (props: RouteSectionProps) => {
</Show>
<div
class={cx({
[styles.fadeOut]: !ctx.machinesQuery.isLoading && loadingCooldown(),
[styles.fadeOut]: !ctx.isLoading() && loadingCooldown(),
})}
>
<Splash />
@@ -258,13 +299,10 @@ const ClanSceneController = (props: RouteSectionProps) => {
<CubeScene
selectedIds={selectedIds}
onSelect={onMachineSelect}
isLoading={isLoading()}
isLoading={ctx.isLoading()}
cubesQuery={ctx.machinesQuery}
onCreate={onCreate}
sceneStore={() => {
const clanURI = useClanURI();
return store.sceneData?.[clanURI];
}}
sceneStore={() => store.sceneData?.[ctx.clanURI]}
setMachinePos={(machineId: string, pos: [number, number]) => {
console.log("calling setStore", machineId, pos);
setStore(
@@ -272,13 +310,13 @@ const ClanSceneController = (props: RouteSectionProps) => {
if (!s.sceneData) {
s.sceneData = {};
}
if (!s.sceneData[clanURI]) {
s.sceneData[clanURI] = {};
if (!s.sceneData[ctx.clanURI]) {
s.sceneData[ctx.clanURI] = {};
}
if (!s.sceneData[clanURI][machineId]) {
s.sceneData[clanURI][machineId] = { position: pos };
if (!s.sceneData[ctx.clanURI][machineId]) {
s.sceneData[ctx.clanURI][machineId] = { position: pos };
} else {
s.sceneData[clanURI][machineId].position = pos;
s.sceneData[ctx.clanURI][machineId].position = pos;
}
}),
);

View File

@@ -7,12 +7,18 @@ export interface ClanStoreType {
clanURIs: string[];
activeClanURI?: string;
sceneData: Record<string, SceneData>;
queryCache: {
clanDetails: Record<string, string>;
};
}
const [store, setStore] = makePersisted(
createStore<ClanStoreType>({
clanURIs: [],
sceneData: {},
queryCache: {
clanDetails: {},
},
}),
{
name: "clanStore",

View File

@@ -0,0 +1,32 @@
import { produce } from "solid-js/store";
import { AsyncStorage } from "@tanstack/query-persist-client-core";
import { setStore, store } from "@/src/stores/clan";
class ClanDetailsStoreImpl implements AsyncStorage {
entries() {
return Object.entries(store.queryCache.clanDetails);
}
getItem(key: string) {
return store.queryCache.clanDetails[key];
}
removeItem(key: string) {
setStore(
produce((state) => {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete state.queryCache.clanDetails[key];
}),
);
}
setItem(key: string, value: string) {
return setStore(
produce((state) => {
state.queryCache.clanDetails[key] = value;
}),
);
}
}
export const ClanDetailsStore = new ClanDetailsStoreImpl();