forked from clan/clan-core
Webview: init 'open clan' workflow
This commit is contained in:
parent
be868ee107
commit
913ab4627c
@ -29,7 +29,6 @@ let
|
||||
++ lib.optional (synapseCfg.settings ? user_directory) "user-search"
|
||||
++ lib.optional (synapseCfg.settings.url_preview_enabled) "url-preview"
|
||||
++ lib.optional (synapseCfg.settings.database.name == "psycopg2") "postgres";
|
||||
|
||||
in
|
||||
{
|
||||
options.services.matrix-synapse.package = lib.mkOption { readOnly = false; };
|
||||
|
@ -71,6 +71,19 @@ def open_file(file_request: FileRequest) -> str | None:
|
||||
finally:
|
||||
main_loop.quit()
|
||||
|
||||
def on_save_finish(
|
||||
file_dialog: Gtk.FileDialog, task: Gio.Task, main_loop: GLib.MainLoop
|
||||
) -> None:
|
||||
try:
|
||||
gfile = file_dialog.save_finish(task)
|
||||
if gfile:
|
||||
nonlocal selected_path
|
||||
selected_path = gfile.get_path()
|
||||
except Exception as e:
|
||||
print(f"Error getting selected file: {e}")
|
||||
finally:
|
||||
main_loop.quit()
|
||||
|
||||
dialog = Gtk.FileDialog()
|
||||
|
||||
if file_request.title:
|
||||
@ -105,12 +118,16 @@ def open_file(file_request: FileRequest) -> str | None:
|
||||
# if select_folder
|
||||
if file_request.mode == "select_folder":
|
||||
dialog.select_folder(
|
||||
callback=lambda dialog, task: on_folder_select(dialog, task, main_loop)
|
||||
callback=lambda dialog, task: on_folder_select(dialog, task, main_loop),
|
||||
)
|
||||
elif file_request.mode == "open_file":
|
||||
dialog.open(
|
||||
callback=lambda dialog, task: on_file_select(dialog, task, main_loop)
|
||||
)
|
||||
elif file_request.mode == "save":
|
||||
dialog.save(
|
||||
callback=lambda dialog, task: on_save_finish(dialog, task, main_loop)
|
||||
)
|
||||
|
||||
# Wait for the user to select a file or directory
|
||||
main_loop.run() # type: ignore
|
||||
|
@ -19,7 +19,7 @@ class FileFilter:
|
||||
@dataclass
|
||||
class FileRequest:
|
||||
# Mode of the os dialog window
|
||||
mode: Literal["open_file", "select_folder"]
|
||||
mode: Literal["open_file", "select_folder", "save"]
|
||||
# Title of the os dialog window
|
||||
title: str | None = None
|
||||
# Pre-applied filters for the file dialog
|
||||
|
@ -1,6 +1,7 @@
|
||||
# !/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass, fields
|
||||
from pathlib import Path
|
||||
|
||||
@ -47,10 +48,15 @@ def create_clan(options: CreateOptions) -> CreateClanResponse:
|
||||
if not directory.exists():
|
||||
directory.mkdir()
|
||||
else:
|
||||
# Directory already exists
|
||||
# Check if it is empty
|
||||
# Throw error otherwise
|
||||
dir_content = os.listdir(directory)
|
||||
if len(dir_content) != 0:
|
||||
raise ClanError(
|
||||
location=f"{directory.resolve()}",
|
||||
msg="Cannot create clan",
|
||||
description="Directory already exists",
|
||||
description="Directory already exists and is not empty.",
|
||||
)
|
||||
|
||||
cmd_responses = {}
|
||||
|
@ -6,10 +6,19 @@ const faker = JSONSchemaFaker;
|
||||
|
||||
faker.option({
|
||||
alwaysFakeOptionals: true,
|
||||
useExamplesValue: true,
|
||||
});
|
||||
|
||||
const getFakeResponse = (method: OperationNames, data: any) => {
|
||||
const fakeData = faker.generate(schema.properties[method].properties.return);
|
||||
|
||||
if (method === "open_file") {
|
||||
return {
|
||||
status: "success",
|
||||
data: "/path/to/clan",
|
||||
};
|
||||
}
|
||||
|
||||
return fakeData;
|
||||
};
|
||||
|
||||
|
@ -1,10 +1,16 @@
|
||||
import { Accessor, For, Match, Switch } from "solid-js";
|
||||
import { MachineListView } from "./routes/machines/view";
|
||||
import { colors } from "./routes/colors/view";
|
||||
import { clan } from "./routes/clan/view";
|
||||
|
||||
export type Route = keyof typeof routes;
|
||||
|
||||
export const routes = {
|
||||
clan: {
|
||||
child: clan,
|
||||
label: "Clan",
|
||||
icon: "groups",
|
||||
},
|
||||
machines: {
|
||||
child: MachineListView,
|
||||
label: "Machines",
|
||||
|
@ -87,11 +87,4 @@ operationNames.forEach((opName) => {
|
||||
pyApi[name] = createFunctions(name);
|
||||
});
|
||||
|
||||
pyApi.open_file.receive((r) => {
|
||||
const { status } = r;
|
||||
if (status === "error") return console.error(r.errors);
|
||||
console.log(r.data);
|
||||
alert(r.data);
|
||||
});
|
||||
|
||||
export { pyApi };
|
||||
|
161
pkgs/webview-ui/app/src/routes/clan/clanDetails.tsx
Normal file
161
pkgs/webview-ui/app/src/routes/clan/clanDetails.tsx
Normal file
@ -0,0 +1,161 @@
|
||||
import { OperationResponse, pyApi } from "@/src/api";
|
||||
import {
|
||||
For,
|
||||
JSX,
|
||||
Match,
|
||||
Show,
|
||||
Switch,
|
||||
createEffect,
|
||||
createSignal,
|
||||
} from "solid-js";
|
||||
import cx from "classnames";
|
||||
|
||||
interface ClanDetailsProps {
|
||||
directory: string;
|
||||
}
|
||||
|
||||
interface MetaFieldsProps {
|
||||
meta: ClanMeta;
|
||||
actions: JSX.Element;
|
||||
editable?: boolean;
|
||||
directory?: string;
|
||||
}
|
||||
|
||||
const fn = (e: SubmitEvent) => {
|
||||
e.preventDefault();
|
||||
console.log(e.currentTarget);
|
||||
};
|
||||
|
||||
export const EditMetaFields = (props: MetaFieldsProps) => {
|
||||
const { meta, editable, actions, directory } = props;
|
||||
|
||||
const [editing, setEditing] = createSignal<
|
||||
keyof MetaFieldsProps["meta"] | null
|
||||
>(null);
|
||||
return (
|
||||
<div class="card card-compact w-96 bg-base-100 shadow-xl">
|
||||
<figure>
|
||||
<img
|
||||
src="https://www.shutterstock.com/image-vector/modern-professional-ninja-mascot-logo-260nw-1729854862.jpg"
|
||||
alt="Clan Logo"
|
||||
/>
|
||||
</figure>
|
||||
<div class="card-body">
|
||||
<form onSubmit={fn}>
|
||||
<h2 class="card-title justify-between">
|
||||
<input
|
||||
classList={{
|
||||
[cx("text-slate-600")]: editing() !== "name",
|
||||
}}
|
||||
readOnly={editing() !== "name"}
|
||||
class="w-full"
|
||||
autofocus
|
||||
onBlur={() => setEditing(null)}
|
||||
type="text"
|
||||
value={meta?.name}
|
||||
onInput={(e) => {
|
||||
console.log(e.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
<Show when={editable}>
|
||||
<button class="btn btn-square btn-ghost btn-sm">
|
||||
<span
|
||||
class="material-icons"
|
||||
onClick={() => {
|
||||
if (editing() !== "name") setEditing("name");
|
||||
else {
|
||||
setEditing(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Show when={editing() !== "name"} fallback="check">
|
||||
edit
|
||||
</Show>
|
||||
</span>
|
||||
</button>
|
||||
</Show>
|
||||
</h2>
|
||||
<div class="flex gap-1 align-middle leading-8">
|
||||
<i class="material-icons">description</i>
|
||||
<span>{meta?.description || "No description"}</span>
|
||||
</div>
|
||||
<Show when={directory}>
|
||||
<div class="flex gap-1 align-middle leading-8">
|
||||
<i class="material-icons">folder</i>
|
||||
<span>{directory}</span>
|
||||
</div>
|
||||
</Show>
|
||||
{actions}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type ClanMeta = Extract<
|
||||
OperationResponse<"show_clan_meta">,
|
||||
{ status: "success" }
|
||||
>["data"];
|
||||
|
||||
export const ClanDetails = (props: ClanDetailsProps) => {
|
||||
const { directory } = props;
|
||||
const [, setLoading] = createSignal(false);
|
||||
const [errors, setErrors] = createSignal<
|
||||
| Extract<
|
||||
OperationResponse<"show_clan_meta">,
|
||||
{ status: "error" }
|
||||
>["errors"]
|
||||
| null
|
||||
>(null);
|
||||
const [data, setData] = createSignal<ClanMeta>();
|
||||
|
||||
const loadMeta = () => {
|
||||
pyApi.show_clan_meta.dispatch({ uri: directory });
|
||||
setLoading(true);
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
loadMeta();
|
||||
pyApi.show_clan_meta.receive((response) => {
|
||||
setLoading(false);
|
||||
if (response.status === "error") {
|
||||
setErrors(response.errors);
|
||||
return console.error(response.errors);
|
||||
}
|
||||
setData(response.data);
|
||||
});
|
||||
});
|
||||
return (
|
||||
<Switch fallback={"loading"}>
|
||||
<Match when={data()}>
|
||||
<EditMetaFields
|
||||
directory={directory}
|
||||
// @ts-expect-error: TODO: figure out how solid allows type narrowing this
|
||||
meta={data()}
|
||||
actions={
|
||||
<div class="card-actions justify-between">
|
||||
<button class="btn btn-link" onClick={() => loadMeta()}>
|
||||
Refresh
|
||||
</button>
|
||||
<button class="btn btn-primary">Open</button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={errors()}>
|
||||
<button class="btn btn-link" onClick={() => loadMeta()}>
|
||||
Retry
|
||||
</button>
|
||||
<For each={errors()}>
|
||||
{(item) => (
|
||||
<div class="flex flex-col gap-3">
|
||||
<span class="bg-red-400 text-white">{item.message}</span>
|
||||
<span class="bg-red-400 text-white">{item.description}</span>
|
||||
<span class="bg-red-400 text-white">{item.location}</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
};
|
82
pkgs/webview-ui/app/src/routes/clan/view.tsx
Normal file
82
pkgs/webview-ui/app/src/routes/clan/view.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import { pyApi } from "@/src/api";
|
||||
import { Match, Switch, createEffect, createSignal } from "solid-js";
|
||||
import toast from "solid-toast";
|
||||
import { ClanDetails, EditMetaFields } from "./clanDetails";
|
||||
|
||||
export const clan = () => {
|
||||
const [mode, setMode] = createSignal<"init" | "open" | "create">("init");
|
||||
const [clanDir, setClanDir] = createSignal<string | null>(null);
|
||||
|
||||
createEffect(() => {
|
||||
console.log(mode());
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<Switch fallback={"invalid"}>
|
||||
<Match when={mode() === "init"}>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-square" onclick={() => setMode("create")}>
|
||||
<span class="material-icons">add</span>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-square"
|
||||
onclick={() => {
|
||||
pyApi.open_file.dispatch({
|
||||
file_request: {
|
||||
mode: "select_folder",
|
||||
title: "Open Clan",
|
||||
},
|
||||
});
|
||||
pyApi.open_file.receive((r) => {
|
||||
// There are two error cases to handle
|
||||
if (r.status !== "success") {
|
||||
console.error(r.errors);
|
||||
toast.error("Error opening clan");
|
||||
return;
|
||||
}
|
||||
// User didn't select anything
|
||||
if (!r.data) {
|
||||
setMode("init");
|
||||
return;
|
||||
}
|
||||
|
||||
setClanDir(r.data);
|
||||
setMode("open");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span class="material-icons">folder_open</span>
|
||||
</button>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={mode() === "open"}>
|
||||
<ClanDetails directory={clanDir() || ""} />
|
||||
</Match>
|
||||
<Match when={mode() === "create"}>
|
||||
<EditMetaFields
|
||||
actions={
|
||||
<div class="card-actions justify-end">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
onClick={() => {
|
||||
pyApi.open_file.dispatch({
|
||||
file_request: { mode: "save" },
|
||||
});
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
meta={{
|
||||
name: "New Clan",
|
||||
description: "nice description",
|
||||
icon: "select icon",
|
||||
}}
|
||||
editable
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user