feat: restore current project copies and choose text mapping
This commit is contained in:
12
package.json
12
package.json
@@ -80,14 +80,14 @@
|
||||
{
|
||||
"name": "depseudonymize-selected-text",
|
||||
"title": "Markierten Text wiederherstellen",
|
||||
"description": "Aktuell markierten Text mit der aktiven Sitzung wiederherstellen und das Ergebnis kopieren oder einfügen.",
|
||||
"mode": "no-view"
|
||||
"description": "Markierten Text übernehmen, Projekt oder Sitzung wählen und wiederherstellen.",
|
||||
"mode": "view"
|
||||
},
|
||||
{
|
||||
"name": "depseudonymize-clipboard",
|
||||
"title": "Zwischenablage wiederherstellen",
|
||||
"description": "Aktuellen Zwischenablage-Text mit der aktiven Sitzung wiederherstellen und das Ergebnis kopieren oder einfügen.",
|
||||
"mode": "no-view"
|
||||
"description": "Zwischenablage übernehmen, Projekt oder Sitzung wählen und wiederherstellen.",
|
||||
"mode": "view"
|
||||
},
|
||||
{
|
||||
"name": "manage-sessions",
|
||||
@@ -104,8 +104,8 @@
|
||||
},
|
||||
{
|
||||
"name": "restore-project-files",
|
||||
"title": "Projektdateien nachbearbeiten",
|
||||
"description": "Zuordnungen freigeben, Personen zusammenführen oder Projektdateien als Klartext exportieren.",
|
||||
"title": "Projektdateien wiederherstellen",
|
||||
"description": "Aktuelle Dateien aus dem Kopienordner als Klartext exportieren, Zuordnungen nachbearbeiten und frühere Stände wiederherstellen.",
|
||||
"mode": "view",
|
||||
"icon": "extension-icon.png"
|
||||
},
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Clipboard } from "@raycast/api";
|
||||
import { depseudonymizeQuickText } from "./quick";
|
||||
|
||||
export default async function Command() {
|
||||
const clipboardText = await Clipboard.readText();
|
||||
await depseudonymizeQuickText(clipboardText ?? "", "Zwischenablage");
|
||||
}
|
||||
5
src/depseudonymize-clipboard.tsx
Normal file
5
src/depseudonymize-clipboard.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import RestoreTextForm from "./depseudonymize-text";
|
||||
|
||||
export default function Command() {
|
||||
return <RestoreTextForm initialSource="clipboard" />;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { showToast, Toast } from "@raycast/api";
|
||||
import { depseudonymizeQuickText } from "./quick";
|
||||
import { getSelectedTextSafely } from "./selection";
|
||||
|
||||
export default async function Command() {
|
||||
const selectedText = await getSelectedTextSafely();
|
||||
if (!selectedText) {
|
||||
await showToast({
|
||||
style: Toast.Style.Failure,
|
||||
title: "Kein markierter Text gefunden",
|
||||
});
|
||||
return;
|
||||
}
|
||||
await depseudonymizeQuickText(selectedText, "Selektion");
|
||||
}
|
||||
5
src/depseudonymize-selected-text.tsx
Normal file
5
src/depseudonymize-selected-text.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import RestoreTextForm from "./depseudonymize-text";
|
||||
|
||||
export default function Command() {
|
||||
return <RestoreTextForm initialSource="selection" />;
|
||||
}
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
Toast,
|
||||
useNavigation,
|
||||
} from "@raycast/api";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getSelectedTextSafely } from "./selection";
|
||||
import { canonicalProjectPath, listAllRuns, listRuns, restoreTextWithCount } from "./lib/velum-project";
|
||||
import { depseudonymize } from "./velum";
|
||||
import { getActiveSessionId, getSession, listSessions } from "./sessions";
|
||||
import type { DepseudonymizeResponse, VelumSession } from "./types";
|
||||
@@ -20,31 +22,46 @@ type FormValues = {
|
||||
sessionId: string;
|
||||
};
|
||||
|
||||
export default function Command() {
|
||||
const CHOOSE_TARGET = "choose-target";
|
||||
|
||||
export default function Command(props: { initialSource?: "selection" | "clipboard" } = {}) {
|
||||
const [text, setText] = useState("");
|
||||
const [sessions, setSessions] = useState<VelumSession[]>([]);
|
||||
const [activeSessionId, setActiveSessionId] = useState<string>();
|
||||
const [projects, setProjects] = useState<Array<{ path: string; name: string }>>([]);
|
||||
const [targetId, setTargetId] = useState(CHOOSE_TARGET);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const { push } = useNavigation();
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
const [loadedSessions, activeId] = await Promise.all([
|
||||
const initialText = props.initialSource === "selection" ? getSelectedTextSafely()
|
||||
: props.initialSource === "clipboard" ? Clipboard.readText() : Promise.resolve(undefined);
|
||||
const [loadedSessions, activeId, savedRuns, selected] = await Promise.all([
|
||||
listSessions(),
|
||||
getActiveSessionId(),
|
||||
listAllRuns(),
|
||||
initialText,
|
||||
]);
|
||||
setSessions(loadedSessions);
|
||||
setActiveSessionId(activeId);
|
||||
const unique = new Map<string, string>();
|
||||
for (const { run } of savedRuns) {
|
||||
const path = canonicalProjectPath(run.projectPath);
|
||||
if (!unique.has(path)) unique.set(path, run.projectName);
|
||||
}
|
||||
setProjects([...unique].map(([path, name]) => ({ path, name })));
|
||||
if (selected) setText(selected);
|
||||
else if (props.initialSource) await showToast({ style: Toast.Style.Failure,
|
||||
title: props.initialSource === "selection" ? "Kein markierter Text gefunden" : "Zwischenablage enthält keinen Text" });
|
||||
setIsLoading(false);
|
||||
}
|
||||
load();
|
||||
load().catch(async (error: Error) => {
|
||||
setIsLoading(false);
|
||||
await showToast({ style: Toast.Style.Failure, title: "Zuordnungen nicht lesbar", message: error.message });
|
||||
});
|
||||
}, []);
|
||||
|
||||
const defaultSessionId = useMemo(
|
||||
() => activeSessionId || sessions[0]?.id,
|
||||
[activeSessionId, sessions],
|
||||
);
|
||||
|
||||
async function loadClipboardText() {
|
||||
const clipboardText = await Clipboard.readText();
|
||||
if (!clipboardText) {
|
||||
@@ -59,19 +76,10 @@ export default function Command() {
|
||||
|
||||
async function handleSubmit(values: FormValues) {
|
||||
const input = values.text.trim();
|
||||
if (!input || !values.sessionId) {
|
||||
if (!input || !values.sessionId || values.sessionId === CHOOSE_TARGET) {
|
||||
await showToast({
|
||||
style: Toast.Style.Failure,
|
||||
title: "Sitzung wählen und Text eingeben",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const session = await getSession(values.sessionId);
|
||||
if (!session) {
|
||||
await showToast({
|
||||
style: Toast.Style.Failure,
|
||||
title: "Sitzung nicht gefunden",
|
||||
title: "Projekt oder Sitzung wählen und Text eingeben",
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -81,11 +89,28 @@ export default function Command() {
|
||||
title: "Stelle wieder her…",
|
||||
});
|
||||
try {
|
||||
const result = await depseudonymize(input, session.mapping);
|
||||
let result: DepseudonymizeResponse;
|
||||
let targetName: string;
|
||||
let targetSubtitle: string;
|
||||
if (values.sessionId.startsWith("project:")) {
|
||||
const projectPath = values.sessionId.slice("project:".length);
|
||||
const saved = (await listRuns(projectPath))[0];
|
||||
if (!saved) throw new Error("Projektstand nicht gefunden.");
|
||||
const restored = restoreTextWithCount(input, saved.run.mapping, saved.run.matchModes);
|
||||
result = { original_text: restored.text, replacements_made: restored.replacements };
|
||||
targetName = `Projekt ${saved.run.projectName}`;
|
||||
targetSubtitle = saved.run.projectPath;
|
||||
} else {
|
||||
const session = await getSession(values.sessionId);
|
||||
if (!session) throw new Error("Sitzung nicht gefunden.");
|
||||
result = await depseudonymize(input, session.mapping);
|
||||
targetName = session.name;
|
||||
targetSubtitle = sessionSubtitle(session);
|
||||
}
|
||||
toast.style = Toast.Style.Success;
|
||||
toast.title = "Wiederhergestellt";
|
||||
toast.message = `${result.replacements_made} Ersetzungen`;
|
||||
push(<DepseudonymizeResult result={result} session={session} />);
|
||||
push(<DepseudonymizeResult result={result} targetName={targetName} targetSubtitle={targetSubtitle} />);
|
||||
} catch (error) {
|
||||
toast.style = Toast.Style.Failure;
|
||||
toast.title = "Wiederherstellung fehlgeschlagen";
|
||||
@@ -120,9 +145,15 @@ export default function Command() {
|
||||
/>
|
||||
<Form.Dropdown
|
||||
id="sessionId"
|
||||
title="Sitzung"
|
||||
defaultValue={defaultSessionId}
|
||||
title="Projekt oder Sitzung"
|
||||
value={targetId}
|
||||
onChange={setTargetId}
|
||||
>
|
||||
<Form.Dropdown.Item value={CHOOSE_TARGET} title="Bitte Projekt oder Sitzung wählen" icon={Icon.Circle} />
|
||||
{projects.map((project) => (
|
||||
<Form.Dropdown.Item key={project.path} value={`project:${project.path}`}
|
||||
title={`Projekt: ${project.name} · ${project.path}`} icon={Icon.Folder} />
|
||||
))}
|
||||
{sessions.map((session) => (
|
||||
<Form.Dropdown.Item
|
||||
key={session.id}
|
||||
@@ -140,14 +171,16 @@ export default function Command() {
|
||||
|
||||
function DepseudonymizeResult({
|
||||
result,
|
||||
session,
|
||||
targetName,
|
||||
targetSubtitle,
|
||||
}: {
|
||||
result: DepseudonymizeResponse;
|
||||
session: VelumSession;
|
||||
targetName: string;
|
||||
targetSubtitle: string;
|
||||
}) {
|
||||
const markdown = [
|
||||
`# ${session.name}`,
|
||||
sessionSubtitle(session),
|
||||
`# ${targetName}`,
|
||||
targetSubtitle,
|
||||
"",
|
||||
"## Wiederhergestellter Text",
|
||||
markdownCodeBlock(result.original_text),
|
||||
|
||||
@@ -255,9 +255,19 @@ function restorePattern(mapping: PlaceholderMapping, matchModes: ProjectRun["mat
|
||||
return new RegExp(parts.join("|"), "gu");
|
||||
}
|
||||
|
||||
export function restoreText(source: string, mapping: PlaceholderMapping, matchModes?: ProjectRun["matchModes"]): string {
|
||||
export function restoreTextWithCount(source: string, mapping: PlaceholderMapping,
|
||||
matchModes?: ProjectRun["matchModes"]): { text: string; replacements: number } {
|
||||
const pattern = restorePattern(mapping, matchModes);
|
||||
return pattern ? source.replace(pattern, (key) => mapping[key].original) : source;
|
||||
let replacements = 0;
|
||||
const text = pattern ? source.replace(pattern, (key) => {
|
||||
replacements++;
|
||||
return mapping[key].original;
|
||||
}) : source;
|
||||
return { text, replacements };
|
||||
}
|
||||
|
||||
export function restoreText(source: string, mapping: PlaceholderMapping, matchModes?: ProjectRun["matchModes"]): string {
|
||||
return restoreTextWithCount(source, mapping, matchModes).text;
|
||||
}
|
||||
|
||||
export function restorePath(relPath: string, mapping: PlaceholderMapping, matchModes?: ProjectRun["matchModes"]): string {
|
||||
@@ -305,6 +315,23 @@ export function runCopiesPath(run: ProjectRun, directory: string): string {
|
||||
return run.copyPath ?? join(directory, "files");
|
||||
}
|
||||
|
||||
export async function scanRestorableCopyFiles(run: ProjectRun, directory: string, current = true): Promise<{
|
||||
root: string; files: ProjectFile[]; skipped: number;
|
||||
}> {
|
||||
let root = current ? runCopiesPath(run, directory) : join(directory, "files");
|
||||
if (current && run.copyPath) {
|
||||
try {
|
||||
if (!(await fs.lstat(root)).isDirectory()) throw new Error("Kopienpfad ist kein regulärer Ordner.");
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
||||
root = join(directory, "files");
|
||||
}
|
||||
}
|
||||
const scan = await scanProjectFiles(root);
|
||||
const files = scan.files.filter((file) => file.kind === "text" || isRestorableDocument(file.relPath));
|
||||
return { root, files, skipped: scan.skipped.length + scan.files.length - files.length };
|
||||
}
|
||||
|
||||
/** Publish only output files next to the project, without the private mapping. */
|
||||
export async function publishProjectCopies(run: ProjectRun, directory: string): Promise<string> {
|
||||
const projectPath = canonicalProjectPath(run.projectPath);
|
||||
|
||||
@@ -4,8 +4,8 @@ import { homedir } from "node:os";
|
||||
import { basename, dirname, join } from "node:path";
|
||||
import { useEffect, useState } from "react";
|
||||
import { depseudonymizeDocument } from "./velum";
|
||||
import { canonicalProjectPath, isRestorableDocument, listAllRuns, resolveWithin, restorePath, restoreText, runCopiesPath } from "./lib/velum-project";
|
||||
import type { ProjectRun } from "./lib/velum-project";
|
||||
import { canonicalProjectPath, listAllRuns, resolveWithin, restorePath, restoreText, runCopiesPath, scanRestorableCopyFiles } from "./lib/velum-project";
|
||||
import type { ProjectFile, ProjectRun } from "./lib/velum-project";
|
||||
import { canonicalPerson, mergePeople } from "./lib/project-review";
|
||||
import { planProjectRevision, reviseProjectRun } from "./lib/project-revision";
|
||||
|
||||
@@ -131,9 +131,26 @@ function RevisionReview(props: { saved: SavedRun; onSaved: () => Promise<void> }
|
||||
</List>;
|
||||
}
|
||||
|
||||
function FilePicker(props: { run: ProjectRun; directory: string }) {
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set(props.run.files.map((file) => file.output)));
|
||||
function FilePicker(props: { run: ProjectRun; directory: string; historical?: boolean }) {
|
||||
const [files, setFiles] = useState<ProjectFile[]>([]);
|
||||
const [sourceRoot, setSourceRoot] = useState("");
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
const [skipped, setSkipped] = useState(0);
|
||||
const [busy, setBusy] = useState(false);
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
async function load() {
|
||||
const scan = await scanRestorableCopyFiles(props.run, props.directory, !props.historical);
|
||||
if (!active) return;
|
||||
setSourceRoot(scan.root);
|
||||
setFiles(scan.files);
|
||||
setSelected(new Set(scan.files.map((file) => file.relPath)));
|
||||
setSkipped(scan.skipped);
|
||||
}
|
||||
load().catch((error: Error) => showToast({ style: Toast.Style.Failure,
|
||||
title: "Kopienordner nicht lesbar", message: error.message }));
|
||||
return () => { active = false; };
|
||||
}, [props.run, props.directory, props.historical]);
|
||||
function toggle(path: string) {
|
||||
setSelected((current) => {
|
||||
const next = new Set(current);
|
||||
@@ -143,23 +160,30 @@ function FilePicker(props: { run: ProjectRun; directory: string }) {
|
||||
});
|
||||
}
|
||||
async function exportFiles() {
|
||||
if (!selected.size) return;
|
||||
if (!selected.size || !sourceRoot) return;
|
||||
setBusy(true);
|
||||
const toast = await showToast({ style: Toast.Style.Animated, title: "Klartext-Kopien werden erstellt" });
|
||||
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const targetRoot = join(homedir(), "Downloads", "Velum", `${basename(props.run.projectName)}-${stamp}`);
|
||||
const parent = join(homedir(), "Downloads", "Velum");
|
||||
let targetRoot: string | undefined;
|
||||
try {
|
||||
await fs.mkdir(targetRoot, { recursive: true, mode: 0o700 });
|
||||
const names = new Set<string>();
|
||||
for (const file of props.run.files.filter((item) => selected.has(item.output))) {
|
||||
const restoredName = restorePath(file.output, props.run.mapping, props.run.matchModes);
|
||||
const planned = files.filter((item) => selected.has(item.relPath)).map((file) => ({
|
||||
file, restoredName: restorePath(file.relPath, props.run.mapping, props.run.matchModes),
|
||||
}));
|
||||
for (const { restoredName } of planned) {
|
||||
if (names.has(restoredName)) throw new Error(`Mehrdeutiger Ausgabename: ${restoredName}`);
|
||||
names.add(restoredName);
|
||||
const source = resolveWithin(join(props.directory, "files"), file.output);
|
||||
}
|
||||
await fs.mkdir(parent, { recursive: true, mode: 0o700 });
|
||||
targetRoot = await fs.mkdtemp(join(parent, `${basename(props.run.projectName)}-${stamp}-`));
|
||||
for (const { file, restoredName } of planned) {
|
||||
const source = resolveWithin(sourceRoot, file.relPath);
|
||||
const target = resolveWithin(targetRoot, restoredName);
|
||||
if (!(await fs.lstat(source)).isFile()) throw new Error(`Keine reguläre Datei: ${file.relPath}`);
|
||||
const data = await fs.readFile(source);
|
||||
const result = file.kind === "document" && isRestorableDocument(file.output)
|
||||
? await depseudonymizeDocument(basename(file.output), data, props.run.mapping)
|
||||
const result = file.kind === "document"
|
||||
? await depseudonymizeDocument(basename(file.relPath), data, props.run.mapping)
|
||||
: Buffer.from(restoreText(data.toString("utf8"), props.run.mapping, props.run.matchModes), "utf8");
|
||||
await fs.mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
||||
await fs.writeFile(target, result, { flag: "wx", mode: 0o600 });
|
||||
@@ -168,21 +192,28 @@ function FilePicker(props: { run: ProjectRun; directory: string }) {
|
||||
toast.title = `${selected.size} Klartext-Dateien in Downloads`;
|
||||
await showInFinder(targetRoot);
|
||||
} catch (error) {
|
||||
if (targetRoot) await fs.rm(targetRoot, { recursive: true, force: true });
|
||||
toast.style = Toast.Style.Failure;
|
||||
toast.title = "Klartext-Export fehlgeschlagen";
|
||||
toast.message = (error as Error).message;
|
||||
} finally { setBusy(false); }
|
||||
}
|
||||
return <List isLoading={busy} navigationTitle={`${props.run.projectName}: Dateien für Klartext wählen`}
|
||||
return <List isLoading={busy || !sourceRoot} navigationTitle={`${props.run.projectName}: Dateien für Klartext wählen`}
|
||||
searchBarPlaceholder="Dateien filtern">
|
||||
<List.Section title={`${selected.size} von ${props.run.files.length} Dateien ausgewählt`}>
|
||||
{props.run.files.map((file) => <List.Item key={file.output}
|
||||
icon={selected.has(file.output) ? Icon.CheckCircle : Icon.Circle}
|
||||
title={restorePath(file.output, props.run.mapping, props.run.matchModes)} subtitle={file.output}
|
||||
<List.Section title={`${selected.size} von ${files.length} Dateien ausgewählt · ${skipped} übersprungen`}>
|
||||
<List.Item key="export" icon={Icon.Download} title="Ausgewählte Dateien wiederherstellen"
|
||||
subtitle="Klartext-Kopien in Downloads/Velum erstellen"
|
||||
actions={<ActionPanel><Action title="Auswahl nach Downloads wiederherstellen"
|
||||
icon={Icon.Download} onAction={exportFiles} /></ActionPanel>} />
|
||||
<List.Item key="source" icon={Icon.Folder} title="Quelle für die Wiederherstellung" subtitle={sourceRoot}
|
||||
actions={<ActionPanel><Action title="Quellordner im Finder anzeigen" onAction={() => showInFinder(sourceRoot)} /></ActionPanel>} />
|
||||
{files.map((file) => <List.Item key={file.relPath}
|
||||
icon={selected.has(file.relPath) ? Icon.CheckCircle : Icon.Circle}
|
||||
title={restorePath(file.relPath, props.run.mapping, props.run.matchModes)} subtitle={file.relPath}
|
||||
actions={<ActionPanel>
|
||||
<Action title={selected.has(file.output) ? "Datei abwählen" : "Datei auswählen"} onAction={() => toggle(file.output)} />
|
||||
<Action title={selected.has(file.relPath) ? "Datei abwählen" : "Datei auswählen"} onAction={() => toggle(file.relPath)} />
|
||||
<Action title="Auswahl nach Downloads rückübersetzen" icon={Icon.Download} onAction={exportFiles} />
|
||||
<Action title="Alle auswählen" onAction={() => setSelected(new Set(props.run.files.map((item) => item.output)))} />
|
||||
<Action title="Alle auswählen" onAction={() => setSelected(new Set(files.map((item) => item.relPath)))} />
|
||||
<Action title="Keine auswählen" onAction={() => setSelected(new Set())} />
|
||||
</ActionPanel>} />)}
|
||||
</List.Section>
|
||||
@@ -201,8 +232,10 @@ function ProjectHistory(props: { projectName: string; entries: SavedRun[] }) {
|
||||
<Action title="Pseudonymisierte Kopien im Finder anzeigen" icon={Icon.Folder}
|
||||
onAction={() => showInFinder(runCopiesPath(run, directory))} />
|
||||
<Action.CopyToClipboard title="Speicherpfad kopieren" content={runCopiesPath(run, directory)} />
|
||||
<Action title="Dateien als Klartext exportieren" icon={Icon.Download}
|
||||
onAction={() => push(<FilePicker run={run} directory={directory} />)} />
|
||||
{run.copyPath && <Action title="Aktuelle Dateien dieses Kopienordners wiederherstellen" icon={Icon.Download}
|
||||
onAction={() => push(<FilePicker run={run} directory={directory} />)} />}
|
||||
<Action title="Gespeicherten Stand wiederherstellen" icon={Icon.Clock}
|
||||
onAction={() => push(<FilePicker run={run} directory={directory} historical />)} />
|
||||
</ActionPanel>} />)}
|
||||
</List.Section>
|
||||
</List>;
|
||||
@@ -232,13 +265,13 @@ export default function Command() {
|
||||
accessories={[{ text: `${run.files.length} Dateien` }, { text: new Date(run.createdAt).toLocaleString("de-AT") },
|
||||
...(entries.length > 1 ? [{ text: `${entries.length} Stände` }] : [])]}
|
||||
actions={<ActionPanel>
|
||||
<Action title="Projektdateien als Klartext wiederherstellen" icon={Icon.Download}
|
||||
onAction={() => push(<FilePicker run={run} directory={directory} />)} />
|
||||
<Action title="Pseudonymisierte Kopien im Finder anzeigen" icon={Icon.Folder}
|
||||
onAction={() => showInFinder(runCopiesPath(run, directory))} />
|
||||
<Action.CopyToClipboard title="Speicherpfad kopieren" content={runCopiesPath(run, directory)} />
|
||||
<Action title="Aktuellen Stand nachbearbeiten" icon={Icon.Pencil}
|
||||
onAction={() => push(<RevisionReview saved={{ run, directory }} onSaved={refresh} />)} />
|
||||
<Action title="Aktuelle Dateien als Klartext exportieren" icon={Icon.Download}
|
||||
onAction={() => push(<FilePicker run={run} directory={directory} />)} />
|
||||
{entries.length > 1 && <Action title="Frühere Stände anzeigen" icon={Icon.Clock}
|
||||
onAction={() => push(<ProjectHistory projectName={run.projectName} entries={entries.slice(1)} />)} />}
|
||||
</ActionPanel>} />;
|
||||
|
||||
@@ -72,6 +72,15 @@ describe("Velum-Projektstände", () => {
|
||||
expect(await fs.readFile(join(copyPath, "Unterordner", "PERSON_01.txt"), "utf8")).toBe("PERSON_01");
|
||||
expect(await fs.readdir(copyPath)).toEqual(["Unterordner"]);
|
||||
expect(await fs.readFile(join(created.directory, "run.json"), "utf8")).toContain("Max Mustermann");
|
||||
await fs.writeFile(join(copyPath, "Antwort.txt"), "PERSON_01 antwortet");
|
||||
const current = await project.scanRestorableCopyFiles(created.run, created.directory);
|
||||
expect(current.root).toBe(copyPath);
|
||||
expect(current.files.map((file) => file.relPath)).toEqual(["Antwort.txt", "Unterordner/PERSON_01.txt"]);
|
||||
const history = await project.scanRestorableCopyFiles(created.run, created.directory, false);
|
||||
expect(history.root).toBe(join(created.directory, "files"));
|
||||
expect(history.files.map((file) => file.relPath)).toEqual(["Unterordner/PERSON_01.txt"]);
|
||||
expect(project.restoreTextWithCount("PERSON_01 antwortet", created.run.mapping))
|
||||
.toEqual({ text: "Max Mustermann antwortet", replacements: 1 });
|
||||
await expect(project.publishProjectCopies(created.run, created.directory)).rejects.toThrow("bereits vorhanden");
|
||||
expect((await project.listRuns(source, support))[0].run.copyPath).toBe(copyPath);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user