Show git revision history
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+13
-10
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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<TabEvents>();
|
||||
const toggleTabs = (tab: Tab) => {
|
||||
|
||||
@@ -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<Tab>) => {
|
||||
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;
|
||||
</script>
|
||||
|
||||
@@ -78,20 +93,30 @@
|
||||
class="btn"
|
||||
on:click|stopPropagation={() => saveHistory()}
|
||||
title="Save current state"><i class="far fa-save" /></button>
|
||||
<button
|
||||
id="clearHistory"
|
||||
class="btn text-red-400"
|
||||
on:click|stopPropagation={() => clearHistory()}
|
||||
title="Delete all saved states"><i class="fas fa-trash-alt" /></button>
|
||||
{#if $historyModeStore !== 'loader'}
|
||||
<button
|
||||
id="clearHistory"
|
||||
class="btn text-red-400"
|
||||
on:click|stopPropagation={() => clearHistory()}
|
||||
title="Delete all saved states"><i class="fas fa-trash-alt" /></button>
|
||||
{/if}
|
||||
</div>
|
||||
<ul class="p-2 space-y-2 overflow-auto h-56" id="historyList">
|
||||
{#if $historyStore.length > 0}
|
||||
{#each $historyStore as { state, time, name }}
|
||||
{#each $historyStore as { state, time, name, url, type }}
|
||||
<li class="rounded p-2 shadow flex-col">
|
||||
<div class="flex">
|
||||
<div class="flex-1">
|
||||
<div class="flex flex-col">
|
||||
<span>{name}</span>
|
||||
{#if url}
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
title="Open revision in new tab"
|
||||
class="hover:underline text-blue-500">{name}</a>
|
||||
{:else}
|
||||
<span>{name}</span>
|
||||
{/if}
|
||||
<span class="text-gray-400 text-sm">{relativeTime(time)}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,9 +124,11 @@
|
||||
<button
|
||||
class="rounded px-2 w-24 bg-green-200 hover:bg-green-300"
|
||||
on:click={() => restoreHistory(state)}><i class="fas fa-undo" /> Restore</button>
|
||||
<button
|
||||
class="rounded px-2 w-24 bg-red-200 hover:bg-red-300"
|
||||
on:click={() => clearHistory(time)}><i class="fas fa-trash-alt" /> Delete</button>
|
||||
{#if type !== 'loader'}
|
||||
<button
|
||||
class="rounded px-2 w-24 bg-red-200 hover:bg-red-300"
|
||||
on:click={() => clearHistory(time)}><i class="fas fa-trash-alt" /> Delete</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -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<boolean> = persist(
|
||||
writable(true),
|
||||
export const historyModeStore: Writable<HistoryType> = persist(
|
||||
writable('manual'),
|
||||
localStorage(),
|
||||
'autoHistoryMode'
|
||||
);
|
||||
@@ -23,17 +23,30 @@ const manualHistoryStore: Writable<HistoryEntry[]> = persist(
|
||||
'manualHistoryStore'
|
||||
);
|
||||
|
||||
export const loaderHistoryStore: Writable<HistoryEntry[]> = writable([] as HistoryEntry[]);
|
||||
|
||||
export const historyStore: Readable<HistoryEntry[]> = 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 => {
|
||||
|
||||
Vendored
+17
-3
@@ -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<MermaidData>;
|
||||
type Loader = (url: string) => Promise<State>;
|
||||
|
||||
@@ -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<string> => {
|
||||
return file.content;
|
||||
};
|
||||
|
||||
export const getGistData = async (gistURL: string): Promise<MermaidData> => {
|
||||
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<GistData> => {
|
||||
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<State> => {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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<string, Loader> = {
|
||||
gist: getGistData
|
||||
gist: loadGistData
|
||||
};
|
||||
|
||||
export const loadDataFromUrl = async (): Promise<void> => {
|
||||
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 = '';
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user