diff --git a/cypress/integration/loadSite.spec.ts b/cypress/integration/loadSite.spec.ts index 0c58d373..1d49cc51 100644 --- a/cypress/integration/loadSite.spec.ts +++ b/cypress/integration/loadSite.spec.ts @@ -29,4 +29,18 @@ describe('Site Loads', () => { cy.contains('Class Diagram').click(); cy.contains('classDiagram'); }); + + it('should load diagram from gist', () => { + cy.visit(`/edit?gist=https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a`); + cy.contains('Party'); + cy.getLocalStorage('codeStore').snapshot(); + }); + + it('should load diagram from raw files', () => { + cy.visit( + `/edit?code=https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/a76fc7c6d19b6b772d86ac87fa700ed2e2117c56/code.mmd&config=https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/a76fc7c6d19b6b772d86ac87fa700ed2e2117c56/config.json` + ); + cy.contains('Party'); + cy.getLocalStorage('codeStore').snapshot(); + }); }); diff --git a/cypress/snapshots.js b/cypress/snapshots.js index f94d5e4a..53fec1c2 100644 --- a/cypress/snapshots.js +++ b/cypress/snapshots.js @@ -1,13 +1,4 @@ module.exports = { - "Site Loads": { - "Check Home page load": { - "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 \",\"mermaid\":\"{\\n \\\"theme\\\": \\\"default\\\"\\n}\",\"updateEditor\":true,\"autoSync\":true,\"updateDiagram\":true}" - }, - "Check Redirect from old URL": { - "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]\",\"mermaid\":\"{\\n \\\"theme\\\": \\\"default\\\"\\n}\",\"updateEditor\":false,\"autoSync\":true,\"updateDiagram\":false}" - } - }, - "__version": "7.3.0", "Auto sync tests": { "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}" @@ -15,5 +6,17 @@ module.exports = { "should not dim diagram when code is in sync": { "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 --> Testing\",\"mermaid\":\"{\\n \\\"theme\\\": \\\"default\\\"\\n}\",\"updateEditor\":false,\"autoSync\":true,\"updateDiagram\":false}" } - } + }, + "Site Loads": { + "Check Home page load": { + "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 \",\"mermaid\":\"{\\n \\\"theme\\\": \\\"default\\\"\\n}\",\"updateEditor\":true,\"autoSync\":true,\"updateDiagram\":true}" + }, + "Check Redirect from old URL": { + "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]\",\"mermaid\":\"{\\n \\\"theme\\\": \\\"default\\\"\\n}\",\"updateEditor\":false,\"autoSync\":true,\"updateDiagram\":true}" + }, + "should load diagram from gist": { + "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}" + } + }, + "__version": "7.4.0" } diff --git a/src/lib/components/card/tabs.svelte b/src/lib/components/card/tabs.svelte index fc11b338..dc479b6f 100644 --- a/src/lib/components/card/tabs.svelte +++ b/src/lib/components/card/tabs.svelte @@ -7,7 +7,7 @@ export let title: string; export let isOpen = false; - let activeTabID: string = tabs[0]?.id; + $: activeTabID = tabs[0]?.id; const dispatch = createEventDispatcher(); const toggleTabs = (tab: Tab) => { diff --git a/src/lib/components/history/history.svelte b/src/lib/components/history/history.svelte index 1010d40d..96350cfd 100644 --- a/src/lib/components/history/history.svelte +++ b/src/lib/components/history/history.svelte @@ -3,29 +3,30 @@ import { codeStore, getStateString } from '$lib/util/state'; import { addHistoryEntry, - autoHistoryMode, + historyModeStore, clearHistoryData, getPreviousState, - historyStore + historyStore, + loaderHistoryStore } from './history'; import { notify, prompt } from '$lib/util/notify'; import { onMount } from 'svelte'; import moment from 'moment'; - import type { State, Tab } from '$lib/types'; + import type { HistoryType, State, Tab } from '$lib/types'; const HISTORY_SAVE_INTERVAL = 60000; const tabSelectHandler = (message: CustomEvent) => { - autoHistoryMode.set('timeline' === message.detail.id); + historyModeStore.set(message.detail.id as HistoryType); }; - const tabs: Tab[] = [ + let tabs: Tab[] = [ { - id: 'saved', + id: 'manual', title: 'Saved', icon: 'far fa-bookmark' }, { - id: 'timeline', + id: 'auto', title: 'Timeline', icon: 'fas fa-history' } @@ -38,7 +39,7 @@ addHistoryEntry({ state: $codeStore, time: Date.now(), - auto + type: auto ? 'auto' : 'manual' }); } else if (!auto) { notify('State already saved.'); @@ -62,12 +63,26 @@ }; onMount(() => { - autoHistoryMode.set(false); + historyModeStore.set('manual'); setInterval(() => { saveHistory(true); }, HISTORY_SAVE_INTERVAL); }); + loaderHistoryStore.subscribe((entries) => { + if (entries.length > 0 && tabs.length === 2) { + tabs = [ + { + id: 'loader', + title: 'Revisions', + icon: 'fab fa-git-alt' + }, + ...tabs + ]; + historyModeStore.set('loader'); + } + }); + let isOpen = true; @@ -78,20 +93,30 @@ class="btn" on:click|stopPropagation={() => saveHistory()} title="Save current state"> - + {#if $historyModeStore !== 'loader'} + + {/if}
    {#if $historyStore.length > 0} - {#each $historyStore as { state, time, name }} + {#each $historyStore as { state, time, name, url, type }}
  • - {name} + {#if url} + {name} + {:else} + {name} + {/if} {relativeTime(time)}
    @@ -99,9 +124,11 @@ - + {#if type !== 'loader'} + + {/if}
  • diff --git a/src/lib/components/history/history.ts b/src/lib/components/history/history.ts index 4b7677a6..8dc0809b 100644 --- a/src/lib/components/history/history.ts +++ b/src/lib/components/history/history.ts @@ -1,12 +1,12 @@ import { derived, Readable, Writable, writable, get } from 'svelte/store'; import { persist, localStorage } from '@macfja/svelte-persistent-store'; import { generateSlug } from 'random-word-slugs'; -import type { HistoryEntry } from '../../types'; +import type { HistoryEntry, HistoryType } from '$lib/types'; const MAX_AUTO_HISTORY_LENGTH = 30; -export const autoHistoryMode: Writable = persist( - writable(true), +export const historyModeStore: Writable = persist( + writable('manual'), localStorage(), 'autoHistoryMode' ); @@ -23,17 +23,30 @@ const manualHistoryStore: Writable = persist( 'manualHistoryStore' ); +export const loaderHistoryStore: Writable = writable([] as HistoryEntry[]); + export const historyStore: Readable = derived( - [autoHistoryMode, autoHistoryStore, manualHistoryStore], - ([autoMode, autoHistories, manualHistories], set) => { - set(autoMode ? autoHistories : manualHistories); + [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 = (entry: HistoryEntry): void => { + if (entry.type === 'loader') { + loaderHistoryStore.update((entries) => [entry, ...entries]); + return; + } entry.name = generateSlug(2); - - if (!entry.auto) { + if (entry.type !== 'auto') { manualHistoryStore.update((entries) => [entry, ...entries]); return; } @@ -46,9 +59,12 @@ export const addHistoryEntry = (entry: HistoryEntry): void => { }; export const clearHistoryData = (time?: number): void => { - (get(autoHistoryMode) ? autoHistoryStore : manualHistoryStore).update((entries) => - entries.filter((entry) => time && entry.time != time) - ); + (get(historyModeStore) === 'auto' ? autoHistoryStore : manualHistoryStore).update((entries) => { + if (get(historyModeStore) !== 'loader') { + entries = entries.filter((entry) => time && entry.time != time); + } + return entries; + }); }; export const getPreviousState = (auto: boolean): string => { diff --git a/src/lib/types.d.ts b/src/lib/types.d.ts index 0a393d08..3969b14d 100644 --- a/src/lib/types.d.ts +++ b/src/lib/types.d.ts @@ -29,14 +29,28 @@ export interface State { updateEditor: boolean; updateDiagram: boolean; autoSync: boolean; + loader?: LoaderConfig; } +export interface GistLoaderConfig { + url: string; +} + +export interface FileLoaderConfig { + codeURL: string; + configURL?: string; +} +export interface LoaderConfig { + type: 'gist' | 'files'; + config: GistLoaderConfig | FileLoaderConfig; +} +export type HistoryType = 'auto' | 'manual' | 'loader'; export interface HistoryEntry { state: State; time: number; name?: string; - auto: boolean; + type: HistoryType; + url?: string; } -type MermaidData = { code: string; config?: string }; -type Loader = (url: string) => Promise; +type Loader = (url: string) => Promise; diff --git a/src/lib/util/fileLoaders/gist.ts b/src/lib/util/fileLoaders/gist.ts index bbcefea0..2a9968f6 100644 --- a/src/lib/util/fileLoaders/gist.ts +++ b/src/lib/util/fileLoaders/gist.ts @@ -1,6 +1,9 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { MermaidData } from '$lib/types'; +import type { State } from '$lib/types'; +import { defaultState } from '../state'; +import { addHistoryEntry } from '../../components/history/history'; const codeFileName = 'code.mmd'; const configFileName = 'config.json'; @@ -16,19 +19,79 @@ const getFileContent = async (file: any): Promise => { return file.content; }; -export const getGistData = async (gistURL: string): Promise => { - const gistID = gistURL.split('/').pop(); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { files } = await (await fetch(`https://api.github.com/gists/${gistID}`)).json(); +interface GistData { + code: string; + config?: string; + author: string; + time: number; + version: string; + url: string; +} +const getGistData = async (gistURL: string): Promise => { + const [_, __, gistID, revisionID] = gistURL.split('github.com').pop().split('/'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { html_url, files, history } = await ( + await fetch(`https://api.github.com/gists/${gistID}${revisionID ? '/' + revisionID : ''}`) + ).json(); if (isValidGist(files)) { const code = await getFileContent(files[codeFileName]); - if (!(configFileName in files)) { - return { code }; + let config: string; + if (configFileName in files) { + config = await getFileContent(files[configFileName]); } - const config = await getFileContent(files[configFileName]); - return { config, code }; + const currentItem = history[0]; + return { + url: `${html_url}/${currentItem.version}`, + code, + config, + author: currentItem.user.login, + time: new Date(currentItem.committed_at).getTime(), + version: (currentItem.version as string).slice(-7) + }; } else { throw 'Invalid gist provided'; } }; + +const getStateFromGist = (gist: GistData): State => { + const state = { + ...defaultState, + code: gist.code + }; + gist.config && (state.mermaid = gist.config); + return state; +}; + +export const loadGistData = async (gistURL: string): Promise => { + try { + const [_, __, gistID, revisionID] = gistURL.split('github.com').pop().split('/'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { history } = await ( + await fetch(`https://api.github.com/gists/${gistID}${revisionID ? '/' + revisionID : ''}`) + ).json(); + const gistHistory: GistData[] = []; + for (const entry of history) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const data: GistData = await getGistData(entry.url).catch(() => undefined); + data && gistHistory.push(data); + } + if (gistHistory.length === 0) { + throw 'Invalid gist provided'; + } + gistHistory.reverse(); + const state = getStateFromGist(gistHistory.slice(-1).pop()); + for (const gist of gistHistory) { + addHistoryEntry({ + state: getStateFromGist(gist), + time: gist.time, + type: 'loader', + url: gist.url, + name: `${gist.author}-${gist.version}` + }); + } + return state; + } catch (err) { + console.error(err); + } +}; diff --git a/src/lib/util/fileLoaders/loader.ts b/src/lib/util/fileLoaders/loader.ts index f4d0b08b..5a04b2a7 100644 --- a/src/lib/util/fileLoaders/loader.ts +++ b/src/lib/util/fileLoaders/loader.ts @@ -1,27 +1,48 @@ -import { getGistData } from './gist'; -import { updateCode, updateConfig } from '../state'; -import type { Loader } from '$lib/types'; +import { loadGistData } from './gist'; +import { updateCodeStore, defaultState } from '../state'; +import type { Loader, State } from '$lib/types'; const loaders: Record = { - gist: getGistData + gist: loadGistData }; export const loadDataFromUrl = async (): Promise => { const searchParams = new URLSearchParams(window.location.search); + let state: State; let code: string, config: string; - if (searchParams.has('code')) { - code = await (await fetch(searchParams.get('code'))).text(); + const codeURL: string = searchParams.get('code'); + const configURL: string = searchParams.get('config'); + + if (codeURL) { + code = await (await fetch(codeURL)).text(); } - if (searchParams.has('config')) { - config = await (await fetch(searchParams.get('config'))).text(); + if (configURL) { + config = await (await fetch(configURL)).text(); + } else { + config = defaultState.mermaid; } if (!code) { for (const [key, value] of searchParams.entries()) { if (key in loaders) { - ({ code, config } = await loaders[key](value)); + state = await loaders[key](value); break; } } + } else { + state = { + code, + mermaid: config, + autoSync: true, + updateDiagram: true, + updateEditor: true, + loader: { + type: 'files', + config: { + codeURL, + configURL + } + } + }; } - code && updateCode(code, true, true); - config && updateConfig(config, true); + updateCodeStore(state); + // window.location.search = ''; }; diff --git a/src/lib/util/state.ts b/src/lib/util/state.ts index ebb839d5..d3c32807 100644 --- a/src/lib/util/state.ts +++ b/src/lib/util/state.ts @@ -4,7 +4,7 @@ import { persist, localStorage } from '@macfja/svelte-persistent-store'; import type { State } from '$lib/types'; import { saveStatistcs } from './stats'; -const defaultState: State = { +export const defaultState: State = { code: `graph TD A[Christmas] -->|Get money| B(Go shopping) B --> C{Let me think}