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.
|
# These imports are unused, but necessary for @API.register to run once.
|
||||||
from clan_cli.api import directory, mdns_discovery, modules
|
from clan_cli.api import directory, mdns_discovery, modules
|
||||||
from clan_cli.arg_actions import AppendOptionAction
|
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.
|
# API endpoints that are not used in the cli.
|
||||||
__all__ = ["directory", "mdns_discovery", "modules"]
|
__all__ = ["directory", "mdns_discovery", "modules", "update"]
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
backups,
|
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 { Layout } from "./layout/layout";
|
||||||
import { Route, Router } from "./Routes";
|
import { Route, Router } from "./Routes";
|
||||||
import { Toaster } from "solid-toast";
|
import { Toaster } from "solid-toast";
|
||||||
@@ -7,9 +7,20 @@ import { makePersisted } from "@solid-primitives/storage";
|
|||||||
|
|
||||||
// Some global state
|
// Some global state
|
||||||
const [route, setRoute] = createSignal<Route>("machines");
|
const [route, setRoute] = createSignal<Route>("machines");
|
||||||
|
createEffect(() => {
|
||||||
|
console.log(route());
|
||||||
|
});
|
||||||
|
|
||||||
export { route, setRoute };
|
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 };
|
export { activeURI, setActiveURI };
|
||||||
|
|
||||||
const [clanList, setClanList] = makePersisted(createSignal<string[]>([]), {
|
const [clanList, setClanList] = makePersisted(createSignal<string[]>([]), {
|
||||||
@@ -17,8 +28,6 @@ const [clanList, setClanList] = makePersisted(createSignal<string[]>([]), {
|
|||||||
storage: localStorage,
|
storage: localStorage,
|
||||||
});
|
});
|
||||||
|
|
||||||
clanList() && setActiveURI(clanList()[0]);
|
|
||||||
|
|
||||||
export { clanList, setClanList };
|
export { clanList, setClanList };
|
||||||
|
|
||||||
const App: Component = () => {
|
const App: Component = () => {
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import { createQuery } from "@tanstack/solid-query";
|
import { createQuery } from "@tanstack/solid-query";
|
||||||
import { activeURI, setRoute } from "../App";
|
import { activeURI, setRoute } from "../App";
|
||||||
import { callApi } from "../api";
|
import { callApi } from "../api";
|
||||||
import { Show } from "solid-js";
|
import { Accessor, createEffect, Show } from "solid-js";
|
||||||
|
|
||||||
export const Header = () => {
|
interface HeaderProps {
|
||||||
const { isLoading, data } = createQuery(() => ({
|
clan_dir: Accessor<string | null>;
|
||||||
queryKey: [`${activeURI()}:meta`],
|
}
|
||||||
|
export const Header = (props: HeaderProps) => {
|
||||||
|
const { clan_dir } = props;
|
||||||
|
|
||||||
|
const query = createQuery(() => ({
|
||||||
|
queryKey: [clan_dir(), "meta"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const currUri = activeURI();
|
const curr = clan_dir();
|
||||||
if (currUri) {
|
if (curr) {
|
||||||
const result = await callApi("show_clan_meta", { uri: currUri });
|
const result = await callApi("show_clan_meta", { uri: curr });
|
||||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||||
return result.data;
|
return result.data;
|
||||||
}
|
}
|
||||||
@@ -29,16 +34,25 @@ export const Header = () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="tooltip tooltip-right" data-tip={data?.name || activeURI()}>
|
<Show when={!query.isFetching && query.data}>
|
||||||
<div class="avatar placeholder online mx-4">
|
{(meta) => (
|
||||||
<div class="w-10 rounded-full bg-slate-700 text-neutral-content">
|
<div class="tooltip tooltip-right" data-tip={activeURI()}>
|
||||||
<span class="text-xl">C</span>
|
<div class="avatar placeholder online mx-4">
|
||||||
<Show when={data?.name}>
|
<div class="w-10 rounded-full bg-slate-700 text-3xl text-neutral-content">
|
||||||
{(name) => <span class="text-xl">{name()}</span>}
|
{meta().name.slice(0, 1)}
|
||||||
</Show>
|
</div>
|
||||||
|
</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>
|
||||||
<div class="flex-none">
|
<div class="flex-none">
|
||||||
<span class="tooltip tooltip-bottom" data-tip="Settings">
|
<span class="tooltip tooltip-bottom" data-tip="Settings">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, JSXElement, Show } from "solid-js";
|
import { Component, JSXElement, Show } from "solid-js";
|
||||||
import { Header } from "./header";
|
import { Header } from "./header";
|
||||||
import { Sidebar } from "../Sidebar";
|
import { Sidebar } from "../Sidebar";
|
||||||
import { clanList, route, setRoute } from "../App";
|
import { activeURI, clanList, route, setRoute } from "../App";
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: JSXElement;
|
children: JSXElement;
|
||||||
@@ -18,7 +18,7 @@ export const Layout: Component<LayoutProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
<div class="drawer-content">
|
<div class="drawer-content">
|
||||||
<Show when={route() !== "welcome"}>
|
<Show when={route() !== "welcome"}>
|
||||||
<Header />
|
<Header clan_dir={activeURI} />
|
||||||
</Show>
|
</Show>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</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 { activeURI } from "@/src/App";
|
||||||
import { createQuery } from "@tanstack/solid-query";
|
import { createQuery } from "@tanstack/solid-query";
|
||||||
import { createEffect } from "solid-js";
|
import { createEffect } from "solid-js";
|
||||||
|
import toast from "solid-toast";
|
||||||
|
|
||||||
export function DiskView() {
|
export function DiskView() {
|
||||||
const query = createQuery(() => ({
|
const query = createQuery(() => ({
|
||||||
queryKey: ["disk", activeURI],
|
queryKey: ["disk", activeURI()],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const currUri = activeURI();
|
const currUri = activeURI();
|
||||||
if (currUri) {
|
if (currUri) {
|
||||||
// Example of calling an API
|
// Example of calling an API
|
||||||
const result = await callApi("get_inventory", { base_path: currUri });
|
const result = await callApi("get_inventory", { base_path: currUri });
|
||||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||||
|
|
||||||
return result.data;
|
return result.data;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
// Example debugging the data
|
// Example debugging the data
|
||||||
console.log(query.data);
|
console.log(query);
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -12,18 +12,19 @@ import {
|
|||||||
setRoute,
|
setRoute,
|
||||||
clanList,
|
clanList,
|
||||||
} from "@/src/App";
|
} 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 { createQuery } from "@tanstack/solid-query";
|
||||||
import { useFloating } from "@/src/floating";
|
import { useFloating } from "@/src/floating";
|
||||||
import {
|
import { autoUpdate, flip, hide, offset, shift } from "@floating-ui/dom";
|
||||||
arrow,
|
import { EditClanForm } from "../clan/editClan";
|
||||||
autoUpdate,
|
|
||||||
flip,
|
|
||||||
hide,
|
|
||||||
offset,
|
|
||||||
shift,
|
|
||||||
size,
|
|
||||||
} from "@floating-ui/dom";
|
|
||||||
|
|
||||||
export const registerClan = async () => {
|
export const registerClan = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -51,9 +52,10 @@ export const registerClan = async () => {
|
|||||||
|
|
||||||
interface ClanDetailsProps {
|
interface ClanDetailsProps {
|
||||||
clan_dir: string;
|
clan_dir: string;
|
||||||
|
setEditURI: Setter<string | null>;
|
||||||
}
|
}
|
||||||
const ClanDetails = (props: ClanDetailsProps) => {
|
const ClanDetails = (props: ClanDetailsProps) => {
|
||||||
const { clan_dir } = props;
|
const { clan_dir, setEditURI } = props;
|
||||||
|
|
||||||
const details = createQuery(() => ({
|
const details = createQuery(() => ({
|
||||||
queryKey: [clan_dir, "meta"],
|
queryKey: [clan_dir, "meta"],
|
||||||
@@ -66,7 +68,6 @@ const ClanDetails = (props: ClanDetailsProps) => {
|
|||||||
|
|
||||||
const [reference, setReference] = createSignal<HTMLElement>();
|
const [reference, setReference] = createSignal<HTMLElement>();
|
||||||
const [floating, setFloating] = createSignal<HTMLElement>();
|
const [floating, setFloating] = createSignal<HTMLElement>();
|
||||||
const [arrowEl, setArrowEl] = createSignal<HTMLElement>();
|
|
||||||
|
|
||||||
// `position` is a reactive object.
|
// `position` is a reactive object.
|
||||||
const position = useFloating(reference, floating, {
|
const position = useFloating(reference, floating, {
|
||||||
@@ -92,6 +93,14 @@ const ClanDetails = (props: ClanDetailsProps) => {
|
|||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-figure text-primary">
|
<div class="stat-figure text-primary">
|
||||||
<div class="join">
|
<div class="join">
|
||||||
|
<button
|
||||||
|
class=" join-item btn-sm"
|
||||||
|
onClick={() => {
|
||||||
|
setEditURI(clan_dir);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="material-icons">edit</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
class=" join-item btn-sm"
|
class=" join-item btn-sm"
|
||||||
classList={{
|
classList={{
|
||||||
@@ -145,56 +154,57 @@ const ClanDetails = (props: ClanDetailsProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-title">Clan URI</div>
|
<div class="stat-title">{clan_dir}</div>
|
||||||
|
|
||||||
<Show when={details.isSuccess}>
|
<Show when={details.isSuccess}>
|
||||||
<div
|
<div class="stat-value">{details.data?.name}</div>
|
||||||
class="stat-value"
|
|
||||||
// classList={{
|
|
||||||
// "text-primary": activeURI() === clan_dir,
|
|
||||||
// }}
|
|
||||||
>
|
|
||||||
{details.data?.name}
|
|
||||||
</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
<Show
|
<Show when={details.isSuccess && details.data?.description}>
|
||||||
when={details.isSuccess && details.data?.description}
|
<div class="stat-desc text-lg">{details.data?.description}</div>
|
||||||
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>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Settings = () => {
|
export const Settings = () => {
|
||||||
|
const [editURI, setEditURI] = createSignal<string | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="card card-normal">
|
<div class="card card-normal">
|
||||||
<div class="card-body">
|
<Switch>
|
||||||
<div class="label">
|
<Match when={editURI()}>
|
||||||
<div class="label-text">Registered Clans</div>
|
{(uri) => (
|
||||||
<button
|
<EditClanForm
|
||||||
class="btn btn-square btn-primary"
|
directory={uri}
|
||||||
onClick={() => {
|
done={() => {
|
||||||
registerClan();
|
setEditURI(null);
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<span class="material-icons">add</span>
|
)}
|
||||||
</button>
|
</Match>
|
||||||
</div>
|
<Match when={!editURI()}>
|
||||||
<div class="stats stats-vertical shadow">
|
<div class="card-body">
|
||||||
<For each={clanList()}>
|
<div class="label">
|
||||||
{(value) => <ClanDetails clan_dir={value} />}
|
<div class="label-text">Registered Clans</div>
|
||||||
</For>
|
<button
|
||||||
</div>
|
class="btn btn-square btn-primary"
|
||||||
</div>
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user