mirror of
https://github.com/mermaid-js/mermaid-live-editor.git
synced 2026-11-04 08:59:08 +00:00
feat: port "rename history entries" onto the runes history module
PR #1982 (merged into develop) replaced history.ts with the runes-based historyState.svelte.ts and rewrote History.svelte, so this re-applies the rename feature on the new architecture: - add renameEntry(id, name) to historyState.svelte.ts (trims, no-ops on blank names or in loader mode, routes via the active slot) - re-add the inline rename UI (pencil button + input, Enter/Escape/blur) to the rewritten History.svelte, with truncation - port the rename unit tests to historyState.test.ts and add an e2e case Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1be6515843
commit
531cd08d11
4 files changed
+122
-11
No files matched your search
@@ -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>
|
||||
|
||||
@@ -139,6 +139,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([
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user