Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e953c320fb | ||
|
|
a4fe43572b | ||
|
|
04b898ee54 | ||
|
|
83175127b0 | ||
|
|
adc9dc7460 | ||
|
|
5a880f3efe | ||
|
|
d160e43eae | ||
|
|
732e44b6b9 |
+10
-16
@@ -1,4 +1,4 @@
|
|||||||
import { disableDebounce } from './util';
|
import { disableDebounce, verifyFileSize } from './util';
|
||||||
describe('Check actions', () => {
|
describe('Check actions', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.clearLocalStorage();
|
cy.clearLocalStorage();
|
||||||
@@ -27,28 +27,22 @@ describe('Check actions', () => {
|
|||||||
|
|
||||||
it('should download png and svg', () => {
|
it('should download png and svg', () => {
|
||||||
cy.clock(new Date(2022, 0, 1).getTime());
|
cy.clock(new Date(2022, 0, 1).getTime());
|
||||||
const downloadsFolder = Cypress.config('downloadsFolder');
|
|
||||||
|
|
||||||
const verifyFileSize = (fileType: string, size: number) => {
|
cy.get(`#downloadPNG`).click();
|
||||||
cy.get(`#download${fileType.toUpperCase()}`).click();
|
verifyFileSize('diagram', 'png', 21_000);
|
||||||
const fileName = `mermaid-diagram-2022-01-01-000000.${fileType}`;
|
|
||||||
const filePath = `${downloadsFolder}/${fileName}`;
|
|
||||||
cy.verifyDownload(fileName);
|
|
||||||
cy.readFile(filePath, null, {
|
|
||||||
log: false
|
|
||||||
}).then((buffer) => expect((buffer as ArrayBuffer).byteLength).to.be.gt(size));
|
|
||||||
cy.task('deleteFile', filePath);
|
|
||||||
};
|
|
||||||
|
|
||||||
verifyFileSize('png', 21_000);
|
cy.get(`#downloadSVG`).click();
|
||||||
verifyFileSize('svg', 11_000);
|
verifyFileSize('diagram', 'svg', 10_000);
|
||||||
|
|
||||||
// Verify downloaded file is different for different diagrams
|
// Verify downloaded file is different for different diagrams
|
||||||
cy.contains('Sample Diagrams').click();
|
cy.contains('Sample Diagrams').click();
|
||||||
cy.contains('ER Diagram').click();
|
cy.contains('ER Diagram').click();
|
||||||
|
|
||||||
verifyFileSize('png', 46_000);
|
cy.get(`#downloadPNG`).click();
|
||||||
verifyFileSize('svg', 12_000);
|
verifyFileSize('diagram', 'png', 46_000);
|
||||||
|
|
||||||
|
cy.get(`#downloadSVG`).click();
|
||||||
|
verifyFileSize('diagram', 'svg', 12_000);
|
||||||
|
|
||||||
cy.clock().invoke('restore');
|
cy.clock().invoke('restore');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { getEditor, disableDebounce } from './util';
|
import { getEditor, disableDebounce, verifyFileSnapshot } from './util';
|
||||||
|
|
||||||
describe('Save History', () => {
|
describe('Save History', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.clock();
|
cy.clock(new Date(2022, 0, 1).getTime());
|
||||||
cy.clearLocalStorage();
|
cy.clearLocalStorage();
|
||||||
cy.visit('/edit');
|
cy.visit('/edit');
|
||||||
disableDebounce();
|
disableDebounce();
|
||||||
@@ -10,6 +10,38 @@ describe('Save History', () => {
|
|||||||
cy.contains('History').click();
|
cy.contains('History').click();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cy.clock().invoke('restore');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load history from localstorage', () => {
|
||||||
|
cy.setLocalStorage(
|
||||||
|
'manualHistoryStore',
|
||||||
|
'[{"state":{"code":"graph TD\\n A[Halloween] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","updateEditor":false,"autoSync":true,"updateDiagram":false},"time":0,"type":"manual","id":"d7ea820e-21dd-418a-b984-fd58acde09df","name":"hollow-art"},{"state":{"code":"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","updateEditor":true,"autoSync":true,"updateDiagram":true},"time":0,"type":"manual","id":"b749ffc6-522b-4a44-86cf-7c1ffc3146b3","name":"helpful-ocean"}]'
|
||||||
|
);
|
||||||
|
cy.setLocalStorage(
|
||||||
|
'autoHistoryStore',
|
||||||
|
'[{"state":{"code":"graph TD\\n A[New Year] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","updateEditor":false,"autoSync":true,"updateDiagram":false},"time":0,"type":"auto","id":"69ea820e-522b-4a44-86cf-fd58acde09df","name":"barking-dog"},{"state":{"code":"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","updateEditor":true,"autoSync":true,"updateDiagram":true},"time":0,"type":"manual","id":"x749ffc6-21dd-418a-b984-7c1ffc3146b3","name":"needy-mosquito"}]'
|
||||||
|
);
|
||||||
|
cy.reload();
|
||||||
|
cy.contains('Actions').click();
|
||||||
|
cy.contains('History').click();
|
||||||
|
cy.get('#historyList').find('li').should('have.length', 2);
|
||||||
|
cy.get('#historyList').find('No items in History').should('not.exist');
|
||||||
|
cy.get('#historyList').contains('helpful-ocean');
|
||||||
|
cy.get('#historyList').contains('hollow-art');
|
||||||
|
cy.contains('Restore').click();
|
||||||
|
cy.contains('Halloween');
|
||||||
|
cy.contains('Timeline').click();
|
||||||
|
|
||||||
|
cy.get('#historyList').find('li').should('have.length', 2);
|
||||||
|
cy.get('#historyList').find('No items in History').should('not.exist');
|
||||||
|
cy.get('#historyList').contains('needy-mosquito');
|
||||||
|
cy.get('#historyList').contains('barking-dog');
|
||||||
|
cy.contains('Restore').click();
|
||||||
|
cy.contains('New Year');
|
||||||
|
});
|
||||||
|
|
||||||
it('should save when clicked', () => {
|
it('should save when clicked', () => {
|
||||||
cy.get('#historyList').find('li').should('have.length', 0);
|
cy.get('#historyList').find('li').should('have.length', 0);
|
||||||
cy.get('#historyList').contains('No items in History');
|
cy.get('#historyList').contains('No items in History');
|
||||||
@@ -64,4 +96,10 @@ describe('Save History', () => {
|
|||||||
}
|
}
|
||||||
cy.get('#historyList').find('li').should('have.length', 30);
|
cy.get('#historyList').find('li').should('have.length', 30);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should download history', () => {
|
||||||
|
cy.get('#saveHistory').click();
|
||||||
|
cy.get(`#downloadHistory`).click();
|
||||||
|
verifyFileSnapshot('history', 'json', 'A[Christmas] -->|Get money| B(Go shopping)');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,3 +9,35 @@ export const getEditor = ({ bottom = true, newline = false } = {}) =>
|
|||||||
.type(`${newline ? '{enter}' : cmd}`);
|
.type(`${newline ? '{enter}' : cmd}`);
|
||||||
|
|
||||||
export const disableDebounce = () => cy.setLocalStorage('noDebounce', 'true');
|
export const disableDebounce = () => cy.setLocalStorage('noDebounce', 'true');
|
||||||
|
|
||||||
|
const downloadsFolder = Cypress.config('downloadsFolder');
|
||||||
|
|
||||||
|
export const verifyFileSize = (
|
||||||
|
fileType: 'history' | 'diagram',
|
||||||
|
extension: string,
|
||||||
|
size: number
|
||||||
|
) => {
|
||||||
|
const fileName = `mermaid-${fileType}-2022-01-01-000000.${extension}`;
|
||||||
|
const filePath = `${downloadsFolder}/${fileName}`;
|
||||||
|
cy.verifyDownload(fileName);
|
||||||
|
cy.readFile(filePath, null, {
|
||||||
|
log: false
|
||||||
|
}).then((buffer) => expect((buffer as ArrayBuffer).byteLength).to.be.gt(size));
|
||||||
|
cy.task('deleteFile', filePath);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const verifyFileSnapshot = (
|
||||||
|
fileType: 'history' | 'diagram',
|
||||||
|
extension: string,
|
||||||
|
content: string
|
||||||
|
) => {
|
||||||
|
const fileName = `mermaid-${fileType}-2022-01-01-000000.${extension}`;
|
||||||
|
const filePath = `${downloadsFolder}/${fileName}`;
|
||||||
|
cy.verifyDownload(fileName);
|
||||||
|
cy.readFile(filePath, null, {
|
||||||
|
log: false
|
||||||
|
}).then((buffer) =>
|
||||||
|
expect(new TextDecoder('utf-8').decode(buffer as ArrayBuffer)).to.contain(content)
|
||||||
|
);
|
||||||
|
cy.task('deleteFile', filePath);
|
||||||
|
};
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ module.exports = {
|
|||||||
"1": "{\"code\":\"graph TD\\n A[Party] -->|Get money| B(Go shopping!!)\\n \",\"mermaid\":\"{\\n \\\"theme\\\": \\\"forest\\\",\\n \\\"test\\\": \\\"hello world\\\"\\n}\",\"updateEditor\":false,\"autoSync\":true,\"updateDiagram\":true,\"loader\":{\"type\":\"files\",\"config\":{\"codeURL\":\"https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/code.mmd\",\"configURL\":\"https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/config.json\"}}}"
|
"1": "{\"code\":\"graph TD\\n A[Party] -->|Get money| B(Go shopping!!)\\n \",\"mermaid\":\"{\\n \\\"theme\\\": \\\"forest\\\",\\n \\\"test\\\": \\\"hello world\\\"\\n}\",\"updateEditor\":false,\"autoSync\":true,\"updateDiagram\":true,\"loader\":{\"type\":\"files\",\"config\":{\"codeURL\":\"https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/code.mmd\",\"configURL\":\"https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/config.json\"}}}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"__version": "10.4.0",
|
"__version": "10.6.0",
|
||||||
"Auto sync tests": {
|
"Auto sync tests": {
|
||||||
"should dim diagram when code is edited": {
|
"should dim diagram when code is edited": {
|
||||||
"1": "{\"code\":\"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)\\n B --> C{Let me think}\\n C -->|One| D[Laptop]\\n C -->|Two| E[iPhone]\\n C -->|Three| F[fa:fa-car Car]\\n C --> Test\",\"mermaid\":\"{\\n \\\"theme\\\": \\\"default\\\"\\n}\",\"updateEditor\":false,\"autoSync\":false,\"updateDiagram\":false}"
|
"1": "{\"code\":\"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)\\n B --> C{Let me think}\\n C -->|One| D[Laptop]\\n C -->|Two| E[iPhone]\\n C -->|Three| F[fa:fa-car Car]\\n C --> Test\",\"mermaid\":\"{\\n \\\"theme\\\": \\\"default\\\"\\n}\",\"updateEditor\":false,\"autoSync\":false,\"updateDiagram\":false}"
|
||||||
|
|||||||
+4
-2
@@ -27,6 +27,7 @@
|
|||||||
"@testing-library/svelte": "3.2.1",
|
"@testing-library/svelte": "3.2.1",
|
||||||
"@types/mermaid": "8.2.9",
|
"@types/mermaid": "8.2.9",
|
||||||
"@types/pako": "1.0.3",
|
"@types/pako": "1.0.3",
|
||||||
|
"@types/uuid": "^8.3.4",
|
||||||
"@typescript-eslint/eslint-plugin": "5.35.1",
|
"@typescript-eslint/eslint-plugin": "5.35.1",
|
||||||
"@typescript-eslint/parser": "5.34.0",
|
"@typescript-eslint/parser": "5.34.0",
|
||||||
"@vitest/ui": "0.22.1",
|
"@vitest/ui": "0.22.1",
|
||||||
@@ -64,7 +65,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@analytics/google-analytics": "1.0.3",
|
"@analytics/google-analytics": "1.0.3",
|
||||||
"@macfja/svelte-persistent-store": "1.3.0",
|
"@macfja/svelte-persistent-store": "2.0.0",
|
||||||
"analytics": "0.8.1",
|
"analytics": "0.8.1",
|
||||||
"analytics-plugin-plausible": "^0.0.6",
|
"analytics-plugin-plausible": "^0.0.6",
|
||||||
"daisyui": "2.24.0",
|
"daisyui": "2.24.0",
|
||||||
@@ -75,7 +76,8 @@
|
|||||||
"monaco-mermaid": "1.0.6",
|
"monaco-mermaid": "1.0.6",
|
||||||
"pako": "2.0.4",
|
"pako": "2.0.4",
|
||||||
"random-word-slugs": "0.1.6",
|
"random-word-slugs": "0.1.6",
|
||||||
"svg-pan-zoom": "^3.6.1"
|
"svg-pan-zoom": "^3.6.1",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{ts,svelte,js,css,md,json}": [
|
"*.{ts,svelte,js,css,md,json}": [
|
||||||
|
|||||||
@@ -7,12 +7,15 @@
|
|||||||
clearHistoryData,
|
clearHistoryData,
|
||||||
getPreviousState,
|
getPreviousState,
|
||||||
historyStore,
|
historyStore,
|
||||||
loaderHistoryStore
|
loaderHistoryStore,
|
||||||
|
restoreHistory
|
||||||
} from './history';
|
} from './history';
|
||||||
import { notify, prompt } from '$lib/util/notify';
|
import { notify, prompt } from '$lib/util/notify';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import type { HistoryType, State, Tab } from '$lib/types';
|
import type { HistoryType, State, Tab } from '$lib/types';
|
||||||
|
import { logEvent } from '$lib/util/stats';
|
||||||
|
|
||||||
const HISTORY_SAVE_INTERVAL = 60000;
|
const HISTORY_SAVE_INTERVAL = 60000;
|
||||||
|
|
||||||
@@ -32,6 +35,39 @@
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const downloadHistory = () => {
|
||||||
|
const data = get(historyStore);
|
||||||
|
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `mermaid-history-${moment().format('YYYY-MM-DD-HHmmss')}.json`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
logEvent('history', {
|
||||||
|
action: 'download'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadHistory = () => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = 'application/json';
|
||||||
|
input.addEventListener('change', ({ target }: Event) => {
|
||||||
|
const file = (<HTMLInputElement>target).files[0];
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const data = JSON.parse(e.target.result as string);
|
||||||
|
restoreHistory(data);
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
});
|
||||||
|
input.click();
|
||||||
|
};
|
||||||
|
|
||||||
const saveHistory = (auto = false) => {
|
const saveHistory = (auto = false) => {
|
||||||
const currentState: string = getStateString();
|
const currentState: string = getStateString();
|
||||||
const previousState: string = getPreviousState(auto);
|
const previousState: string = getPreviousState(auto);
|
||||||
@@ -46,14 +82,14 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearHistory = (date?: number): void => {
|
const clearHistory = (id?: string): void => {
|
||||||
if (!date && !prompt('Clear all saved items?')) {
|
if (!id && !prompt('Clear all saved items?')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
clearHistoryData(date);
|
clearHistoryData(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const restoreHistory = (state: State): void => {
|
const restoreHistoryItem = (state: State): void => {
|
||||||
inputStateStore.set({ ...state, updateEditor: true, updateDiagram: true });
|
inputStateStore.set({ ...state, updateEditor: true, updateDiagram: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,6 +124,19 @@
|
|||||||
|
|
||||||
<Card on:select={tabSelectHandler} bind:isOpen {tabs} title="History">
|
<Card on:select={tabSelectHandler} bind:isOpen {tabs} title="History">
|
||||||
<div slot="actions">
|
<div slot="actions">
|
||||||
|
<button
|
||||||
|
id="uploadHistory"
|
||||||
|
class="btn btn-xs btn-secondary w-12"
|
||||||
|
on:click|stopPropagation={() => uploadHistory()}
|
||||||
|
title="Upload history"><i class="fa fa-upload" /></button>
|
||||||
|
{#if $historyStore.length > 0}
|
||||||
|
<button
|
||||||
|
id="downloadHistory"
|
||||||
|
class="btn btn-xs btn-secondary w-12"
|
||||||
|
on:click|stopPropagation={() => downloadHistory()}
|
||||||
|
title="Download history"><i class="fa fa-download" /></button>
|
||||||
|
{/if}
|
||||||
|
|
|
||||||
<button
|
<button
|
||||||
id="saveHistory"
|
id="saveHistory"
|
||||||
class="btn btn-xs btn-success w-12"
|
class="btn btn-xs btn-success w-12"
|
||||||
@@ -103,7 +152,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<ul class="p-2 space-y-2 overflow-auto h-56" id="historyList">
|
<ul class="p-2 space-y-2 overflow-auto h-56" id="historyList">
|
||||||
{#if $historyStore.length > 0}
|
{#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">
|
<li class="rounded p-2 shadow flex-col">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
@@ -121,10 +170,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 content-center">
|
<div class="flex gap-2 content-center">
|
||||||
<button class="btn btn-success" on:click={() => restoreHistory(state)}
|
<button class="btn btn-success" on:click={() => restoreHistoryItem(state)}
|
||||||
><i class="fas fa-undo mr-1" />Restore</button>
|
><i class="fas fa-undo mr-1" />Restore</button>
|
||||||
{#if type !== 'loader'}
|
{#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>
|
><i class="fas fa-trash-alt mr-1" />Delete</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,26 +1,28 @@
|
|||||||
import { derived, writable, get } from 'svelte/store';
|
import { derived, writable, get } from 'svelte/store';
|
||||||
import type { Readable, Writable } from 'svelte/store';
|
import type { Readable, Writable } from 'svelte/store';
|
||||||
import { persist, localStorage } from '@macfja/svelte-persistent-store';
|
import { persist, createLocalStorage } from '@macfja/svelte-persistent-store';
|
||||||
import { generateSlug } from 'random-word-slugs';
|
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';
|
||||||
|
import { logEvent } from '$lib/util/stats';
|
||||||
|
|
||||||
const MAX_AUTO_HISTORY_LENGTH = 30;
|
const MAX_AUTO_HISTORY_LENGTH = 30;
|
||||||
|
|
||||||
export const historyModeStore: Writable<HistoryType> = persist(
|
export const historyModeStore: Writable<HistoryType> = persist(
|
||||||
writable('manual'),
|
writable('manual'),
|
||||||
localStorage(),
|
createLocalStorage(),
|
||||||
'autoHistoryMode'
|
'autoHistoryMode'
|
||||||
);
|
);
|
||||||
|
|
||||||
const autoHistoryStore: Writable<HistoryEntry[]> = persist(
|
const autoHistoryStore: Writable<HistoryEntry[]> = persist(
|
||||||
writable([]),
|
writable([]),
|
||||||
localStorage(),
|
createLocalStorage(),
|
||||||
'autoHistoryStore'
|
'autoHistoryStore'
|
||||||
);
|
);
|
||||||
|
|
||||||
const manualHistoryStore: Writable<HistoryEntry[]> = persist(
|
const manualHistoryStore: Writable<HistoryEntry[]> = persist(
|
||||||
writable([]),
|
writable([]),
|
||||||
localStorage(),
|
createLocalStorage(),
|
||||||
'manualHistoryStore'
|
'manualHistoryStore'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -41,28 +43,39 @@ 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') {
|
if (entry.type === 'loader') {
|
||||||
loaderHistoryStore.update((entries) => [entry, ...entries]);
|
loaderHistoryStore.update((entries) => [entry, ...entries]);
|
||||||
return;
|
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]);
|
manualHistoryStore.update((entries) => [entry, ...entries]);
|
||||||
return;
|
logEvent('history', { action: 'save' });
|
||||||
}
|
}
|
||||||
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) => {
|
(get(historyModeStore) === 'auto' ? autoHistoryStore : manualHistoryStore).update((entries) => {
|
||||||
if (get(historyModeStore) !== 'loader') {
|
if (get(historyModeStore) !== 'loader') {
|
||||||
entries = entries.filter((entry) => time && entry.time != time);
|
entries = entries.filter(({ id }) => idToClear && id != idToClear);
|
||||||
|
logEvent('history', { action: 'clear', type: idToClear ? 'single' : 'all' });
|
||||||
}
|
}
|
||||||
return entries;
|
return entries;
|
||||||
});
|
});
|
||||||
@@ -75,3 +88,54 @@ export const getPreviousState = (auto: boolean): string => {
|
|||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const restoreHistory = (data: HistoryEntry[]) => {
|
||||||
|
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.`
|
||||||
|
);
|
||||||
|
logEvent('history', {
|
||||||
|
action: 'restore',
|
||||||
|
success: entryCount,
|
||||||
|
invalid: invalidEntryCount,
|
||||||
|
duplicates: entries.length - entryCount
|
||||||
|
});
|
||||||
|
} 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;
|
config: GistLoaderConfig | FileLoaderConfig;
|
||||||
}
|
}
|
||||||
export type HistoryType = 'auto' | 'manual' | 'loader';
|
export type HistoryType = 'auto' | 'manual' | 'loader';
|
||||||
export interface HistoryEntry {
|
export type HistoryEntry = { id: string; state: State; time: number; url?: string } & (
|
||||||
state: State;
|
| {
|
||||||
time: number;
|
type: 'loader';
|
||||||
name?: string;
|
name: string;
|
||||||
type: HistoryType;
|
}
|
||||||
url?: string;
|
| {
|
||||||
}
|
type: HistoryType;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export interface DocConfig {
|
export interface DocConfig {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
@@ -84,3 +87,4 @@ export interface DocConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Loader = (url: string) => Promise<State>;
|
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,35 @@
|
|||||||
|
import { writable, get, type Writable } from 'svelte/store';
|
||||||
|
import { persist, createLocalStorage } from '@macfja/svelte-persistent-store';
|
||||||
|
import { injectHistoryIDs } from '$lib/components/history/history';
|
||||||
|
import { logEvent } from './stats';
|
||||||
|
|
||||||
|
interface MigrationState {
|
||||||
|
version: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const migrations: { [key: string]: () => void } = {
|
||||||
|
injectHistoryIDs
|
||||||
|
};
|
||||||
|
|
||||||
|
const migrationStore: Writable<MigrationState> = persist(
|
||||||
|
writable({ version: -1 }),
|
||||||
|
createLocalStorage(),
|
||||||
|
'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();
|
||||||
|
logEvent('migration', { key });
|
||||||
|
migrationStore.set({ version: i });
|
||||||
|
}
|
||||||
|
logEvent('migration', { status: 'complete', from: version, to: allMigrations.length - 1 });
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { writable, get, derived } from 'svelte/store';
|
import { writable, get, derived } from 'svelte/store';
|
||||||
import { persist, localStorage } from '@macfja/svelte-persistent-store';
|
import { persist, createLocalStorage } from '@macfja/svelte-persistent-store';
|
||||||
import { saveStatistics } from './stats';
|
import { saveStatistics } from './stats';
|
||||||
import { serializeState, deserializeState } from './serde';
|
import { serializeState, deserializeState } from './serde';
|
||||||
import { cmdKey } from './util';
|
import { cmdKey } from './util';
|
||||||
@@ -40,7 +40,7 @@ const urlParseFailedState = `graph TD
|
|||||||
click D href "https://github.com/mermaid-js/mermaid-live-editor/issues/new?assignees=&labels=bug&template=bug_report.md&title=Broken%20link" "Raise issue"`;
|
click D href "https://github.com/mermaid-js/mermaid-live-editor/issues/new?assignees=&labels=bug&template=bug_report.md&title=Broken%20link" "Raise issue"`;
|
||||||
|
|
||||||
// inputStateStore handles all updates and is shared externally when exporting via URL, History, etc.
|
// inputStateStore handles all updates and is shared externally when exporting via URL, History, etc.
|
||||||
export const inputStateStore = persist(writable(defaultState), localStorage(), 'codeStore');
|
export const inputStateStore = persist(writable(defaultState), createLocalStorage(), 'codeStore');
|
||||||
|
|
||||||
// All internal reads should be done via stateStore, but it should not be persisted/shared externally.
|
// All internal reads should be done via stateStore, but it should not be persisted/shared externally.
|
||||||
export const stateStore: Readable<ValidatedState> = derived([inputStateStore], ([state]) => {
|
export const stateStore: Readable<ValidatedState> = derived([inputStateStore], ([state]) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
import type { Writable } from 'svelte/store';
|
import type { Writable } from 'svelte/store';
|
||||||
import { persist, localStorage } from '@macfja/svelte-persistent-store';
|
import { persist, createLocalStorage } from '@macfja/svelte-persistent-store';
|
||||||
import { logEvent } from './stats';
|
import { logEvent } from './stats';
|
||||||
|
|
||||||
export interface ThemeConfig {
|
export interface ThemeConfig {
|
||||||
@@ -12,7 +12,7 @@ export const themeStore: Writable<ThemeConfig> = persist(
|
|||||||
writable({
|
writable({
|
||||||
isDark: false
|
isDark: false
|
||||||
}),
|
}),
|
||||||
localStorage(),
|
createLocalStorage(),
|
||||||
'themeStore'
|
'themeStore'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,12 @@
|
|||||||
import { setTheme, themeStore } from '$lib/util/theme';
|
import { setTheme, themeStore } from '$lib/util/theme';
|
||||||
import { toggleDarkTheme } from '$lib/util/state';
|
import { toggleDarkTheme } from '$lib/util/state';
|
||||||
import { initHandler } from '$lib/util/util';
|
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.
|
// 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.
|
// Then move it into src and vite will bundle it automatically.
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
applyMigrations();
|
||||||
window.addEventListener('hashchange', async (ev) => {
|
window.addEventListener('hashchange', async (ev) => {
|
||||||
await initHandler();
|
await initHandler();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -245,13 +245,19 @@
|
|||||||
"@jridgewell/resolve-uri" "^3.0.3"
|
"@jridgewell/resolve-uri" "^3.0.3"
|
||||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||||
|
|
||||||
"@macfja/svelte-persistent-store@1.3.0":
|
"@macfja/serializer@^1.0.2":
|
||||||
version "1.3.0"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@macfja/svelte-persistent-store/-/svelte-persistent-store-1.3.0.tgz#e3ef6440cde04657ab77284dacd447b12a89cb1e"
|
resolved "https://registry.yarnpkg.com/@macfja/serializer/-/serializer-1.0.2.tgz#9170d0a298787244d9c9b7d31bbbe64762dd1677"
|
||||||
integrity sha512-3lPsEAFe28zCNTyA+/pYfUgzRrH6KZvoG0i4IHVhYOs3Xzaqg9hN7LdHa+Qrv7GncXn/6XhDEpb36hB0f7CzXA==
|
integrity sha512-ORAF9M5DtarMV6RqZjvVdfsHQ0yTdBDgpA3dFmRBdg/A4TKIwPBjZ9NMjGFQZmJXt/GAHEEMhixu3Rkg5eGWuA==
|
||||||
|
|
||||||
|
"@macfja/svelte-persistent-store@2.0.0":
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@macfja/svelte-persistent-store/-/svelte-persistent-store-2.0.0.tgz#2505e1b0a66966355f26e29b1806aec8954ab4c1"
|
||||||
|
integrity sha512-JX9dre6pFG+lK0GiHefXv9xKEtkSSzHAWsjLj8Kgw/gW5KMzWKaTqvJ0e98Xiv72fzUWWWqgW5Dz5DpHVxbbwA==
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@macfja/serializer" "^1.0.2"
|
||||||
browser-cookies "^1.2.0"
|
browser-cookies "^1.2.0"
|
||||||
esserializer "^1.3.2"
|
cyrup "^0.8.1"
|
||||||
idb-keyval "^5.1.3"
|
idb-keyval "^5.1.3"
|
||||||
|
|
||||||
"@nodelib/fs.scandir@2.1.5":
|
"@nodelib/fs.scandir@2.1.5":
|
||||||
@@ -458,6 +464,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/jest" "*"
|
"@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":
|
"@types/yauzl@^2.9.1":
|
||||||
version "2.9.1"
|
version "2.9.1"
|
||||||
resolved "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz"
|
resolved "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz"
|
||||||
@@ -1437,6 +1448,11 @@ cypress@10.6.0:
|
|||||||
untildify "^4.0.0"
|
untildify "^4.0.0"
|
||||||
yauzl "^2.10.0"
|
yauzl "^2.10.0"
|
||||||
|
|
||||||
|
cyrup@^0.8.1:
|
||||||
|
version "0.8.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/cyrup/-/cyrup-0.8.1.tgz#f2be38ea4a6e2f52ebe96731d56c09c2cb8a0f45"
|
||||||
|
integrity sha512-nBtYqOSyaXLlUbr0KzDiHsvR6jqqrJmbyXBUur2dC1axZjlFm+v++2SYFXKMQC+Dj+0ywy+9WJjV8Eu+6vemYQ==
|
||||||
|
|
||||||
d3-array@1, d3-array@^1.1.1, d3-array@^1.2.0:
|
d3-array@1, d3-array@^1.1.1, d3-array@^1.2.0:
|
||||||
version "1.2.4"
|
version "1.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f"
|
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f"
|
||||||
@@ -2568,11 +2584,6 @@ esrecurse@^4.3.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
estraverse "^5.2.0"
|
estraverse "^5.2.0"
|
||||||
|
|
||||||
esserializer@^1.3.2:
|
|
||||||
version "1.3.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/esserializer/-/esserializer-1.3.2.tgz#4788444a7d08108a57ddc6a77602e8c853d3f8da"
|
|
||||||
integrity sha512-48xQjlQ9Wr6dUfjLAC1IQF0bMIk6m62mN7BH0PklRP0NDfm2ISY16EXM8t2Wfc6WcLKser+X8ggeeFwieBkufQ==
|
|
||||||
|
|
||||||
estraverse@^4.1.1:
|
estraverse@^4.1.1:
|
||||||
version "4.3.0"
|
version "4.3.0"
|
||||||
resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz"
|
resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz"
|
||||||
|
|||||||
Reference in New Issue
Block a user