Merge pull request #1918 from mvanhorn/feat/rename-history-entries

feat: allow renaming history entries
This commit is contained in:
Sidharth Vinod authored and GitHub committed 2026-07-12 18:01:33 +00:00
commit e67de3a20b
4 files changed
+122 -11

No files matched your search

+52 -11
View File
@@ -10,6 +10,7 @@
import BookmarkIcon from '~icons/material-symbols/bookmark-outline-rounded';
import TrashAltIcon from '~icons/material-symbols/delete-outline-rounded';
import DownloadIcon from '~icons/material-symbols/download-rounded';
import EditIcon from '~icons/material-symbols/edit-outline-rounded';
import SaveIcon from '~icons/material-symbols/save-outline-rounded';
import UndoIcon from '~icons/material-symbols/settings-backup-restore-rounded';
import UploadIcon from '~icons/material-symbols/upload-rounded';
@@ -23,6 +24,7 @@
clearActive,
historyState,
removeEntry,
renameEntry,
restoreEntries,
setMode
} from './historyState.svelte';
@@ -48,6 +50,17 @@
}
});
// Inline rename state for a single entry.
let editingId: string | null = $state(null);
let editValue = $state('');
const commitRename = () => {
if (editingId !== null && editValue.trim()) {
renameEntry(editingId, editValue);
}
editingId = null;
};
const emptyMessage = $derived(
historyState.mode === 'auto'
? 'No timeline snapshots yet.\nThe Timeline is saved automatically every minute.'
@@ -147,21 +160,49 @@
{/if}
</div>
{/snippet}
<ul class="flex h-full min-w-fit flex-col gap-2 overflow-auto p-2" id="historyList">
<ul class="flex h-full flex-col gap-2 overflow-auto p-2" id="historyList">
{#if entriesWithUrl.length > 0}
{#each entriesWithUrl as { id, state, time, name, url, type, openUrl } (id)}
<li class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<div class="flex flex-col">
{#if url}
<a
href={url}
target="_blank"
title="Open revision in new tab"
class="text-blue-500 hover:underline">{name}</a>
{:else}
<span class="whitespace-nowrap">{name}</span>
{/if}
<div class="flex min-w-0 flex-1 flex-col">
<div class="flex min-w-0 items-center gap-1 overflow-hidden">
{#if url}
<a
href={url}
target="_blank"
rel="noopener"
title="Open revision in new tab"
class="min-w-0 truncate text-blue-500 hover:underline">{name}</a>
{:else if editingId === id}
<input
class="min-w-0 flex-1 rounded border px-1 text-sm"
bind:value={editValue}
aria-label="Rename entry"
onkeydown={(event) => {
if (event.key === 'Enter') {
commitRename();
} else if (event.key === 'Escape') {
editingId = null;
}
}}
onblur={commitRename} />
{:else}
<span class="min-w-0 truncate" title={name}>{name}</span>
{#if type !== 'loader'}
<button
type="button"
class="shrink-0 opacity-50 hover:opacity-100"
title="Rename"
onclick={() => {
editingId = id;
editValue = name ?? '';
}}>
<EditIcon class="size-3.5" />
</button>
{/if}
{/if}
</div>
<span class="text-xs whitespace-nowrap text-primary-foreground/30">
{new Date(time).toLocaleString()}
</span>
@@ -101,6 +101,16 @@ export const removeEntry = (id: string): void => {
logEvent('history', { action: 'clear', type: 'single' });
};
export const renameEntry = (id: string, name: string): void => {
const trimmed = name.trim();
const slot = slotFor(mode.value);
if (!trimmed || !slot) {
return;
}
slot.value = slot.value.map((entry) => (entry.id === id ? { ...entry, name: trimmed } : entry));
logEvent('history', { action: 'rename' });
};
export const clearActive = (): void => {
const slot = slotFor(mode.value);
if (!slot) {
@@ -8,6 +8,7 @@ import {
historyState,
injectHistoryIDs,
removeEntry,
renameEntry,
restoreEntries,
setLoaderEntries,
setMode,
@@ -184,6 +185,51 @@ describe('removeEntry / clearActive', () => {
});
});
describe('renameEntry', () => {
it('renames an entry in the active (manual) store', () => {
addManualEntry(codeState('graph TD\n A-->B'));
setMode('manual');
renameEntry(historyState.entries[0].id, 'my-custom-name');
expect(historyState.entries[0].name).toBe('my-custom-name');
});
it('renames an entry in the auto store', () => {
addAutoEntry(codeState('graph TD\n A-->B'));
setMode('auto');
renameEntry(historyState.entries[0].id, 'auto-renamed');
expect(historyState.entries[0].name).toBe('auto-renamed');
});
it('only renames the targeted entry and trims the name', () => {
addManualEntry(codeState('graph TD\n A-->B'));
addManualEntry(codeState('graph TD\n A-->C'));
setMode('manual');
const firstName = historyState.entries[0].name;
const targetId = historyState.entries[1].id;
renameEntry(targetId, ' trimmed-name ');
expect(historyState.entries[0].name).toBe(firstName);
expect(historyState.entries[1].name).toBe('trimmed-name');
});
it('ignores blank names', () => {
addManualEntry(codeState('graph TD\n A-->B'));
setMode('manual');
const before = historyState.entries[0].name;
renameEntry(historyState.entries[0].id, ' ');
expect(historyState.entries[0].name).toBe(before);
});
it('does nothing in loader mode', () => {
setLoaderEntries([
{ name: 'rev', state: defaultState, time: 1, type: 'loader', url: 'http://x' }
]);
setMode('loader');
const id = historyState.entries[0].id;
renameEntry(id, 'changed');
expect(historyState.entries[0].name).toBe('rev');
});
});
describe('setLoaderEntries', () => {
it('replaces the in-memory revisions and assigns ids', () => {
setLoaderEntries([
+14
View File
@@ -144,4 +144,18 @@ test.describe('History', () => {
await expect(page.locator('#historyList li')).toHaveCount(0);
await expect(page.locator('#historyList')).toContainText('No saved states yet.');
});
test('renames a saved entry inline', async ({ page }) => {
await openHistory(page);
await page.locator('#saveHistory').click();
await expect(page.locator('#historyList li')).toHaveCount(1);
await page.getByRole('button', { name: 'Rename' }).first().click();
const input = page.getByRole('textbox', { name: 'Rename entry' });
await input.fill('my-custom-name');
await input.press('Enter');
await expect(page.locator('#historyList')).toContainText('my-custom-name');
await expect(page.locator('#historyList li')).toHaveCount(1);
});
});