feat: Upload history
This commit is contained in:
+3
-1
@@ -27,6 +27,7 @@
|
||||
"@testing-library/svelte": "3.2.1",
|
||||
"@types/mermaid": "8.2.9",
|
||||
"@types/pako": "1.0.3",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.34.0",
|
||||
"@typescript-eslint/parser": "5.34.0",
|
||||
"@vitest/ui": "0.22.1",
|
||||
@@ -75,7 +76,8 @@
|
||||
"monaco-mermaid": "1.0.6",
|
||||
"pako": "2.0.4",
|
||||
"random-word-slugs": "0.1.6",
|
||||
"svg-pan-zoom": "^3.6.1"
|
||||
"svg-pan-zoom": "^3.6.1",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,svelte,js,css,md,json}": [
|
||||
|
||||
@@ -77,11 +77,11 @@
|
||||
}
|
||||
};
|
||||
|
||||
const clearHistory = (date?: number): void => {
|
||||
if (!date && !prompt('Clear all saved items?')) {
|
||||
const clearHistory = (id?: string): void => {
|
||||
if (!id && !prompt('Clear all saved items?')) {
|
||||
return;
|
||||
}
|
||||
clearHistoryData(date);
|
||||
clearHistoryData(id);
|
||||
};
|
||||
|
||||
const restoreHistoryItem = (state: State): void => {
|
||||
@@ -147,7 +147,7 @@
|
||||
</div>
|
||||
<ul class="p-2 space-y-2 overflow-auto h-56" id="historyList">
|
||||
{#if $historyStore.length > 0}
|
||||
{#each $historyStore as { state, time, name, url, type }}
|
||||
{#each $historyStore as { id, state, time, name, url, type }}
|
||||
<li class="rounded p-2 shadow flex-col">
|
||||
<div class="flex">
|
||||
<div class="flex-1">
|
||||
@@ -168,7 +168,7 @@
|
||||
<button class="btn btn-success" on:click={() => restoreHistoryItem(state)}
|
||||
><i class="fas fa-undo mr-1" />Restore</button>
|
||||
{#if type !== 'loader'}
|
||||
<button class="btn btn-error" on:click={() => clearHistory(time)}
|
||||
<button class="btn btn-error" on:click={() => clearHistory(id)}
|
||||
><i class="fas fa-trash-alt mr-1" />Delete</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,8 @@ import { derived, writable, get } from 'svelte/store';
|
||||
import type { Readable, Writable } from 'svelte/store';
|
||||
import { persist, localStorage } from '@macfja/svelte-persistent-store';
|
||||
import { generateSlug } from 'random-word-slugs';
|
||||
import type { HistoryEntry, HistoryType } from '$lib/types';
|
||||
import type { HistoryEntry, HistoryType, Optional } from '$lib/types';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
|
||||
const MAX_AUTO_HISTORY_LENGTH = 30;
|
||||
|
||||
@@ -41,28 +42,37 @@ export const historyStore: Readable<HistoryEntry[]> = derived(
|
||||
}
|
||||
);
|
||||
|
||||
export const addHistoryEntry = (entry: HistoryEntry): void => {
|
||||
export const addHistoryEntry = (entryToAdd: Optional<HistoryEntry, 'id'>): void => {
|
||||
const entry: HistoryEntry = {
|
||||
...entryToAdd,
|
||||
id: uuidV4()
|
||||
};
|
||||
|
||||
if (entry.type === 'loader') {
|
||||
loaderHistoryStore.update((entries) => [entry, ...entries]);
|
||||
return;
|
||||
}
|
||||
entry.name = generateSlug(2);
|
||||
if (entry.type !== 'auto') {
|
||||
|
||||
if (!entry.name) {
|
||||
entry.name = generateSlug(2);
|
||||
}
|
||||
|
||||
if (entry.type === 'auto') {
|
||||
autoHistoryStore.update((entries) => {
|
||||
if (entries.length >= MAX_AUTO_HISTORY_LENGTH) {
|
||||
entries = entries.slice(0, MAX_AUTO_HISTORY_LENGTH - 1);
|
||||
}
|
||||
return [entry, ...entries];
|
||||
});
|
||||
} else if (entry.type === 'manual') {
|
||||
manualHistoryStore.update((entries) => [entry, ...entries]);
|
||||
return;
|
||||
}
|
||||
autoHistoryStore.update((entries) => {
|
||||
if (entries.length === MAX_AUTO_HISTORY_LENGTH) {
|
||||
entries.pop();
|
||||
}
|
||||
return [entry, ...entries];
|
||||
});
|
||||
};
|
||||
|
||||
export const clearHistoryData = (time?: number): void => {
|
||||
export const clearHistoryData = (idToClear?: string): void => {
|
||||
(get(historyModeStore) === 'auto' ? autoHistoryStore : manualHistoryStore).update((entries) => {
|
||||
if (get(historyModeStore) !== 'loader') {
|
||||
entries = entries.filter((entry) => time && entry.time != time);
|
||||
entries = entries.filter(({ id }) => idToClear && id != idToClear);
|
||||
}
|
||||
return entries;
|
||||
});
|
||||
@@ -77,6 +87,46 @@ export const getPreviousState = (auto: boolean): string => {
|
||||
};
|
||||
|
||||
export const restoreHistory = (data: HistoryEntry[]) => {
|
||||
// Should this replace the current history or append to it?
|
||||
manualHistoryStore.set(data);
|
||||
const entries = data.filter(validateEntry);
|
||||
const invalidEntryCount = data.length - entries.length;
|
||||
if (invalidEntryCount > 0) {
|
||||
console.error(`${invalidEntryCount} invalid history entries were removed.`);
|
||||
console.error(data);
|
||||
}
|
||||
if (entries.length > 0) {
|
||||
let entryCount = 0;
|
||||
(entries[0].type === 'auto' ? autoHistoryStore : manualHistoryStore).update((existing) => {
|
||||
const existingIDs = new Set(existing.map(({ id }) => id));
|
||||
const newEntries = entries.filter(({ id }) => !existingIDs.has(id));
|
||||
entryCount = newEntries.length;
|
||||
const combined = [...existing, ...newEntries];
|
||||
combined.sort((a, b) => b.time - a.time);
|
||||
return combined;
|
||||
});
|
||||
|
||||
alert(
|
||||
`${entryCount} entries restored. ${invalidEntryCount} invalid, ${
|
||||
entries.length - entryCount
|
||||
} duplicates.`
|
||||
);
|
||||
} else {
|
||||
alert('No valid entries found.');
|
||||
}
|
||||
};
|
||||
|
||||
export const injectHistoryIDs = (): void => {
|
||||
const setIDs = (entries: HistoryEntry[]) => {
|
||||
for (const entry of entries) {
|
||||
if (!entry.id) {
|
||||
entry.id = uuidV4();
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
};
|
||||
autoHistoryStore.update(setIDs);
|
||||
manualHistoryStore.update(setIDs);
|
||||
};
|
||||
|
||||
const validateEntry = (entry: HistoryEntry): boolean => {
|
||||
return entry.type && entry.state && entry.time && true;
|
||||
};
|
||||
|
||||
Vendored
+11
-7
@@ -68,13 +68,16 @@ export interface LoaderConfig {
|
||||
config: GistLoaderConfig | FileLoaderConfig;
|
||||
}
|
||||
export type HistoryType = 'auto' | 'manual' | 'loader';
|
||||
export interface HistoryEntry {
|
||||
state: State;
|
||||
time: number;
|
||||
name?: string;
|
||||
type: HistoryType;
|
||||
url?: string;
|
||||
}
|
||||
export type HistoryEntry = { id: string; state: State; time: number; url?: string } & (
|
||||
| {
|
||||
type: 'loader';
|
||||
name: string;
|
||||
}
|
||||
| {
|
||||
type: HistoryType;
|
||||
name?: string;
|
||||
}
|
||||
);
|
||||
|
||||
export interface DocConfig {
|
||||
[key: string]: {
|
||||
@@ -84,3 +87,4 @@ export interface DocConfig {
|
||||
}
|
||||
|
||||
export type Loader = (url: string) => Promise<State>;
|
||||
export type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { writable, get, type Writable } from 'svelte/store';
|
||||
import { persist, localStorage } from '@macfja/svelte-persistent-store';
|
||||
import { injectHistoryIDs } from '$lib/components/history/history';
|
||||
|
||||
interface MigrationState {
|
||||
version: number;
|
||||
}
|
||||
|
||||
const migrations: { [key: string]: () => void } = {
|
||||
injectHistoryIDs
|
||||
};
|
||||
|
||||
const migrationStore: Writable<MigrationState> = persist(
|
||||
writable({ version: -1 }),
|
||||
localStorage(),
|
||||
'migrations'
|
||||
);
|
||||
|
||||
export const applyMigrations = (): void => {
|
||||
const { version }: MigrationState = get(migrationStore);
|
||||
const allMigrations = Object.entries(migrations);
|
||||
if (version === allMigrations.length - 1) {
|
||||
return;
|
||||
}
|
||||
console.log(`Current migration version: v${version}. Migrating to v${allMigrations.length - 1}.`);
|
||||
for (let i = version + 1; i < allMigrations.length; i++) {
|
||||
const [key, fn] = allMigrations[i];
|
||||
console.log(`Applying migration ${i}: ${key}.`);
|
||||
fn();
|
||||
migrationStore.set({ version: i });
|
||||
}
|
||||
};
|
||||
@@ -6,10 +6,12 @@
|
||||
import { setTheme, themeStore } from '$lib/util/theme';
|
||||
import { toggleDarkTheme } from '$lib/util/state';
|
||||
import { initHandler } from '$lib/util/util';
|
||||
import { applyMigrations } from '$lib/util/migrations';
|
||||
|
||||
// This can be removed once https://github.com/sveltejs/kit/issues/1612 is fixed.
|
||||
// Then move it into src and vite will bundle it automatically.
|
||||
onMount(() => {
|
||||
applyMigrations();
|
||||
window.addEventListener('hashchange', async (ev) => {
|
||||
await initHandler();
|
||||
});
|
||||
|
||||
@@ -458,6 +458,11 @@
|
||||
dependencies:
|
||||
"@types/jest" "*"
|
||||
|
||||
"@types/uuid@^8.3.4":
|
||||
version "8.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
|
||||
integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
|
||||
|
||||
"@types/yauzl@^2.9.1":
|
||||
version "2.9.1"
|
||||
resolved "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz"
|
||||
|
||||
Reference in New Issue
Block a user