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

@@ -89,6 +89,12 @@
"description": "Zwischenablage übernehmen, Projekt oder Sitzung wählen und wiederherstellen.", "description": "Zwischenablage übernehmen, Projekt oder Sitzung wählen und wiederherstellen.",
"mode": "view" "mode": "view"
}, },
{
"name": "show-translation-tables",
"title": "Übersetzungstabellen anzeigen",
"description": "Gespeicherte Zuordnungen je Projekt oder Sitzung durchsuchen.",
"mode": "view"
},
{ {
"name": "manage-sessions", "name": "manage-sessions",
"title": "Sitzungen verwalten", "title": "Sitzungen verwalten",

View File

@@ -25,6 +25,7 @@ import {
} from "./sessions"; } from "./sessions";
import type { VelumSession } from "./types"; import type { VelumSession } from "./types";
import { mappingSummary, sessionSubtitle } from "./ui"; import { mappingSummary, sessionSubtitle } from "./ui";
import { TranslationTable } from "./show-translation-tables";
export default function Command() { export default function Command() {
const [sessions, setSessions] = useState<VelumSession[]>([]); const [sessions, setSessions] = useState<VelumSession[]>([]);
@@ -145,6 +146,11 @@ export default function Command() {
accessories={[{ text: `${countMappingEntries(session.mapping)}` }]} accessories={[{ text: `${countMappingEntries(session.mapping)}` }]}
actions={ actions={
<ActionPanel> <ActionPanel>
<Action.Push
title="Übersetzungstabelle anzeigen"
icon={Icon.List}
target={<TranslationTable title={session.name} mapping={session.mapping} />}
/>
<Action <Action
title="Als Aktive Sitzung Setzen" title="Als Aktive Sitzung Setzen"
icon={Icon.CheckCircle} icon={Icon.CheckCircle}

View File

@@ -8,6 +8,7 @@ import { canonicalProjectPath, listAllRuns, resolveWithin, restorePath, restoreT
import type { ProjectFile, ProjectRun } from "./lib/velum-project"; import type { ProjectFile, ProjectRun } from "./lib/velum-project";
import { canonicalPerson, mergePeople } from "./lib/project-review"; import { canonicalPerson, mergePeople } from "./lib/project-review";
import { planProjectRevision, reviseProjectRun } from "./lib/project-revision"; import { planProjectRevision, reviseProjectRun } from "./lib/project-revision";
import { TranslationTable } from "./show-translation-tables";
type SavedRun = { run: ProjectRun; directory: string }; type SavedRun = { run: ProjectRun; directory: string };
@@ -229,6 +230,9 @@ function ProjectHistory(props: { projectName: string; entries: SavedRun[] }) {
title={new Date(run.createdAt).toLocaleString("de-AT")} title={new Date(run.createdAt).toLocaleString("de-AT")}
subtitle={`${run.files.length} Dateien`} subtitle={`${run.files.length} Dateien`}
actions={<ActionPanel> actions={<ActionPanel>
<Action.Push title="Übersetzungstabelle anzeigen" icon={Icon.List}
target={<TranslationTable title={`${props.projectName} · ${new Date(run.createdAt).toLocaleString("de-AT")}`}
mapping={run.mapping} />} />
<Action title="Pseudonymisierte Kopien im Finder anzeigen" icon={Icon.Folder} <Action title="Pseudonymisierte Kopien im Finder anzeigen" icon={Icon.Folder}
onAction={() => showInFinder(runCopiesPath(run, directory))} /> onAction={() => showInFinder(runCopiesPath(run, directory))} />
<Action.CopyToClipboard title="Speicherpfad kopieren" content={runCopiesPath(run, directory)} /> <Action.CopyToClipboard title="Speicherpfad kopieren" content={runCopiesPath(run, directory)} />
@@ -265,6 +269,8 @@ export default function Command() {
accessories={[{ text: `${run.files.length} Dateien` }, { text: new Date(run.createdAt).toLocaleString("de-AT") }, accessories={[{ text: `${run.files.length} Dateien` }, { text: new Date(run.createdAt).toLocaleString("de-AT") },
...(entries.length > 1 ? [{ text: `${entries.length} Stände` }] : [])]} ...(entries.length > 1 ? [{ text: `${entries.length} Stände` }] : [])]}
actions={<ActionPanel> actions={<ActionPanel>
<Action.Push title="Übersetzungstabelle anzeigen" icon={Icon.List}
target={<TranslationTable title={run.projectName} mapping={run.mapping} />} />
<Action title="Projektdateien als Klartext wiederherstellen" icon={Icon.Download} <Action title="Projektdateien als Klartext wiederherstellen" icon={Icon.Download}
onAction={() => push(<FilePicker run={run} directory={directory} />)} /> onAction={() => push(<FilePicker run={run} directory={directory} />)} />
<Action title="Pseudonymisierte Kopien im Finder anzeigen" icon={Icon.Folder} <Action title="Pseudonymisierte Kopien im Finder anzeigen" icon={Icon.Folder}

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>
);
}