Clan-app: edit clan, memoize active clan
This commit is contained in:
@@ -7,10 +7,10 @@ from types import ModuleType
|
||||
# These imports are unused, but necessary for @API.register to run once.
|
||||
from clan_cli.api import directory, mdns_discovery, modules
|
||||
from clan_cli.arg_actions import AppendOptionAction
|
||||
from clan_cli.clan import show
|
||||
from clan_cli.clan import show, update
|
||||
|
||||
# API endpoints that are not used in the cli.
|
||||
__all__ = ["directory", "mdns_discovery", "modules"]
|
||||
__all__ = ["directory", "mdns_discovery", "modules", "update"]
|
||||
|
||||
from . import (
|
||||
backups,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createSignal, type Component } from "solid-js";
|
||||
import { createEffect, createSignal, type Component } from "solid-js";
|
||||
import { Layout } from "./layout/layout";
|
||||
import { Route, Router } from "./Routes";
|
||||
import { Toaster } from "solid-toast";
|
||||
@@ -7,9 +7,20 @@ import { makePersisted } from "@solid-primitives/storage";
|
||||
|
||||
// Some global state
|
||||
const [route, setRoute] = createSignal<Route>("machines");
|
||||
createEffect(() => {
|
||||
console.log(route());
|
||||
});
|
||||
|
||||
export { route, setRoute };
|
||||
|
||||
const [activeURI, setActiveURI] = createSignal<string | null>(null);
|
||||
const [activeURI, setActiveURI] = makePersisted(
|
||||
createSignal<string | null>(null),
|
||||
{
|
||||
name: "activeURI",
|
||||
storage: localStorage,
|
||||
}
|
||||
);
|
||||
|
||||
export { activeURI, setActiveURI };
|
||||
|
||||
const [clanList, setClanList] = makePersisted(createSignal<string[]>([]), {
|
||||
@@ -17,8 +28,6 @@ const [clanList, setClanList] = makePersisted(createSignal<string[]>([]), {
|
||||
storage: localStorage,
|
||||
});
|
||||
|
||||
clanList() && setActiveURI(clanList()[0]);
|
||||
|
||||
export { clanList, setClanList };
|
||||
|
||||
const App: Component = () => {
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import { createQuery } from "@tanstack/solid-query";
|
||||
import { activeURI, setRoute } from "../App";
|
||||
import { callApi } from "../api";
|
||||
import { Show } from "solid-js";
|
||||
import { Accessor, createEffect, Show } from "solid-js";
|
||||
|
||||
export const Header = () => {
|
||||
const { isLoading, data } = createQuery(() => ({
|
||||
queryKey: [`${activeURI()}:meta`],
|
||||
interface HeaderProps {
|
||||
clan_dir: Accessor<string | null>;
|
||||
}
|
||||
export const Header = (props: HeaderProps) => {
|
||||
const { clan_dir } = props;
|
||||
|
||||
const query = createQuery(() => ({
|
||||
queryKey: [clan_dir(), "meta"],
|
||||
queryFn: async () => {
|
||||
const currUri = activeURI();
|
||||
if (currUri) {
|
||||
const result = await callApi("show_clan_meta", { uri: currUri });
|
||||
const curr = clan_dir();
|
||||
if (curr) {
|
||||
const result = await callApi("show_clan_meta", { uri: curr });
|
||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||
return result.data;
|
||||
}
|
||||
@@ -29,16 +34,25 @@ export const Header = () => {
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="tooltip tooltip-right" data-tip={data?.name || activeURI()}>
|
||||
<div class="avatar placeholder online mx-4">
|
||||
<div class="w-10 rounded-full bg-slate-700 text-neutral-content">
|
||||
<span class="text-xl">C</span>
|
||||
<Show when={data?.name}>
|
||||
{(name) => <span class="text-xl">{name()}</span>}
|
||||
</Show>
|
||||
<Show when={!query.isFetching && query.data}>
|
||||
{(meta) => (
|
||||
<div class="tooltip tooltip-right" data-tip={activeURI()}>
|
||||
<div class="avatar placeholder online mx-4">
|
||||
<div class="w-10 rounded-full bg-slate-700 text-3xl text-neutral-content">
|
||||
{meta().name.slice(0, 1)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
<span class="flex flex-col">
|
||||
<Show when={!query.isFetching && query.data}>
|
||||
{(meta) => [
|
||||
<span class="text-primary">{meta().name}</span>,
|
||||
<span class="text-neutral">{meta()?.description}</span>,
|
||||
]}
|
||||
</Show>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<span class="tooltip tooltip-bottom" data-tip="Settings">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, JSXElement, Show } from "solid-js";
|
||||
import { Header } from "./header";
|
||||
import { Sidebar } from "../Sidebar";
|
||||
import { clanList, route, setRoute } from "../App";
|
||||
import { activeURI, clanList, route, setRoute } from "../App";
|
||||
|
||||
interface LayoutProps {
|
||||
children: JSXElement;
|
||||
@@ -18,7 +18,7 @@ export const Layout: Component<LayoutProps> = (props) => {
|
||||
/>
|
||||
<div class="drawer-content">
|
||||
<Show when={route() !== "welcome"}>
|
||||
<Header />
|
||||
<Header clan_dir={activeURI} />
|
||||
</Show>
|
||||
{props.children}
|
||||
</div>
|
||||
|
||||
175
pkgs/webview-ui/app/src/routes/clan/editClan.tsx
Normal file
175
pkgs/webview-ui/app/src/routes/clan/editClan.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import { OperationResponse, callApi, pyApi } from "@/src/api";
|
||||
import { Accessor, Show, Switch, Match } from "solid-js";
|
||||
import {
|
||||
SubmitHandler,
|
||||
createForm,
|
||||
required,
|
||||
reset,
|
||||
} from "@modular-forms/solid";
|
||||
import toast from "solid-toast";
|
||||
import { createQuery } from "@tanstack/solid-query";
|
||||
|
||||
type CreateForm = Meta;
|
||||
|
||||
interface EditClanFormProps {
|
||||
directory: Accessor<string>;
|
||||
done: () => void;
|
||||
}
|
||||
export const EditClanForm = (props: EditClanFormProps) => {
|
||||
const { directory } = props;
|
||||
const details = createQuery(() => ({
|
||||
queryKey: [directory(), "meta"],
|
||||
queryFn: async () => {
|
||||
const result = await callApi("show_clan_meta", { uri: directory() });
|
||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||
return result.data;
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={details?.data}>
|
||||
{(data) => (
|
||||
<FinalEditClanForm
|
||||
initial={data()}
|
||||
directory={directory()}
|
||||
done={props.done}
|
||||
/>
|
||||
)}
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
||||
interface FinalEditClanFormProps {
|
||||
initial: CreateForm;
|
||||
directory: string;
|
||||
done: () => void;
|
||||
}
|
||||
export const FinalEditClanForm = (props: FinalEditClanFormProps) => {
|
||||
const [formStore, { Form, Field }] = createForm<CreateForm>({
|
||||
initialValues: props.initial,
|
||||
});
|
||||
|
||||
const handleSubmit: SubmitHandler<CreateForm> = async (values, event) => {
|
||||
await toast.promise(
|
||||
(async () => {
|
||||
await callApi("update_clan_meta", {
|
||||
options: {
|
||||
directory: props.directory,
|
||||
meta: values,
|
||||
},
|
||||
});
|
||||
})(),
|
||||
{
|
||||
loading: "Updating clan...",
|
||||
success: "Clan Successfully updated",
|
||||
error: "Failed to update clan",
|
||||
}
|
||||
);
|
||||
props.done();
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="card card-normal">
|
||||
<Form onSubmit={handleSubmit} shouldActive>
|
||||
<Field name="icon">
|
||||
{(field, props) => (
|
||||
<>
|
||||
<figure>
|
||||
<Show
|
||||
when={field.value}
|
||||
fallback={
|
||||
<span class="material-icons aspect-square size-60 rounded-lg text-[18rem]">
|
||||
group
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{(icon) => (
|
||||
<img
|
||||
class="aspect-square size-60 rounded-lg"
|
||||
src={icon()}
|
||||
alt="Clan Logo"
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</figure>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
<div class="card-body">
|
||||
<Field
|
||||
name="name"
|
||||
validate={[required("Please enter a unique name for the clan.")]}
|
||||
>
|
||||
{(field, props) => (
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text block after:ml-0.5 after:text-primary after:content-['*']">
|
||||
Name
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<input
|
||||
{...props}
|
||||
disabled={formStore.submitting}
|
||||
required
|
||||
placeholder="Clan Name"
|
||||
class="input input-bordered"
|
||||
classList={{ "input-error": !!field.error }}
|
||||
value={field.value}
|
||||
/>
|
||||
<div class="label">
|
||||
{field.error && (
|
||||
<span class="label-text-alt">{field.error}</span>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
</Field>
|
||||
<Field name="description">
|
||||
{(field, props) => (
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">Description</span>
|
||||
</div>
|
||||
|
||||
<input
|
||||
{...props}
|
||||
disabled={formStore.submitting}
|
||||
required
|
||||
type="text"
|
||||
placeholder="Some words about your clan"
|
||||
class="input input-bordered"
|
||||
classList={{ "input-error": !!field.error }}
|
||||
value={field.value || ""}
|
||||
/>
|
||||
<div class="label">
|
||||
{field.error && (
|
||||
<span class="label-text-alt">{field.error}</span>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
</Field>
|
||||
{
|
||||
<div class="card-actions justify-end">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="submit"
|
||||
disabled={formStore.submitting}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type Meta = Extract<
|
||||
OperationResponse<"show_clan_meta">,
|
||||
{ status: "success" }
|
||||
>["data"];
|
||||
@@ -2,24 +2,24 @@ import { callApi } from "@/src/api";
|
||||
import { activeURI } from "@/src/App";
|
||||
import { createQuery } from "@tanstack/solid-query";
|
||||
import { createEffect } from "solid-js";
|
||||
import toast from "solid-toast";
|
||||
|
||||
export function DiskView() {
|
||||
const query = createQuery(() => ({
|
||||
queryKey: ["disk", activeURI],
|
||||
queryKey: ["disk", activeURI()],
|
||||
queryFn: async () => {
|
||||
const currUri = activeURI();
|
||||
if (currUri) {
|
||||
// Example of calling an API
|
||||
const result = await callApi("get_inventory", { base_path: currUri });
|
||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||
|
||||
return result.data;
|
||||
}
|
||||
},
|
||||
}));
|
||||
createEffect(() => {
|
||||
// Example debugging the data
|
||||
console.log(query.data);
|
||||
console.log(query);
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -12,18 +12,19 @@ import {
|
||||
setRoute,
|
||||
clanList,
|
||||
} from "@/src/App";
|
||||
import { createEffect, createSignal, For, Show } from "solid-js";
|
||||
import {
|
||||
createEffect,
|
||||
createSignal,
|
||||
For,
|
||||
Match,
|
||||
Setter,
|
||||
Show,
|
||||
Switch,
|
||||
} from "solid-js";
|
||||
import { createQuery } from "@tanstack/solid-query";
|
||||
import { useFloating } from "@/src/floating";
|
||||
import {
|
||||
arrow,
|
||||
autoUpdate,
|
||||
flip,
|
||||
hide,
|
||||
offset,
|
||||
shift,
|
||||
size,
|
||||
} from "@floating-ui/dom";
|
||||
import { autoUpdate, flip, hide, offset, shift } from "@floating-ui/dom";
|
||||
import { EditClanForm } from "../clan/editClan";
|
||||
|
||||
export const registerClan = async () => {
|
||||
try {
|
||||
@@ -51,9 +52,10 @@ export const registerClan = async () => {
|
||||
|
||||
interface ClanDetailsProps {
|
||||
clan_dir: string;
|
||||
setEditURI: Setter<string | null>;
|
||||
}
|
||||
const ClanDetails = (props: ClanDetailsProps) => {
|
||||
const { clan_dir } = props;
|
||||
const { clan_dir, setEditURI } = props;
|
||||
|
||||
const details = createQuery(() => ({
|
||||
queryKey: [clan_dir, "meta"],
|
||||
@@ -66,7 +68,6 @@ const ClanDetails = (props: ClanDetailsProps) => {
|
||||
|
||||
const [reference, setReference] = createSignal<HTMLElement>();
|
||||
const [floating, setFloating] = createSignal<HTMLElement>();
|
||||
const [arrowEl, setArrowEl] = createSignal<HTMLElement>();
|
||||
|
||||
// `position` is a reactive object.
|
||||
const position = useFloating(reference, floating, {
|
||||
@@ -92,6 +93,14 @@ const ClanDetails = (props: ClanDetailsProps) => {
|
||||
<div class="stat">
|
||||
<div class="stat-figure text-primary">
|
||||
<div class="join">
|
||||
<button
|
||||
class=" join-item btn-sm"
|
||||
onClick={() => {
|
||||
setEditURI(clan_dir);
|
||||
}}
|
||||
>
|
||||
<span class="material-icons">edit</span>
|
||||
</button>
|
||||
<button
|
||||
class=" join-item btn-sm"
|
||||
classList={{
|
||||
@@ -145,56 +154,57 @@ const ClanDetails = (props: ClanDetailsProps) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-title">Clan URI</div>
|
||||
<div class="stat-title">{clan_dir}</div>
|
||||
|
||||
<Show when={details.isSuccess}>
|
||||
<div
|
||||
class="stat-value"
|
||||
// classList={{
|
||||
// "text-primary": activeURI() === clan_dir,
|
||||
// }}
|
||||
>
|
||||
{details.data?.name}
|
||||
</div>
|
||||
<div class="stat-value">{details.data?.name}</div>
|
||||
</Show>
|
||||
<Show
|
||||
when={details.isSuccess && details.data?.description}
|
||||
fallback={<div class="stat-desc text-lg">{clan_dir}</div>}
|
||||
>
|
||||
<div
|
||||
class="stat-desc text-lg"
|
||||
// classList={{
|
||||
// "text-primary": activeURI() === clan_dir,
|
||||
// }}
|
||||
>
|
||||
{details.data?.description}
|
||||
</div>
|
||||
<Show when={details.isSuccess && details.data?.description}>
|
||||
<div class="stat-desc text-lg">{details.data?.description}</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Settings = () => {
|
||||
const [editURI, setEditURI] = createSignal<string | null>(null);
|
||||
|
||||
return (
|
||||
<div class="card card-normal">
|
||||
<div class="card-body">
|
||||
<div class="label">
|
||||
<div class="label-text">Registered Clans</div>
|
||||
<button
|
||||
class="btn btn-square btn-primary"
|
||||
onClick={() => {
|
||||
registerClan();
|
||||
}}
|
||||
>
|
||||
<span class="material-icons">add</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="stats stats-vertical shadow">
|
||||
<For each={clanList()}>
|
||||
{(value) => <ClanDetails clan_dir={value} />}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
<Switch>
|
||||
<Match when={editURI()}>
|
||||
{(uri) => (
|
||||
<EditClanForm
|
||||
directory={uri}
|
||||
done={() => {
|
||||
setEditURI(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Match>
|
||||
<Match when={!editURI()}>
|
||||
<div class="card-body">
|
||||
<div class="label">
|
||||
<div class="label-text">Registered Clans</div>
|
||||
<button
|
||||
class="btn btn-square btn-primary"
|
||||
onClick={() => {
|
||||
registerClan();
|
||||
}}
|
||||
>
|
||||
<span class="material-icons">add</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="stats stats-vertical shadow">
|
||||
<For each={clanList()}>
|
||||
{(value) => (
|
||||
<ClanDetails clan_dir={value} setEditURI={setEditURI} />
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user