diff --git a/pkgs/clan-app/ui/package-lock.json b/pkgs/clan-app/ui/package-lock.json index b8543f65f..e3616b477 100644 --- a/pkgs/clan-app/ui/package-lock.json +++ b/pkgs/clan-app/ui/package-lock.json @@ -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" diff --git a/pkgs/clan-app/ui/package.json b/pkgs/clan-app/ui/package.json index 93c386801..b60a38c0c 100644 --- a/pkgs/clan-app/ui/package.json +++ b/pkgs/clan-app/ui/package.json @@ -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", diff --git a/pkgs/clan-app/ui/src/components/ListClansModal/ListClansModal.tsx b/pkgs/clan-app/ui/src/components/ListClansModal/ListClansModal.tsx index 7a2a5c711..022a5d415 100644 --- a/pkgs/clan-app/ui/src/components/ListClansModal/ListClansModal.tsx +++ b/pkgs/clan-app/ui/src/components/ListClansModal/ListClansModal.tsx @@ -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); }} > diff --git a/pkgs/clan-app/ui/src/components/Modal/Modal.tsx b/pkgs/clan-app/ui/src/components/Modal/Modal.tsx index cdca5d8c7..e69089fb1 100644 --- a/pkgs/clan-app/ui/src/components/Modal/Modal.tsx +++ b/pkgs/clan-app/ui/src/components/Modal/Modal.tsx @@ -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} - { - props.onClose(); - }} - > - - + + + + + {(metaHeader) => ( diff --git a/pkgs/clan-app/ui/src/hooks/queries.ts b/pkgs/clan-app/ui/src/hooks/queries.ts index 72817df99..97d58cd56 100644 --- a/pkgs/clan-app/ui/src/hooks/queries.ts +++ b/pkgs/clan-app/ui/src/hooks/queries.ts @@ -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; export type ClanListQueryResult = UseQueryResult[]; +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(() => ({ 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, + }; + }, + }; + }), })); }; diff --git a/pkgs/clan-app/ui/src/index.tsx b/pkgs/clan-app/ui/src/index.tsx index 79c8de509..27056d870 100644 --- a/pkgs/clan-app/ui/src/index.tsx +++ b/pkgs/clan-app/ui/src/index.tsx @@ -2,21 +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({ - defaultOptions: { - queries: { - retry: false, - }, - }, -}); +import { DefaultQueryClient } from "@/src/hooks/queries"; const root = document.getElementById("app"); @@ -32,7 +25,7 @@ if (import.meta.env.DEV) { render( () => ( - + {import.meta.env.DEV && } {Routes} diff --git a/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx b/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx index b26d41f54..23b9fa4f4 100644 --- a/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx +++ b/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx @@ -35,6 +35,7 @@ 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"; interface ClanContextProps { clanURI: string; @@ -44,16 +45,18 @@ interface ClanContextProps { allClansQueries: UseQueryResult[]; isLoading(): boolean; + isError(): boolean; } class DefaultClanContext implements ClanContextProps { public readonly clanURI: string; - public readonly machinesQuery: MachinesQueryResult; public readonly activeClanQuery: UseQueryResult; public readonly otherClanQueries: UseQueryResult[]; public readonly allClansQueries: UseQueryResult[]; + public readonly machinesQuery: MachinesQueryResult; + allQueries: UseQueryResult[]; constructor( @@ -75,6 +78,10 @@ class DefaultClanContext implements ClanContextProps { isLoading(): boolean { return this.allQueries.some((q) => q.isLoading); } + + isError(): boolean { + return this.activeClanQuery.isError; + } } export const ClanContext = createContext(); @@ -83,8 +90,15 @@ export const Clan: Component = (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); @@ -164,7 +178,6 @@ const ClanSceneController = (props: RouteSectionProps) => { } const navigate = useNavigate(); - const { clanURI } = ctx; const [dialogHandlers, setDialogHandlers] = createSignal<{ resolve: ({ id }: { id: string }) => void; @@ -182,7 +195,7 @@ const ClanSceneController = (props: RouteSectionProps) => { const api = callApi("create_machine", { opts: { clan_dir: { - identifier: clanURI, + identifier: ctx.clanURI, }, machine: { name: values.name, @@ -206,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>(new Set()); const onMachineSelect = (ids: Set) => { // 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) { @@ -245,6 +267,9 @@ const ClanSceneController = (props: RouteSectionProps) => { return ( <> + + + { @@ -285,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; } }), ); diff --git a/pkgs/clan-app/ui/src/stores/clan.ts b/pkgs/clan-app/ui/src/stores/clan.ts index 3eaa591a3..f69c6b395 100644 --- a/pkgs/clan-app/ui/src/stores/clan.ts +++ b/pkgs/clan-app/ui/src/stores/clan.ts @@ -7,12 +7,18 @@ export interface ClanStoreType { clanURIs: string[]; activeClanURI?: string; sceneData: Record; + queryCache: { + clanDetails: Record; + }; } const [store, setStore] = makePersisted( createStore({ clanURIs: [], sceneData: {}, + queryCache: { + clanDetails: {}, + }, }), { name: "clanStore", diff --git a/pkgs/clan-app/ui/src/stores/clanDetails.ts b/pkgs/clan-app/ui/src/stores/clanDetails.ts new file mode 100644 index 000000000..5e4c68c42 --- /dev/null +++ b/pkgs/clan-app/ui/src/stores/clanDetails.ts @@ -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();