feat: show project and session translation tables

This commit is contained in:
muena
2026-09-28 15:05:01 +02:00
parent ec1f9a4e55
commit 7bc0142f95
4 changed files with 242 additions and 0 deletions

View File

@@ -0,0 +1,224 @@
import {
Action,
ActionPanel,
Detail,
Icon,
List,
showToast,
Toast,
} from "@raycast/api";
import { useEffect, useState } from "react";
import { canonicalProjectPath, listAllRuns } from "./lib/velum-project";
import { listSessions } from "./sessions";
import type { PlaceholderMapping } from "./types";
import { markdownCodeBlock } from "./ui";
type TableSource = {
id: string;
title: string;
subtitle: string;
mapping: PlaceholderMapping;
};
export function TranslationTable({
title,
mapping,
}: {
title: string;
mapping: PlaceholderMapping;
}) {
const groups = new Map<string, Array<[string, PlaceholderMapping[string]]>>();
for (const entry of Object.entries(mapping).sort(([a], [b]) =>
a.localeCompare(b),
)) {
const type = entry[1].type || "Sonstige";
const entries = groups.get(type) ?? [];
entries.push(entry);
groups.set(type, entries);
}
return (
<List
navigationTitle={`Übersetzungstabelle · ${title}`}
searchBarPlaceholder="Platzhalter oder Klartext suchen"
>
<List.EmptyView
title="Keine Zuordnungen"
description="Für diesen Stand sind noch keine Übersetzungen gespeichert."
/>
{[...groups]
.sort(([a], [b]) => a.localeCompare(b))
.map(([type, entries]) => (
<List.Section key={type} title={`${type} · ${entries.length}`}>
{entries.map(([placeholder, entry]) => (
<List.Item
key={placeholder}
icon={Icon.ArrowRight}
title={placeholder}
subtitle={entry.original}
keywords={[entry.original, entry.type, entry.merged_into ?? ""]}
accessories={
entry.merged_into ? [{ text: `→ ${entry.merged_into}` }] : []
}
actions={
<ActionPanel>
<Action.Push
title="Eintrag vollständig anzeigen"
icon={Icon.Eye}
target={
<Detail
navigationTitle={placeholder}
markdown={[
`# ${entry.type}`,
"",
"Platzhalter",
markdownCodeBlock(placeholder),
"",
"Klartext",
markdownCodeBlock(entry.original),
...(entry.merged_into
? [
"",
"Zusammengeführt mit",
markdownCodeBlock(entry.merged_into),
]
: []),
].join("\n")}
actions={
<ActionPanel>
<Action.CopyToClipboard
title="Platzhalter kopieren"
content={placeholder}
/>
<Action.CopyToClipboard
title="Klartext kopieren"
content={entry.original}
/>
</ActionPanel>
}
/>
}
/>
<Action.CopyToClipboard
title="Platzhalter kopieren"
content={placeholder}
/>
<Action.CopyToClipboard
title="Klartext kopieren"
content={entry.original}
/>
</ActionPanel>
}
/>
))}
</List.Section>
))}
</List>
);
}
export default function Command() {
const [projects, setProjects] = useState<TableSource[]>([]);
const [sessions, setSessions] = useState<TableSource[]>([]);
const [busy, setBusy] = useState(true);
useEffect(() => {
Promise.all([listAllRuns(), listSessions()])
.then(([runs, savedSessions]) => {
const latest = new Map<string, TableSource>();
for (const { run } of runs) {
const path = canonicalProjectPath(run.projectPath);
if (!latest.has(path))
latest.set(path, {
id: path,
title: run.projectName,
subtitle: `${path} · ${new Date(run.createdAt).toLocaleString("de-AT")}`,
mapping: run.mapping,
});
}
setProjects(
[...latest.values()].sort((a, b) => a.title.localeCompare(b.title)),
);
setSessions(
savedSessions.map((session) => ({
id: session.id,
title: session.name,
subtitle: `${Object.keys(session.mapping).length} Zuordnungen · ${new Date(session.updatedAt).toLocaleString("de-AT")}`,
mapping: session.mapping,
})),
);
})
.catch((error: Error) =>
showToast({
style: Toast.Style.Failure,
title: "Übersetzungstabellen nicht lesbar",
message: error.message,
}),
)
.finally(() => setBusy(false));
}, []);
return (
<List
isLoading={busy}
navigationTitle="Übersetzungstabellen"
searchBarPlaceholder="Projekt oder Sitzung suchen"
>
<List.EmptyView
title="Keine Tabellen"
description="Pseudonymisiere zuerst Text oder Projektdateien."
/>
<List.Section title="Projekte">
{projects.map((source) => (
<List.Item
key={source.id}
icon={Icon.Folder}
title={source.title}
subtitle={source.subtitle}
accessories={[
{ text: `${Object.keys(source.mapping).length} Zuordnungen` },
]}
actions={
<ActionPanel>
<Action.Push
title="Übersetzungstabelle anzeigen"
icon={Icon.List}
target={
<TranslationTable
title={source.title}
mapping={source.mapping}
/>
}
/>
</ActionPanel>
}
/>
))}
</List.Section>
<List.Section title="Sitzungen">
{sessions.map((source) => (
<List.Item
key={source.id}
icon={Icon.Person}
title={source.title}
subtitle={source.subtitle}
actions={
<ActionPanel>
<Action.Push
title="Übersetzungstabelle anzeigen"
icon={Icon.List}
target={
<TranslationTable
title={source.title}
mapping={source.mapping}
/>
}
/>
</ActionPanel>
}
/>
))}
</List.Section>
</List>
);
}