From fb3b75838967f18103167bda15f3ebf87057bc5b Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Tue, 15 Nov 2022 15:00:06 +0530 Subject: [PATCH] fix: Case chang --- src/lib/components/Actions.svelte | 260 ++++++++++++++++++ src/lib/components/Card/Card.svelte | 31 +++ src/lib/components/Card/Tabs.svelte | 52 ++++ .../Card/__snapshots__/card.test.ts.snap | 3 + src/lib/components/Card/card.test.ts | 24 ++ .../{editor.svelte => Editor.svelte} | 0 src/lib/components/History/History.svelte | 192 +++++++++++++ src/lib/components/History/history.test.ts | 120 ++++++++ src/lib/components/History/history.ts | 141 ++++++++++ src/lib/components/Navbar.svelte | 82 ++++++ src/lib/components/Preset.svelte | 144 ++++++++++ src/lib/components/Theme.svelte | 64 +++++ src/lib/components/View.svelte | 157 +++++++++++ src/lib/components/history/history.test.ts | 2 +- 14 files changed, 1271 insertions(+), 1 deletion(-) create mode 100644 src/lib/components/Actions.svelte create mode 100644 src/lib/components/Card/Card.svelte create mode 100644 src/lib/components/Card/Tabs.svelte create mode 100644 src/lib/components/Card/__snapshots__/card.test.ts.snap create mode 100644 src/lib/components/Card/card.test.ts rename src/lib/components/{editor.svelte => Editor.svelte} (100%) create mode 100644 src/lib/components/History/History.svelte create mode 100644 src/lib/components/History/history.test.ts create mode 100644 src/lib/components/History/history.ts create mode 100644 src/lib/components/Navbar.svelte create mode 100644 src/lib/components/Preset.svelte create mode 100644 src/lib/components/Theme.svelte create mode 100644 src/lib/components/View.svelte diff --git a/src/lib/components/Actions.svelte b/src/lib/components/Actions.svelte new file mode 100644 index 00000000..13fefaa7 --- /dev/null +++ b/src/lib/components/Actions.svelte @@ -0,0 +1,260 @@ + + + +
+ {#if isClipboardAvailable()} + + {/if} + + + + + + + + + + + + +
+ PNG size + + + + + + + {#if imagemodeselected !== 'auto'} + + {/if} +
+ +
+ + +
+ +
+ + +
+ {#if isNetlify} + + {/if} +
+
diff --git a/src/lib/components/Card/Card.svelte b/src/lib/components/Card/Card.svelte new file mode 100644 index 00000000..89173cdd --- /dev/null +++ b/src/lib/components/Card/Card.svelte @@ -0,0 +1,31 @@ + + +
+
(isOpen = !isOpen)} + on:keypress={() => (isOpen = !isOpen)}> +
+ +
+ +
+
+
+ {#if isOpen} +
+ +
+ {/if} +
diff --git a/src/lib/components/Card/Tabs.svelte b/src/lib/components/Card/Tabs.svelte new file mode 100644 index 00000000..3c7c8f9d --- /dev/null +++ b/src/lib/components/Card/Tabs.svelte @@ -0,0 +1,52 @@ + + +
+ (isOpen = !isOpen)} + on:keypress|stopPropagation={() => (isOpen = !isOpen)}> + {#if isCloseable} + + {/if} + {title} + {#if isOpen && tabs} +
    + {#each tabs as tab} +
    toggleTabs(tab)} + on:keypress|stopPropagation={() => toggleTabs(tab)}> + + {tab.title} +
    + {/each} +
+ {/if} +
+ + diff --git a/src/lib/components/Card/__snapshots__/card.test.ts.snap b/src/lib/components/Card/__snapshots__/card.test.ts.snap new file mode 100644 index 00000000..eb662c95 --- /dev/null +++ b/src/lib/components/Card/__snapshots__/card.test.ts.snap @@ -0,0 +1,3 @@ +// Vitest Snapshot v1 + +exports[`card.svelte > mounts 1`] = `"
TabTest
    title1
    title2
"`; diff --git a/src/lib/components/Card/card.test.ts b/src/lib/components/Card/card.test.ts new file mode 100644 index 00000000..4624ab48 --- /dev/null +++ b/src/lib/components/Card/card.test.ts @@ -0,0 +1,24 @@ +import { cleanup, render } from '@testing-library/svelte'; +import { describe, expect, it, afterEach } from 'vitest'; +import Card from './Card.svelte'; + +describe('card.svelte', () => { + // TODO: @testing-library/svelte claims to add this automatically but it doesn't work without explicit afterEach + afterEach(() => cleanup()); + + it('mounts', () => { + const { container } = render(Card, { + title: 'TabTest', + tabs: [ + { id: 't1', title: 'title1' }, + { id: 't2', title: 'title2' } + ] + }); + expect(container).toBeTruthy(); + expect(container).toHaveTextContent('TabTest'); + expect(container).toHaveTextContent('title1'); + expect(container).toHaveTextContent('title2'); + expect(container).not.toHaveTextContent('title3'); + expect(container.innerHTML).toMatchSnapshot(); + }); +}); diff --git a/src/lib/components/editor.svelte b/src/lib/components/Editor.svelte similarity index 100% rename from src/lib/components/editor.svelte rename to src/lib/components/Editor.svelte diff --git a/src/lib/components/History/History.svelte b/src/lib/components/History/History.svelte new file mode 100644 index 00000000..87500041 --- /dev/null +++ b/src/lib/components/History/History.svelte @@ -0,0 +1,192 @@ + + + +
+ + {#if $historyStore.length > 0} + + {/if} + | + + {#if $historyModeStore !== 'loader'} + + {/if} +
+
    + {#if $historyStore.length > 0} + {#each $historyStore as { id, state, time, name, url, type }} +
  • +
    +
    +
    + {#if url} + {name} + {:else} + {name} + {/if} + {relativeTime(time)} +
    +
    +
    + + {#if type !== 'loader'} + + {/if} +
    +
    +
  • + {/each} + {:else} +
    + No items in History
    + Click the Save button to save current state and restore it later.
    + Timeline will automatically be saved every minute. +
    + {/if} +
+
diff --git a/src/lib/components/History/history.test.ts b/src/lib/components/History/history.test.ts new file mode 100644 index 00000000..cdb50830 --- /dev/null +++ b/src/lib/components/History/history.test.ts @@ -0,0 +1,120 @@ +import type { HistoryEntry } from '$lib/types'; +import { describe, it, expect } from 'vitest'; +import { + addHistoryEntry, + injectHistoryIDs, + clearHistoryData, + historyModeStore, + historyStore +} from './history'; +import { defaultState } from '$lib/util/state'; +import { get } from 'svelte/store'; + +describe('history', () => { + it('should handle saving individual history entry', () => { + expect(window.localStorage.getItem('manualHistoryStore')).toBe('[]'); + expect(window.localStorage.getItem('autoHistoryStore')).toBe('[]'); + + addHistoryEntry({ + state: defaultState, + time: 12345, + type: 'manual' + }); + + const [manualEntry]: HistoryEntry[] = JSON.parse( + window.localStorage.getItem('manualHistoryStore') + ); + + expect(manualEntry.time).toBe(12345); + expect(manualEntry.type).toBe('manual'); + expect(manualEntry.name).not.toBeNull(); + expect(manualEntry.state).not.toBeNull(); + + addHistoryEntry({ + state: defaultState, + time: 54321, + type: 'auto' + }); + + const [autoEntry]: HistoryEntry[] = JSON.parse(window.localStorage.getItem('autoHistoryStore')); + + expect(autoEntry.time).toBe(54321); + expect(autoEntry.type).toBe('auto'); + expect(autoEntry.name).not.toBeNull(); + expect(autoEntry.state).not.toBeNull(); + + historyModeStore.set('manual'); + clearHistoryData(); + historyModeStore.set('auto'); + clearHistoryData(); + expect(window.localStorage.getItem('manualHistoryStore')).toBe('[]'); + expect(window.localStorage.getItem('autoHistoryStore')).toBe('[]'); + }); + + it('should clear history entries', () => { + addHistoryEntry({ + state: defaultState, + time: 12345, + type: 'manual' + }); + addHistoryEntry({ + state: { ...defaultState, code: 'graph TD\\n A[Christmas] -->|Get money| B(Go shopping)' }, + time: 123456, + type: 'manual' + }); + + historyModeStore.set('manual'); + const store: HistoryEntry[] = get(historyStore); + expect(store.length).toBe(2); + clearHistoryData(store[1].id); + expect(get(historyStore).length).toBe(1); + clearHistoryData(); + expect(get(historyStore).length).toBe(0); + + historyModeStore.set('auto'); + addHistoryEntry({ + state: defaultState, + time: 54321, + type: 'auto' + }); + addHistoryEntry({ + state: { ...defaultState, code: 'graph TD\\n A[Christmas] -->|Get money| B(Go shopping)' }, + time: 654321, + type: 'auto' + }); + expect(get(historyStore).length).toBe(2); + clearHistoryData(); + expect(get(historyStore).length).toBe(0); + // Test calling when history is empty + clearHistoryData(); + expect(get(historyStore).length).toBe(0); + }); +}); + +describe('history migration', () => { + it('should inject history IDs as migration', () => { + window.localStorage.setItem( + 'manualHistoryStore', + '[{"state":{"code":"graph TD\\n A[Halloween] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","autoSync":true,"updateDiagram":false},"time":0,"type":"manual","name":"hollow-art"},{"state":{"code":"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","autoSync":true,"updateDiagram":true},"time":0,"type":"manual","name":"helpful-ocean"}]' + ); + window.localStorage.setItem( + 'autoHistoryStore', + '[{"state":{"code":"graph TD\\n A[New Year] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","autoSync":true,"updateDiagram":false},"time":0,"type":"auto","name":"barking-dog"},{"state":{"code":"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","autoSync":true,"updateDiagram":true},"time":0,"type":"manual","name":"needy-mosquito"}]' + ); + let manualHistoryStore: HistoryEntry[] = JSON.parse( + window.localStorage.getItem('manualHistoryStore') + ); + let autoHistoryStore: HistoryEntry[] = JSON.parse( + window.localStorage.getItem('autoHistoryStore') + ); + expect(manualHistoryStore.every(({ id }) => id !== undefined)).toBe(false); + expect(autoHistoryStore.every(({ id }) => id !== undefined)).toBe(false); + + injectHistoryIDs(); + + manualHistoryStore = JSON.parse(window.localStorage.getItem('manualHistoryStore')); + autoHistoryStore = JSON.parse(window.localStorage.getItem('autoHistoryStore')); + expect(manualHistoryStore.every(({ id }) => id !== undefined)).toBe(true); + expect(autoHistoryStore.every(({ id }) => id !== undefined)).toBe(true); + }); +}); diff --git a/src/lib/components/History/history.ts b/src/lib/components/History/history.ts new file mode 100644 index 00000000..20f19ba3 --- /dev/null +++ b/src/lib/components/History/history.ts @@ -0,0 +1,141 @@ +import { derived, writable, get } from 'svelte/store'; +import type { Readable, Writable } from 'svelte/store'; +import { persist, localStorage } from '$lib/util/persist'; +import { generateSlug } from 'random-word-slugs'; +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; + +export const historyModeStore: Writable = persist( + writable('manual'), + localStorage(), + 'autoHistoryMode' +); + +const autoHistoryStore: Writable = persist( + writable([]), + localStorage(), + 'autoHistoryStore' +); + +const manualHistoryStore: Writable = persist( + writable([]), + localStorage(), + 'manualHistoryStore' +); + +export const loaderHistoryStore: Writable = writable([] as HistoryEntry[]); + +export const historyStore: Readable = derived( + [historyModeStore, autoHistoryStore, manualHistoryStore, loaderHistoryStore], + ([historyMode, autoHistories, manualHistories, loadedHistories], set) => { + if (historyMode === 'auto') { + set(autoHistories); + } else if (historyMode === 'manual') { + set(manualHistories); + } else if (historyMode === 'loader') { + set(loadedHistories); + } else { + set(autoHistories); + } + } +); + +export const addHistoryEntry = (entryToAdd: Optional): void => { + const entry: HistoryEntry = { + ...entryToAdd, + id: uuidV4() + }; + + if (entry.type === 'loader') { + loaderHistoryStore.update((entries) => [entry, ...entries]); + return; + } + + 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]); + logEvent('history', { action: 'save' }); + } +}; + +export const clearHistoryData = (idToClear?: string): void => { + (get(historyModeStore) === 'auto' ? autoHistoryStore : manualHistoryStore).update((entries) => { + if (get(historyModeStore) !== 'loader') { + entries = entries.filter(({ id }) => idToClear && id != idToClear); + logEvent('history', { action: 'clear', type: idToClear ? 'single' : 'all' }); + } + return entries; + }); +}; + +export const getPreviousState = (auto: boolean): string => { + const entries = get(auto ? autoHistoryStore : manualHistoryStore); + if (entries.length > 0) { + return JSON.stringify(entries[0].state); + } + 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; +}; diff --git a/src/lib/components/Navbar.svelte b/src/lib/components/Navbar.svelte new file mode 100644 index 00000000..c434b7e4 --- /dev/null +++ b/src/lib/components/Navbar.svelte @@ -0,0 +1,82 @@ + + + + + + + diff --git a/src/lib/components/Preset.svelte b/src/lib/components/Preset.svelte new file mode 100644 index 00000000..059b0134 --- /dev/null +++ b/src/lib/components/Preset.svelte @@ -0,0 +1,144 @@ + + + +
+ {#each diagramOrder as sample} + + {/each} +
+
diff --git a/src/lib/components/Theme.svelte b/src/lib/components/Theme.svelte new file mode 100644 index 00000000..2e860077 --- /dev/null +++ b/src/lib/components/Theme.svelte @@ -0,0 +1,64 @@ + + + diff --git a/src/lib/components/View.svelte b/src/lib/components/View.svelte new file mode 100644 index 00000000..dc5cfc6d --- /dev/null +++ b/src/lib/components/View.svelte @@ -0,0 +1,157 @@ + + +{#if (error && $stateStore.error instanceof Error) || outOfSync} +
+ {#if error} + {$stateStore.error} + {:else} + Diagram out of sync.
+ Press (Sync button) or {cmdKey} + Enter to sync. + {/if} +
+{/if} + +
+
+
+ + diff --git a/src/lib/components/history/history.test.ts b/src/lib/components/history/history.test.ts index d5a0242b..cdb50830 100644 --- a/src/lib/components/history/history.test.ts +++ b/src/lib/components/history/history.test.ts @@ -7,7 +7,7 @@ import { historyModeStore, historyStore } from './history'; -import { defaultState } from '../../util/state'; +import { defaultState } from '$lib/util/state'; import { get } from 'svelte/store'; describe('history', () => {