Show git revision history

This commit is contained in:
Sidharth Vinod
2021-06-05 11:52:13 +05:30
parent f5878feaa5
commit fdff260f7f
9 changed files with 223 additions and 65 deletions
+14
View File
@@ -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
View File
@@ -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"
}
+1 -1
View File
@@ -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) => {
+46 -19
View File
@@ -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>
+27 -11
View File
@@ -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 => {
+17 -3
View File
@@ -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>;
+72 -9
View File
@@ -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);
}
};
+32 -11
View File
@@ -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 = '';
};
+1 -1
View File
@@ -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}