feat(journal): delete sessions from the ui
Add a confirmation dialog that spells out the removal is irreversible, and expose delete from the GM session dropdown and the lobby session list.
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
|||||||
setMyRole,
|
setMyRole,
|
||||||
useJournalStream,
|
useJournalStream,
|
||||||
} from "../stores/journalStream";
|
} from "../stores/journalStream";
|
||||||
|
import { DeleteSessionDialog } from "./DeleteSessionDialog";
|
||||||
|
|
||||||
export const ConnectDialog: Component = () => {
|
export const ConnectDialog: Component = () => {
|
||||||
const stream = useJournalStream();
|
const stream = useJournalStream();
|
||||||
@@ -28,6 +29,10 @@ export const ConnectDialog: Component = () => {
|
|||||||
const [busy, setBusy] = createSignal(false);
|
const [busy, setBusy] = createSignal(false);
|
||||||
const [error, setError] = createSignal<string | null>(null);
|
const [error, setError] = createSignal<string | null>(null);
|
||||||
const [autoJoined, setAutoJoined] = createSignal(false);
|
const [autoJoined, setAutoJoined] = createSignal(false);
|
||||||
|
const [sessionToDelete, setSessionToDelete] = createSignal<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
const ensureBroker = async () => {
|
const ensureBroker = async () => {
|
||||||
if (stream.connected) return;
|
if (stream.connected) return;
|
||||||
@@ -145,13 +150,27 @@ export const ConnectDialog: Component = () => {
|
|||||||
<div class="max-h-40 overflow-y-auto border border-gray-200 rounded divide-y divide-gray-100">
|
<div class="max-h-40 overflow-y-auto border border-gray-200 rounded divide-y divide-gray-100">
|
||||||
<For each={Object.entries(manifest().sessions)}>
|
<For each={Object.entries(manifest().sessions)}>
|
||||||
{([id, meta]) => (
|
{([id, meta]) => (
|
||||||
<button
|
<div class="flex items-center">
|
||||||
onClick={() => handleJoin(id)}
|
<button
|
||||||
disabled={busy()}
|
onClick={() => handleJoin(id)}
|
||||||
class="w-full text-left px-2 py-1.5 text-xs text-gray-700 hover:bg-gray-50 disabled:opacity-50"
|
disabled={busy()}
|
||||||
>
|
class="flex-1 min-w-0 text-left px-2 py-1.5 text-xs text-gray-700 hover:bg-gray-50 disabled:opacity-50 truncate"
|
||||||
{meta.name || id}
|
>
|
||||||
</button>
|
{meta.name || id}
|
||||||
|
</button>
|
||||||
|
<Show when={role() === "gm"}>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setSessionToDelete({ id, name: meta.name || id })
|
||||||
|
}
|
||||||
|
disabled={busy()}
|
||||||
|
class="shrink-0 px-2 py-1.5 text-[10px] text-gray-300 hover:text-red-500 disabled:opacity-50"
|
||||||
|
title="删除会话"
|
||||||
|
>
|
||||||
|
🗑
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
<Show when={Object.keys(manifest().sessions).length === 0}>
|
<Show when={Object.keys(manifest().sessions).length === 0}>
|
||||||
@@ -187,6 +206,17 @@ export const ConnectDialog: Component = () => {
|
|||||||
<Show when={error()}>
|
<Show when={error()}>
|
||||||
<p class="text-red-500 text-sm text-center">{error()}</p>
|
<p class="text-red-500 text-sm text-center">{error()}</p>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
<Show when={sessionToDelete()}>
|
||||||
|
{(target) => (
|
||||||
|
<DeleteSessionDialog
|
||||||
|
sessionId={target().id}
|
||||||
|
sessionName={target().name}
|
||||||
|
active={stream.sessionId === target().id}
|
||||||
|
onClose={() => setSessionToDelete(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/**
|
||||||
|
* DeleteSessionDialog — destructive confirmation before removing a session.
|
||||||
|
*
|
||||||
|
* Deletion removes the session directory on the server (meta + full message
|
||||||
|
* log), so the warning spells out that it cannot be undone.
|
||||||
|
*/
|
||||||
|
import { Component, createSignal, Show } from "solid-js";
|
||||||
|
import { deleteSession } from "../stores/journalStream";
|
||||||
|
|
||||||
|
interface DeleteSessionDialogProps {
|
||||||
|
sessionId: string;
|
||||||
|
sessionName: string;
|
||||||
|
/** Whether this is the session the user is currently in. */
|
||||||
|
active: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteSessionDialog: Component<DeleteSessionDialogProps> = (
|
||||||
|
props,
|
||||||
|
) => {
|
||||||
|
const [deleting, setDeleting] = createSignal(false);
|
||||||
|
const [error, setError] = createSignal<string | null>(null);
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
setDeleting(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await deleteSession(props.sessionId);
|
||||||
|
props.onClose();
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : "删除失败");
|
||||||
|
} finally {
|
||||||
|
setDeleting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 z-[60] flex items-center justify-center bg-black/20"
|
||||||
|
onClick={props.onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-white rounded-lg border border-gray-200 shadow-xl w-[300px] p-4 space-y-3"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-sm font-semibold text-red-600">删除会话</h3>
|
||||||
|
<button
|
||||||
|
onClick={props.onClose}
|
||||||
|
class="text-gray-400 hover:text-gray-600 text-sm"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-600">
|
||||||
|
确定要删除会话{" "}
|
||||||
|
<span class="font-semibold">{props.sessionName}</span>
|
||||||
|
(<span class="font-mono">{props.sessionId}</span>)吗?
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-red-500">
|
||||||
|
此操作不可恢复:该会话的全部消息记录、玩家名单和派生状态都会从服务器删除。
|
||||||
|
{props.active && " 你当前正在该会话中,删除后将返回会话列表。"}
|
||||||
|
</p>
|
||||||
|
<Show when={error()}>
|
||||||
|
<p class="text-xs text-red-500">{error()}</p>
|
||||||
|
</Show>
|
||||||
|
<div class="flex justify-end gap-1">
|
||||||
|
<button
|
||||||
|
onClick={props.onClose}
|
||||||
|
class="px-3 py-1 text-xs text-gray-600 hover:text-gray-800"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleDelete}
|
||||||
|
disabled={deleting()}
|
||||||
|
class="bg-red-600 text-white rounded px-3 py-1 text-xs font-medium hover:bg-red-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{deleting() ? "删除中…" : "删除"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,6 +8,7 @@ import { SessionDropdown } from "./SessionDropdown";
|
|||||||
interface JournalHeaderProps {
|
interface JournalHeaderProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onCreateSession: () => void;
|
onCreateSession: () => void;
|
||||||
|
onDeleteSession: (id: string, name: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const JournalHeader: Component<JournalHeaderProps> = (props) => {
|
export const JournalHeader: Component<JournalHeaderProps> = (props) => {
|
||||||
@@ -54,7 +55,10 @@ export const JournalHeader: Component<JournalHeaderProps> = (props) => {
|
|||||||
</p>
|
</p>
|
||||||
{/* Clickable subtitle — session dropdown (GM only) */}
|
{/* Clickable subtitle — session dropdown (GM only) */}
|
||||||
<Show when={stream.myRole === "gm"}>
|
<Show when={stream.myRole === "gm"}>
|
||||||
<SessionDropdown onCreate={props.onCreateSession} />
|
<SessionDropdown
|
||||||
|
onCreate={props.onCreateSession}
|
||||||
|
onDelete={props.onDeleteSession}
|
||||||
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { JournalHeader } from "./JournalHeader";
|
|||||||
import { ConnectDialog } from "./ConnectDialog";
|
import { ConnectDialog } from "./ConnectDialog";
|
||||||
import { InviteDialog } from "./InviteDialog";
|
import { InviteDialog } from "./InviteDialog";
|
||||||
import { CreateSessionDialog } from "./CreateSessionDialog";
|
import { CreateSessionDialog } from "./CreateSessionDialog";
|
||||||
|
import { DeleteSessionDialog } from "./DeleteSessionDialog";
|
||||||
import { VariableView } from "./VariableView";
|
import { VariableView } from "./VariableView";
|
||||||
|
|
||||||
export interface JournalPanelProps {
|
export interface JournalPanelProps {
|
||||||
@@ -26,6 +27,10 @@ export const JournalPanel: Component<JournalPanelProps> = (props) => {
|
|||||||
const stream = useJournalStream();
|
const stream = useJournalStream();
|
||||||
const [showInvite, setShowInvite] = createSignal(false);
|
const [showInvite, setShowInvite] = createSignal(false);
|
||||||
const [showCreateSession, setShowCreateSession] = createSignal(false);
|
const [showCreateSession, setShowCreateSession] = createSignal(false);
|
||||||
|
const [sessionToDelete, setSessionToDelete] = createSignal<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
} | null>(null);
|
||||||
const [viewMode, setViewMode] = createSignal<"stream" | "variables">("stream");
|
const [viewMode, setViewMode] = createSignal<"stream" | "variables">("stream");
|
||||||
|
|
||||||
// Player list (exclude gm and observer)
|
// Player list (exclude gm and observer)
|
||||||
@@ -56,6 +61,7 @@ export const JournalPanel: Component<JournalPanelProps> = (props) => {
|
|||||||
<JournalHeader
|
<JournalHeader
|
||||||
onClose={props.onClose}
|
onClose={props.onClose}
|
||||||
onCreateSession={() => setShowCreateSession(true)}
|
onCreateSession={() => setShowCreateSession(true)}
|
||||||
|
onDeleteSession={(id, name) => setSessionToDelete({ id, name })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Player list tabs */}
|
{/* Player list tabs */}
|
||||||
@@ -126,6 +132,18 @@ export const JournalPanel: Component<JournalPanelProps> = (props) => {
|
|||||||
<Show when={showCreateSession()}>
|
<Show when={showCreateSession()}>
|
||||||
<CreateSessionDialog onClose={() => setShowCreateSession(false)} />
|
<CreateSessionDialog onClose={() => setShowCreateSession(false)} />
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
{/* Delete session modal */}
|
||||||
|
<Show when={sessionToDelete()}>
|
||||||
|
{(target) => (
|
||||||
|
<DeleteSessionDialog
|
||||||
|
sessionId={target().id}
|
||||||
|
sessionName={target().name}
|
||||||
|
active={stream.sessionId === target().id}
|
||||||
|
onClose={() => setSessionToDelete(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
</JournalContext.Provider>
|
</JournalContext.Provider>
|
||||||
</aside>
|
</aside>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
|
|
||||||
interface SessionDropdownProps {
|
interface SessionDropdownProps {
|
||||||
onCreate: () => void;
|
onCreate: () => void;
|
||||||
|
onDelete: (id: string, name: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SessionDropdown: Component<SessionDropdownProps> = (props) => {
|
export const SessionDropdown: Component<SessionDropdownProps> = (props) => {
|
||||||
@@ -54,22 +55,37 @@ export const SessionDropdown: Component<SessionDropdownProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<For each={Object.entries(manifest().sessions)}>
|
<For each={Object.entries(manifest().sessions)}>
|
||||||
{([id, meta]) => (
|
{([id, meta]) => (
|
||||||
<button
|
<div
|
||||||
onClick={(e) => {
|
class={`flex items-center ${
|
||||||
e.stopPropagation();
|
id === stream.sessionId ? "bg-blue-50" : ""
|
||||||
handleSessionSelect(id);
|
|
||||||
}}
|
|
||||||
class={`w-full text-left px-3 py-1 text-xs hover:bg-gray-100 flex items-center gap-1 ${
|
|
||||||
id === stream.sessionId
|
|
||||||
? "bg-blue-50 text-blue-700"
|
|
||||||
: "text-gray-700"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span class="truncate flex-1">{meta.name || id}</span>
|
<button
|
||||||
<Show when={id === stream.sessionId}>
|
onClick={(e) => {
|
||||||
<span class="text-blue-500 shrink-0">✓</span>
|
e.stopPropagation();
|
||||||
</Show>
|
handleSessionSelect(id);
|
||||||
</button>
|
}}
|
||||||
|
class={`flex-1 min-w-0 text-left px-3 py-1 text-xs hover:bg-gray-100 flex items-center gap-1 ${
|
||||||
|
id === stream.sessionId ? "text-blue-700" : "text-gray-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span class="truncate flex-1">{meta.name || id}</span>
|
||||||
|
<Show when={id === stream.sessionId}>
|
||||||
|
<span class="text-blue-500 shrink-0">✓</span>
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setDropdownOpen(false);
|
||||||
|
props.onDelete(id, meta.name || id);
|
||||||
|
}}
|
||||||
|
class="shrink-0 px-2 py-1 text-[10px] text-gray-300 hover:text-red-500"
|
||||||
|
title="删除会话"
|
||||||
|
>
|
||||||
|
🗑
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
<div class="border-t border-gray-100 my-0.5" />
|
<div class="border-t border-gray-100 my-0.5" />
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export { ConnectDialog } from "./ConnectDialog";
|
|||||||
export { InviteDialog } from "./InviteDialog";
|
export { InviteDialog } from "./InviteDialog";
|
||||||
export { SessionDropdown } from "./SessionDropdown";
|
export { SessionDropdown } from "./SessionDropdown";
|
||||||
export { CreateSessionDialog } from "./CreateSessionDialog";
|
export { CreateSessionDialog } from "./CreateSessionDialog";
|
||||||
|
export { DeleteSessionDialog } from "./DeleteSessionDialog";
|
||||||
export { StreamView } from "./StreamView";
|
export { StreamView } from "./StreamView";
|
||||||
export { StreamMessageCard } from "./StreamMessage";
|
export { StreamMessageCard } from "./StreamMessage";
|
||||||
export { ComposePanel } from "./ComposePanel";
|
export { ComposePanel } from "./ComposePanel";
|
||||||
|
|||||||
Reference in New Issue
Block a user