UI/refactor: unify clan details and edit
This commit is contained in:
21
pkgs/webview-ui/app/src/hooks/index.ts
Normal file
21
pkgs/webview-ui/app/src/hooks/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { callApi } from "../api";
|
||||
import { setActiveURI, setClanList } from "../App";
|
||||
|
||||
export const registerClan = async () => {
|
||||
try {
|
||||
const loc = await callApi("open_file", {
|
||||
file_request: { mode: "select_folder" },
|
||||
});
|
||||
if (loc.status === "success" && loc.data) {
|
||||
const data = loc.data[0];
|
||||
setClanList((s) => {
|
||||
const res = new Set([...s, data]);
|
||||
return Array.from(res);
|
||||
});
|
||||
setActiveURI(data);
|
||||
return data;
|
||||
}
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
};
|
||||
@@ -7,15 +7,12 @@ import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
|
||||
import { MachineDetails } from "./routes/machines/[name]/view";
|
||||
import { Layout } from "./layout/layout";
|
||||
import { MachineListView } from "./routes/machines/view";
|
||||
import { CreateClan } from "./routes/clan/view";
|
||||
import { Settings } from "./routes/settings";
|
||||
import { EditClanForm } from "./routes/clan/editClan";
|
||||
import { ClanList, CreateClan, ClanDetails } from "./routes/clans";
|
||||
import { Flash } from "./routes/flash/view";
|
||||
import { CreateMachine } from "./routes/machines/create";
|
||||
import { HostList } from "./routes/hosts/view";
|
||||
import { Welcome } from "./routes/welcome";
|
||||
import { Toaster } from "solid-toast";
|
||||
import { Details } from "./routes/clan/details";
|
||||
|
||||
const client = new QueryClient();
|
||||
|
||||
@@ -74,7 +71,7 @@ export const routes: AppRoute[] = [
|
||||
{
|
||||
path: "/",
|
||||
label: "Overview",
|
||||
component: () => <Settings />,
|
||||
component: () => <ClanList />,
|
||||
},
|
||||
{
|
||||
path: "/create",
|
||||
@@ -85,7 +82,7 @@ export const routes: AppRoute[] = [
|
||||
path: "/:id",
|
||||
label: "Details",
|
||||
hidden: true,
|
||||
component: () => <Details />,
|
||||
component: () => <ClanDetails />,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -64,11 +64,18 @@ export const Header = (props: HeaderProps) => {
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<span class="tooltip tooltip-bottom" data-tip="Settings">
|
||||
<button class="link" onClick={() => navigate("/clan")}>
|
||||
<Show when={activeURI()}>
|
||||
{(d) => (
|
||||
<span class="tooltip tooltip-bottom" data-tip="Clan Settings">
|
||||
<button
|
||||
class="link"
|
||||
onClick={() => navigate(`/clans/${window.btoa(d())}`)}
|
||||
>
|
||||
<span class="material-icons">settings</span>
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
import { callApi, SuccessQuery } from "@/src/api";
|
||||
import { BackButton } from "@/src/components/BackButton";
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { createQuery } from "@tanstack/solid-query";
|
||||
import { createSignal, For, Match, Switch } from "solid-js";
|
||||
import { Show } from "solid-js";
|
||||
import {
|
||||
createForm,
|
||||
FieldValues,
|
||||
getValue,
|
||||
getValues,
|
||||
setValue,
|
||||
} from "@modular-forms/solid";
|
||||
import { TextInput } from "@/src/components/TextInput";
|
||||
import toast from "solid-toast";
|
||||
|
||||
type AdminData = SuccessQuery<"get_admin_service">["data"];
|
||||
|
||||
interface ClanDetailsProps {
|
||||
admin: AdminData;
|
||||
base_url: string;
|
||||
}
|
||||
interface AdminSettings extends FieldValues {
|
||||
allowedKeys: { name: string; value: string }[];
|
||||
}
|
||||
|
||||
const ClanDetails = (props: ClanDetailsProps) => {
|
||||
const items = () =>
|
||||
Object.entries<string>(
|
||||
(props.admin?.config?.allowedKeys as Record<string, string>) || {},
|
||||
);
|
||||
const [formStore, { Form, Field }] = createForm<AdminSettings>({
|
||||
initialValues: {
|
||||
allowedKeys: items().map(([name, value]) => ({ name, value })),
|
||||
},
|
||||
});
|
||||
|
||||
const [keys, setKeys] = createSignal<1[]>(
|
||||
new Array(items().length || 1).fill(1),
|
||||
);
|
||||
|
||||
const handleSubmit = async (values: AdminSettings) => {
|
||||
console.log("submitting", values, getValues(formStore));
|
||||
|
||||
const r = await callApi("set_admin_service", {
|
||||
base_url: props.base_url,
|
||||
allowed_keys: values.allowedKeys.reduce(
|
||||
(acc, curr) => ({ ...acc, [curr.name]: curr.value }),
|
||||
{},
|
||||
),
|
||||
});
|
||||
if (r.status === "success") {
|
||||
toast.success("Successfully updated admin settings");
|
||||
}
|
||||
if (r.status === "error") {
|
||||
toast.error(`Failed to update admin settings: ${r.errors[0].message}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span class="text-xl text-primary">Clan Admin Settings</span>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<div class="grid grid-cols-12 gap-2">
|
||||
<span class="col-span-12 text-lg text-neutral">
|
||||
Each of the following keys can be used to authenticate on any
|
||||
machine
|
||||
</span>
|
||||
<For each={keys()}>
|
||||
{(name, idx) => (
|
||||
<>
|
||||
<Field name={`allowedKeys.${idx()}.name`}>
|
||||
{(field, props) => (
|
||||
<TextInput
|
||||
formStore={formStore}
|
||||
inputProps={props}
|
||||
label={"Name"}
|
||||
adornment={{
|
||||
position: "start",
|
||||
content: (
|
||||
<span class="material-icons text-gray-400">key</span>
|
||||
),
|
||||
}}
|
||||
value={field.value ?? ""}
|
||||
error={field.error}
|
||||
class="col-span-4"
|
||||
required
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Field name={`allowedKeys.${idx()}.value`}>
|
||||
{(field, props) => (
|
||||
<>
|
||||
<TextInput
|
||||
formStore={formStore}
|
||||
inputProps={props}
|
||||
label={"Value"}
|
||||
value={field.value ?? ""}
|
||||
error={field.error}
|
||||
class="col-span-6"
|
||||
required
|
||||
/>
|
||||
<span class="tooltip mt-auto" data-tip="Select file">
|
||||
<label
|
||||
class={"form-control w-full"}
|
||||
aria-disabled={formStore.submitting}
|
||||
>
|
||||
<div class="input input-bordered relative flex items-center gap-2">
|
||||
<input
|
||||
value=""
|
||||
// Disable drag n drop
|
||||
onDrop={(e) => e.preventDefault()}
|
||||
class="absolute -ml-4 size-full cursor-pointer opacity-0"
|
||||
type="file"
|
||||
onInput={async (e) => {
|
||||
console.log(e.target.files);
|
||||
if (!e.target.files) return;
|
||||
|
||||
const content = await e.target.files[0].text();
|
||||
console.log(content);
|
||||
setValue(
|
||||
formStore,
|
||||
`allowedKeys.${idx()}.value`,
|
||||
content,
|
||||
);
|
||||
if (
|
||||
!getValue(
|
||||
formStore,
|
||||
`allowedKeys.${idx()}.name`,
|
||||
)
|
||||
) {
|
||||
setValue(
|
||||
formStore,
|
||||
`allowedKeys.${idx()}.name`,
|
||||
e.target.files[0].name,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span class="material-icons">file_open</span>
|
||||
</div>
|
||||
</label>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
<button
|
||||
class="btn btn-ghost col-span-1 self-end"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setKeys((c) => c.filter((_, i) => i !== idx()));
|
||||
setValue(formStore, `allowedKeys.${idx()}.name`, "");
|
||||
setValue(formStore, `allowedKeys.${idx()}.value`, "");
|
||||
}}
|
||||
>
|
||||
<span class="material-icons">delete</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<div class="my-2 flex w-full gap-2">
|
||||
<button
|
||||
class="btn btn-square btn-ghost"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setKeys((c) => [...c, 1]);
|
||||
}}
|
||||
>
|
||||
<span class="material-icons">add</span>
|
||||
</button>
|
||||
<button class="btn" type="submit">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Details = () => {
|
||||
const params = useParams();
|
||||
const clan_dir = window.atob(params.id);
|
||||
const query = createQuery(() => ({
|
||||
queryKey: [clan_dir, "get_admin_service"],
|
||||
queryFn: async () => {
|
||||
const result = await callApi("get_admin_service", {
|
||||
base_url: clan_dir,
|
||||
});
|
||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||
return result.data || null;
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<div class="p-2">
|
||||
<BackButton />
|
||||
<Show
|
||||
when={!query.isLoading}
|
||||
fallback={<span class="loading loading-lg"></span>}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={query.data}>
|
||||
{(d) => <ClanDetails admin={query.data} base_url={clan_dir} />}
|
||||
</Match>
|
||||
</Switch>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,175 +0,0 @@
|
||||
import { callApi, OperationResponse, pyApi } from "@/src/api";
|
||||
import { Accessor, Match, Show, Switch } from "solid-js";
|
||||
import {
|
||||
createForm,
|
||||
required,
|
||||
reset,
|
||||
SubmitHandler,
|
||||
} 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"];
|
||||
@@ -1,9 +0,0 @@
|
||||
import { ClanForm } from "./clanDetails";
|
||||
|
||||
export const CreateClan = () => {
|
||||
return (
|
||||
<div>
|
||||
<ClanForm />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -15,7 +15,7 @@ type CreateForm = Meta & {
|
||||
template: string;
|
||||
};
|
||||
|
||||
export const ClanForm = () => {
|
||||
export const CreateClan = () => {
|
||||
const [formStore, { Form, Field }] = createForm<CreateForm>({
|
||||
initialValues: {
|
||||
name: "",
|
||||
394
pkgs/webview-ui/app/src/routes/clans/details.tsx
Normal file
394
pkgs/webview-ui/app/src/routes/clans/details.tsx
Normal file
@@ -0,0 +1,394 @@
|
||||
import { callApi, SuccessQuery } from "@/src/api";
|
||||
import { BackButton } from "@/src/components/BackButton";
|
||||
import { useParams } from "@solidjs/router";
|
||||
import {
|
||||
createQuery,
|
||||
QueryClient,
|
||||
useQueryClient,
|
||||
} from "@tanstack/solid-query";
|
||||
import { createSignal, For, Match, Switch } from "solid-js";
|
||||
import { Show } from "solid-js";
|
||||
import {
|
||||
createForm,
|
||||
FieldValues,
|
||||
getValue,
|
||||
getValues,
|
||||
required,
|
||||
setValue,
|
||||
SubmitHandler,
|
||||
} from "@modular-forms/solid";
|
||||
import { TextInput } from "@/src/components/TextInput";
|
||||
import toast from "solid-toast";
|
||||
|
||||
interface AdminModuleFormProps {
|
||||
admin: AdminData;
|
||||
base_url: string;
|
||||
}
|
||||
interface AdminSettings extends FieldValues {
|
||||
allowedKeys: { name: string; value: string }[];
|
||||
}
|
||||
|
||||
interface EditClanFormProps {
|
||||
initial: GeneralData;
|
||||
directory: string;
|
||||
}
|
||||
|
||||
const EditClanForm = (props: EditClanFormProps) => {
|
||||
const [formStore, { Form, Field }] = createForm<GeneralData>({
|
||||
initialValues: props.initial,
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleSubmit: SubmitHandler<GeneralData> = 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",
|
||||
},
|
||||
);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [props.directory, "meta"],
|
||||
});
|
||||
};
|
||||
const curr_name = () => props.initial.name;
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit} shouldActive>
|
||||
<Field name="icon">
|
||||
{(field) => (
|
||||
<>
|
||||
<figure class="p-1">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="text-3xl text-primary">{curr_name()}'s</div>
|
||||
<div class="text-secondary">Wide settings</div>
|
||||
</div>
|
||||
</figure>
|
||||
<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">
|
||||
<span class="text-xl text-primary">General</span>
|
||||
<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 || !formStore.dirty}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
const AdminModuleForm = (props: AdminModuleFormProps) => {
|
||||
const items = () =>
|
||||
Object.entries<string>(
|
||||
(props.admin?.config?.allowedKeys as Record<string, string>) || {},
|
||||
);
|
||||
const [formStore, { Form, Field }] = createForm<AdminSettings>({
|
||||
initialValues: {
|
||||
allowedKeys: items().map(([name, value]) => ({ name, value })),
|
||||
},
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [keys, setKeys] = createSignal<1[]>(
|
||||
new Array(items().length || 1).fill(1),
|
||||
);
|
||||
|
||||
const handleSubmit = async (values: AdminSettings) => {
|
||||
console.log("submitting", values, getValues(formStore));
|
||||
|
||||
const r = await callApi("set_admin_service", {
|
||||
base_url: props.base_url,
|
||||
allowed_keys: values.allowedKeys.reduce(
|
||||
(acc, curr) => ({ ...acc, [curr.name]: curr.value }),
|
||||
{},
|
||||
),
|
||||
});
|
||||
if (r.status === "success") {
|
||||
toast.success("Successfully updated admin settings");
|
||||
}
|
||||
if (r.status === "error") {
|
||||
toast.error(`Failed to update admin settings: ${r.errors[0].message}`);
|
||||
}
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [props.base_url, "get_admin_service"],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<div class="card-body">
|
||||
<span class="text-xl text-primary">Administration</span>
|
||||
<div class="grid grid-cols-12 gap-2">
|
||||
<span class="col-span-12 text-lg text-neutral">
|
||||
Each of the following keys can be used to authenticate on machines
|
||||
</span>
|
||||
<For each={keys()}>
|
||||
{(name, idx) => (
|
||||
<>
|
||||
<Field name={`allowedKeys.${idx()}.name`}>
|
||||
{(field, props) => (
|
||||
<TextInput
|
||||
formStore={formStore}
|
||||
inputProps={props}
|
||||
label={"Name"}
|
||||
adornment={{
|
||||
position: "start",
|
||||
content: (
|
||||
<span class="material-icons text-gray-400">key</span>
|
||||
),
|
||||
}}
|
||||
value={field.value ?? ""}
|
||||
error={field.error}
|
||||
class="col-span-4"
|
||||
required
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Field name={`allowedKeys.${idx()}.value`}>
|
||||
{(field, props) => (
|
||||
<>
|
||||
<TextInput
|
||||
formStore={formStore}
|
||||
inputProps={props}
|
||||
label={"Value"}
|
||||
value={field.value ?? ""}
|
||||
error={field.error}
|
||||
class="col-span-6"
|
||||
required
|
||||
/>
|
||||
<span class="tooltip mt-auto" data-tip="Select file">
|
||||
<label
|
||||
class={"form-control w-full"}
|
||||
aria-disabled={formStore.submitting}
|
||||
>
|
||||
<div class="btn btn-secondary relative flex items-center justify-center">
|
||||
<input
|
||||
value=""
|
||||
// Disable drag n drop
|
||||
onDrop={(e) => e.preventDefault()}
|
||||
class="absolute -ml-4 size-full cursor-pointer opacity-0"
|
||||
type="file"
|
||||
onInput={async (e) => {
|
||||
console.log(e.target.files);
|
||||
if (!e.target.files) return;
|
||||
|
||||
const content = await e.target.files[0].text();
|
||||
console.log(content);
|
||||
setValue(
|
||||
formStore,
|
||||
`allowedKeys.${idx()}.value`,
|
||||
content,
|
||||
);
|
||||
if (
|
||||
!getValue(
|
||||
formStore,
|
||||
`allowedKeys.${idx()}.name`,
|
||||
)
|
||||
) {
|
||||
setValue(
|
||||
formStore,
|
||||
`allowedKeys.${idx()}.name`,
|
||||
e.target.files[0].name,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span class="material-icons">file_open</span>
|
||||
</div>
|
||||
</label>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
<button
|
||||
class="btn btn-ghost col-span-1 self-end"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setKeys((c) => c.filter((_, i) => i !== idx()));
|
||||
setValue(formStore, `allowedKeys.${idx()}.name`, "");
|
||||
setValue(formStore, `allowedKeys.${idx()}.value`, "");
|
||||
}}
|
||||
>
|
||||
<span class="material-icons">delete</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
<div class="my-2 flex w-full gap-2">
|
||||
<button
|
||||
class="btn btn-square btn-ghost"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setKeys((c) => [...c, 1]);
|
||||
}}
|
||||
>
|
||||
<span class="material-icons">add</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
<div class="card-actions justify-end">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="submit"
|
||||
disabled={formStore.submitting || !formStore.dirty}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
type GeneralData = SuccessQuery<"show_clan_meta">["data"];
|
||||
type AdminData = SuccessQuery<"get_admin_service">["data"];
|
||||
|
||||
export const ClanDetails = () => {
|
||||
const params = useParams();
|
||||
const clan_dir = window.atob(params.id);
|
||||
// Fetch general meta data
|
||||
const clanQuery = createQuery(() => ({
|
||||
queryKey: [clan_dir, "meta"],
|
||||
queryFn: async () => {
|
||||
const result = await callApi("show_clan_meta", { uri: clan_dir });
|
||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||
return result.data;
|
||||
},
|
||||
}));
|
||||
// Fetch admin settings
|
||||
const adminQuery = createQuery(() => ({
|
||||
queryKey: [clan_dir, "get_admin_service"],
|
||||
queryFn: async () => {
|
||||
const result = await callApi("get_admin_service", {
|
||||
base_url: clan_dir,
|
||||
});
|
||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||
return result.data || null;
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<div class="card card-normal">
|
||||
<BackButton />
|
||||
<Show
|
||||
when={!adminQuery.isLoading}
|
||||
fallback={
|
||||
<div>
|
||||
<span class="loading loading-lg"></span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={clanQuery.data}>
|
||||
{(d) => <EditClanForm initial={d()} directory={clan_dir} />}
|
||||
</Match>
|
||||
</Switch>
|
||||
</Show>
|
||||
<div class="divider"></div>
|
||||
<Show
|
||||
when={!adminQuery.isLoading}
|
||||
fallback={
|
||||
<div>
|
||||
<span class="loading loading-lg"></span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={adminQuery.data}>
|
||||
{(d) => <AdminModuleForm admin={d()} base_url={clan_dir} />}
|
||||
</Match>
|
||||
</Switch>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
3
pkgs/webview-ui/app/src/routes/clans/index.ts
Normal file
3
pkgs/webview-ui/app/src/routes/clans/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./list";
|
||||
export * from "./create";
|
||||
export * from "./details";
|
||||
@@ -4,39 +4,14 @@ import { createSignal, For, Match, Setter, Show, Switch } from "solid-js";
|
||||
import { createQuery } from "@tanstack/solid-query";
|
||||
import { useFloating } from "@/src/floating";
|
||||
import { autoUpdate, flip, hide, offset, shift } from "@floating-ui/dom";
|
||||
import { EditClanForm } from "../clan/editClan";
|
||||
import { useNavigate, A } from "@solidjs/router";
|
||||
import { fileURLToPath } from "url";
|
||||
import { registerClan } from "@/src/hooks";
|
||||
|
||||
export const registerClan = async () => {
|
||||
try {
|
||||
const loc = await callApi("open_file", {
|
||||
file_request: { mode: "select_folder" },
|
||||
});
|
||||
if (loc.status === "success" && loc.data) {
|
||||
const data = loc.data[0];
|
||||
setClanList((s) => {
|
||||
const res = new Set([...s, data]);
|
||||
return Array.from(res);
|
||||
});
|
||||
setActiveURI(data);
|
||||
// setRoute((r) => {
|
||||
// if (r === "welcome") return "machines";
|
||||
// return r;
|
||||
// });
|
||||
return data;
|
||||
}
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
interface ClanDetailsProps {
|
||||
interface ClanItemProps {
|
||||
clan_dir: string;
|
||||
setEditURI: Setter<string | null>;
|
||||
}
|
||||
const ClanDetails = (props: ClanDetailsProps) => {
|
||||
const { clan_dir, setEditURI } = props;
|
||||
const ClanItem = (props: ClanItemProps) => {
|
||||
const { clan_dir } = props;
|
||||
|
||||
const details = createQuery(() => ({
|
||||
queryKey: [clan_dir, "meta"],
|
||||
@@ -46,7 +21,7 @@ const ClanDetails = (props: ClanDetailsProps) => {
|
||||
return result.data;
|
||||
},
|
||||
}));
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [reference, setReference] = createSignal<HTMLElement>();
|
||||
const [floating, setFloating] = createSignal<HTMLElement>();
|
||||
|
||||
@@ -87,10 +62,8 @@ const ClanDetails = (props: ClanDetailsProps) => {
|
||||
<div class="stat-figure text-primary">
|
||||
<div class="join">
|
||||
<button
|
||||
class=" join-item btn-sm"
|
||||
onClick={() => {
|
||||
setEditURI(clan_dir);
|
||||
}}
|
||||
class="join-item btn-sm"
|
||||
onClick={() => navigate(`/clans/${window.btoa(clan_dir)}`)}
|
||||
>
|
||||
<span class="material-icons">edit</span>
|
||||
</button>
|
||||
@@ -157,37 +130,16 @@ const ClanDetails = (props: ClanDetailsProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Settings = () => {
|
||||
const [editURI, setEditURI] = createSignal<string | null>(null);
|
||||
|
||||
export const ClanList = () => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div class="card card-normal">
|
||||
<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 text-2xl text-neutral">
|
||||
Registered Clans
|
||||
</div>
|
||||
<div class="label-text text-2xl text-neutral">Registered Clans</div>
|
||||
<div class="flex gap-2">
|
||||
<span class="tooltip tooltip-top" data-tip="Register clan">
|
||||
<button
|
||||
class="btn btn-square btn-ghost"
|
||||
onClick={() => {
|
||||
registerClan();
|
||||
}}
|
||||
>
|
||||
<button class="btn btn-square btn-ghost" onClick={registerClan}>
|
||||
<span class="material-icons">post_add</span>
|
||||
</button>
|
||||
</span>
|
||||
@@ -205,14 +157,10 @@ export const Settings = () => {
|
||||
</div>
|
||||
<div class="stats stats-vertical shadow">
|
||||
<For each={clanList()}>
|
||||
{(value) => (
|
||||
<ClanDetails clan_dir={value} setEditURI={setEditURI} />
|
||||
)}
|
||||
{(value) => <ClanItem clan_dir={value} />}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { setActiveURI, setClanList } from "@/src/App";
|
||||
import { registerClan } from "../settings";
|
||||
import { setActiveURI } from "@/src/App";
|
||||
import { registerClan } from "@/src/hooks";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
|
||||
export const Welcome = () => {
|
||||
|
||||
Reference in New Issue
Block a user