Merge pull request #1374 from mermaid-js/release-promotion
Release live editor
This commit is contained in:
+13
-11
@@ -1,6 +1,6 @@
|
||||
import { defineConfig } from 'cypress';
|
||||
import fs from 'fs';
|
||||
import { isFileExist, findFiles } from 'cy-verify-downloads';
|
||||
import path from 'path';
|
||||
export default defineConfig({
|
||||
projectId: '2ckppp',
|
||||
viewportWidth: 1440,
|
||||
@@ -15,17 +15,19 @@ export default defineConfig({
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
on('task', {
|
||||
isFileExist,
|
||||
findFiles,
|
||||
deleteFile(path) {
|
||||
fs.rmSync(path);
|
||||
return null;
|
||||
},
|
||||
readFileMaybe(filename) {
|
||||
if (fs.existsSync(filename)) {
|
||||
return fs.readFileSync(filename, 'utf8');
|
||||
readAndDeleteFile({ fileNamePattern, folder, mode }) {
|
||||
const fileNameRegex = new RegExp(fileNamePattern);
|
||||
const files = fs.readdirSync(folder);
|
||||
const filename = files.find((file) => file.match(fileNameRegex));
|
||||
const filePath = path.join(folder, filename);
|
||||
try {
|
||||
if (mode === 'size') {
|
||||
return fs.statSync(filePath).size;
|
||||
}
|
||||
return fs.readFileSync(filePath, 'utf8');
|
||||
} finally {
|
||||
fs.rmSync(filePath);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -26,8 +26,6 @@ describe('Check actions', () => {
|
||||
});
|
||||
|
||||
it('should download png and svg', () => {
|
||||
cy.clock(new Date(2022, 0, 1).getTime());
|
||||
|
||||
cy.get(`#downloadPNG`).click();
|
||||
verifyFileSizeGreaterThan('diagram', 'png', 34_000);
|
||||
|
||||
@@ -43,7 +41,5 @@ describe('Check actions', () => {
|
||||
|
||||
cy.get(`#downloadSVG`).click();
|
||||
verifyFileSizeGreaterThan('diagram', 'svg', 11_000);
|
||||
|
||||
cy.clock().invoke('restore');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,6 +50,24 @@ describe('Auto sync tests', () => {
|
||||
cy.getLocalStorage('codeStore').snapshot();
|
||||
});
|
||||
|
||||
it('should automatically defer rendering when complex diagrams are edited', () => {
|
||||
cy.get('#view').should('not.have.class', 'outOfSync');
|
||||
typeInEditor(`
|
||||
A & B & C & D & E --> F & G & K & Z & i
|
||||
A & B & C & D & E --> F & G & K & Z & i
|
||||
A & B & C & D & E --> F & G & K & Z & i
|
||||
A & B & C & D & E --> F & G & K & Z & i
|
||||
A & B & C & D & E --> F & G & K & Z & i
|
||||
A & B & C & D & E --> F & G & K & Z & i
|
||||
A & B & C & D & E --> F & G & K & Z & i
|
||||
A & B & C & D & E --> F & G & K & Z & i
|
||||
A & B & C & D & E --> F & G & K & Z & i`);
|
||||
cy.get('#view').should('have.class', 'outOfSync');
|
||||
cy.get('#errorContainer').should('contain.text', 'It will be updated automatically.');
|
||||
// The class should be removed automatically after 1 second.
|
||||
cy.get('#view').should('not.have.class', 'outOfSync');
|
||||
});
|
||||
|
||||
it('supports commenting code out/in', () => {
|
||||
cy.get('#editor').contains('Car').click();
|
||||
cy.get('#editor').get('textarea').type(`${cmd}/`, { force: true });
|
||||
|
||||
+15
-20
@@ -29,17 +29,15 @@ export const verifyFileSizeGreaterThan = (
|
||||
extension: string,
|
||||
size: number
|
||||
) => {
|
||||
const fileName = `mermaid-${fileType}-2022-01-01-000000.${extension}`;
|
||||
const filePath = `${downloadsFolder}/${fileName}`;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
cy.verifyDownload(fileName);
|
||||
cy.readFile(filePath, null, {
|
||||
log: false
|
||||
}).then((buffer: ArrayBuffer) => {
|
||||
expect(buffer.byteLength).to.be.gt(size);
|
||||
expect(buffer.byteLength).to.be.lt(size * 1.3);
|
||||
cy.get('#view').should('not.have.class', 'outOfSync');
|
||||
cy.task('readAndDeleteFile', {
|
||||
folder: downloadsFolder,
|
||||
fileNamePattern: `^mermaid-${fileType}-.*.${extension}$`,
|
||||
mode: 'size'
|
||||
}).then((fileSize: number) => {
|
||||
expect(fileSize).to.be.gt(size);
|
||||
expect(fileSize).to.be.lt(size * 1.3);
|
||||
});
|
||||
cy.task('deleteFile', filePath);
|
||||
};
|
||||
|
||||
export const verifyFileSnapshot = (
|
||||
@@ -47,14 +45,11 @@ export const verifyFileSnapshot = (
|
||||
extension: string,
|
||||
content: string
|
||||
) => {
|
||||
const fileName = `mermaid-${fileType}-2022-01-01-000000.${extension}`;
|
||||
const filePath = `${downloadsFolder}/${fileName}`;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
cy.verifyDownload(fileName);
|
||||
cy.readFile(filePath, null, {
|
||||
log: false
|
||||
}).then((buffer: ArrayBuffer) =>
|
||||
expect(new TextDecoder('utf8').decode(buffer)).to.contain(content)
|
||||
);
|
||||
cy.task('deleteFile', filePath);
|
||||
cy.task('readAndDeleteFile', {
|
||||
folder: downloadsFolder,
|
||||
fileNamePattern: `^mermaid-${fileType}-.*.${extension}$`,
|
||||
mode: 'content'
|
||||
}).then((fileContent: number) => {
|
||||
expect(fileContent).to.contain(content);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands';
|
||||
require('cy-verify-downloads').addCustomCommand();
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"types": ["cypress", "cypress-localstorage-commands", "cy-verify-downloads", "node"]
|
||||
"types": ["cypress", "cypress-localstorage-commands", "node"]
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
||||
|
||||
+7
-7
@@ -24,19 +24,19 @@
|
||||
"devDependencies": {
|
||||
"@cypress/snapshot": "2.1.7",
|
||||
"@sveltejs/adapter-static": "3.0.1",
|
||||
"@sveltejs/kit": "2.3.0",
|
||||
"@sveltejs/kit": "2.4.3",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.1",
|
||||
"@testing-library/svelte": "4.0.5",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/pako": "2.0.3",
|
||||
"@types/uuid": "9.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "6.18.1",
|
||||
"@typescript-eslint/parser": "6.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "6.19.1",
|
||||
"@typescript-eslint/parser": "6.19.1",
|
||||
"@vitest/ui": "^1.1.3",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"c8": "7.14.0",
|
||||
"chai": "^4.3.7",
|
||||
"cssnano": "^6.0.0",
|
||||
"cy-verify-downloads": "0.2.2",
|
||||
"cypress": "12.17.4",
|
||||
"cypress-localstorage-commands": "2.2.5",
|
||||
"eslint": "8.56.0",
|
||||
@@ -50,7 +50,6 @@
|
||||
"eslint-plugin-unicorn": "^50.0.1",
|
||||
"eslint-plugin-vitest": "^0.3.20",
|
||||
"esserializer": "^1.3.11",
|
||||
"font-awesome": "^4.7.0",
|
||||
"husky": "^8.0.3",
|
||||
"jsdom": "^21.1.2",
|
||||
"lint-staged": "^15.2.0",
|
||||
@@ -72,8 +71,9 @@
|
||||
"dependencies": {
|
||||
"daisyui": "2.52.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"js-base64": "3.7.5",
|
||||
"mermaid": "10.6.1",
|
||||
"js-base64": "3.7.6",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mermaid": "10.7.0",
|
||||
"monaco-editor": "0.45.0",
|
||||
"pako": "2.1.0",
|
||||
"plausible-tracker": "^0.3.8",
|
||||
|
||||
+2
-2
@@ -17,8 +17,8 @@
|
||||
<link rel="manifest" href="%sveltekit.assets%/manifest.json" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"
|
||||
integrity="sha512-Avb2QiuDEEvB4bZJYdft2mNjVShBftLdPG8FJ0V7irTLQ8Uo0qcPxh4Plq7G5tGm0rU+1SPhVotteLpBERwTkw=="
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer" />
|
||||
%sveltekit.head%
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import Card from '$lib/components/Card/Card.svelte';
|
||||
import { waitForRender } from '$lib/util/autoSync';
|
||||
import { env } from '$lib/util/env';
|
||||
import { pakoSerde } from '$lib/util/serde';
|
||||
import { stateStore } from '$lib/util/state';
|
||||
import { logEvent } from '$lib/util/stats';
|
||||
import { toBase64 } from 'js-base64';
|
||||
import dayjs from 'dayjs';
|
||||
import { toBase64 } from 'js-base64';
|
||||
|
||||
const { krokiRendererUrl, rendererUrl } = env;
|
||||
type Exporter = (context: CanvasRenderingContext2D, image: HTMLImageElement) => () => void;
|
||||
@@ -30,7 +31,11 @@
|
||||
return toBase64(svgString);
|
||||
};
|
||||
|
||||
const exportImage = (event: Event, exporter: Exporter) => {
|
||||
const exportImage = async (event: Event, exporter: Exporter) => {
|
||||
await waitForRender();
|
||||
if (document.querySelector('.outOfSync')) {
|
||||
throw new Error('Diagram is out of sync');
|
||||
}
|
||||
const canvas: HTMLCanvasElement = document.createElement('canvas');
|
||||
const svg = document.querySelector<HTMLElement>('#container svg');
|
||||
if (!svg) {
|
||||
@@ -122,13 +127,13 @@
|
||||
};
|
||||
};
|
||||
|
||||
const onCopyClipboard = (event: Event) => {
|
||||
exportImage(event, clipboardCopy);
|
||||
const onCopyClipboard = async (event: Event) => {
|
||||
await exportImage(event, clipboardCopy);
|
||||
logEvent('copyClipboard');
|
||||
};
|
||||
|
||||
const onDownloadPNG = (event: Event) => {
|
||||
exportImage(event, downloadImage);
|
||||
const onDownloadPNG = async (event: Event) => {
|
||||
await exportImage(event, downloadImage);
|
||||
logEvent('download', {
|
||||
type: 'png'
|
||||
});
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
initEditor(monaco);
|
||||
errorDebug(100);
|
||||
errorDebug();
|
||||
editor = monaco.editor.create(divElement, editorOptions);
|
||||
editor.onDidChangeModelContent(({ isFlush }) => {
|
||||
const newText = editor?.getValue();
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
import { onMount } from 'svelte';
|
||||
import panzoom from 'svg-pan-zoom';
|
||||
import type { State, ValidatedState } from '$lib/types';
|
||||
import { logEvent } from '$lib/util/stats';
|
||||
import { logEvent, saveStatistics } from '$lib/util/stats';
|
||||
import { cmdKey } from '$lib/util/util';
|
||||
import { render as renderDiagram } from '$lib/util/mermaid';
|
||||
import type { MermaidConfig } from 'mermaid';
|
||||
import { recordRenderTime, shouldRefreshView } from '$lib/util/autoSync';
|
||||
|
||||
let code = '';
|
||||
let config = '';
|
||||
@@ -59,6 +60,7 @@
|
||||
};
|
||||
|
||||
const handleStateChange = async (state: ValidatedState) => {
|
||||
const startTime = Date.now();
|
||||
if (state.error !== undefined) {
|
||||
error = true;
|
||||
errorLines = state.error.toString().split('\n');
|
||||
@@ -76,6 +78,12 @@
|
||||
if (code === state.code && config === state.mermaid && panZoomEnabled === state.panZoom) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldRefreshView()) {
|
||||
outOfSync = true;
|
||||
return;
|
||||
}
|
||||
|
||||
code = state.code;
|
||||
config = state.mermaid;
|
||||
panZoomEnabled = state.panZoom;
|
||||
@@ -114,6 +122,11 @@
|
||||
console.error('view fail', error_);
|
||||
error = true;
|
||||
}
|
||||
const timeTaken = Date.now() - startTime;
|
||||
saveStatistics(code, timeTaken);
|
||||
recordRenderTime(timeTaken, () => {
|
||||
$inputStateStore.updateDiagram = true;
|
||||
});
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
@@ -140,7 +153,11 @@
|
||||
{/each}
|
||||
{:else}
|
||||
Diagram out of sync. <br />
|
||||
Press <i class="fas fa-sync" /> (Sync button) or <kbd>{cmdKey} + Enter</kbd> to sync.
|
||||
{#if $stateStore.autoSync}
|
||||
It will be updated automatically.
|
||||
{:else}
|
||||
Press <i class="fas fa-sync" /> (Sync button) or <kbd>{cmdKey} + Enter</kbd> to sync.
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import debounce from 'lodash-es/debounce';
|
||||
import { get } from 'svelte/store';
|
||||
import { stateStore } from './state';
|
||||
|
||||
let shouldSync = true;
|
||||
let updater: () => void;
|
||||
let renderPromise: Promise<void> | undefined;
|
||||
let resolveRenderPromise: (() => void) | undefined;
|
||||
const renderDelay = 1000;
|
||||
const slowRenderThreshold = 150;
|
||||
|
||||
const debouncedRender = debounce(() => {
|
||||
shouldSync = true;
|
||||
updater();
|
||||
}, renderDelay);
|
||||
|
||||
export const recordRenderTime = (renderTimeMs: number, updaterFunction: () => void): void => {
|
||||
resolveRenderPromise?.();
|
||||
const { autoSync } = get(stateStore);
|
||||
if (!autoSync) {
|
||||
return;
|
||||
}
|
||||
updater = updaterFunction;
|
||||
const isSlow = renderTimeMs > slowRenderThreshold;
|
||||
if (!shouldSync) {
|
||||
debouncedRender();
|
||||
}
|
||||
shouldSync = !isSlow;
|
||||
};
|
||||
|
||||
export const shouldRefreshView = (): boolean => {
|
||||
if (!renderPromise) {
|
||||
renderPromise = new Promise((resolve) => {
|
||||
resolveRenderPromise = () => {
|
||||
renderPromise = undefined;
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (!shouldSync) {
|
||||
debouncedRender();
|
||||
}
|
||||
return shouldSync;
|
||||
};
|
||||
|
||||
export const waitForRender = (): Promise<void> => {
|
||||
return renderPromise ?? Promise.resolve();
|
||||
};
|
||||
@@ -1,15 +1,25 @@
|
||||
<script lang="ts">
|
||||
const taglines = [
|
||||
'Try diagramming with ChatGPT at Mermaid Chart',
|
||||
"Try Mermaid's Visual Editor at Mermaid Chart",
|
||||
'Enjoy live collaboration with teammates at Mermaid Chart'
|
||||
];
|
||||
const taglines = {
|
||||
announcement_bar_ai_diagramming: 'Try diagramming with ChatGPT at Mermaid Chart',
|
||||
announcement_bar_visual_editor: "Try Mermaid's Visual Editor at Mermaid Chart",
|
||||
announcement_bar_live_collaboration: 'Enjoy live collaboration with teammates at Mermaid Chart'
|
||||
};
|
||||
const taglineKeys = Object.keys(taglines);
|
||||
const taglineKey = taglineKeys[Math.floor(Math.random() * taglineKeys.length)];
|
||||
const tagline = taglines[taglineKey];
|
||||
const url =
|
||||
'https://www.mermaidchart.com/?' +
|
||||
new URLSearchParams({
|
||||
utm_source: 'mermaid_live_editor',
|
||||
utm_medium: taglineKey,
|
||||
utm_campaign: 'promo_2024'
|
||||
}).toString();
|
||||
</script>
|
||||
|
||||
<a
|
||||
href="https://www.mermaidchart.com/"
|
||||
href={url}
|
||||
target="_blank"
|
||||
class="flex flex-grow justify-center gap-6 align-middle tracking-wide">
|
||||
{taglines[Math.floor(Math.random() * taglines.length)]}
|
||||
{tagline}
|
||||
<button class="rounded bg-gray-800 p-1 px-4 text-sm font-light">Try it now</button>
|
||||
</a>
|
||||
|
||||
@@ -14,7 +14,7 @@ const promotions: Promotion[] = [
|
||||
{
|
||||
id: 'holiday-2023',
|
||||
startDate: new Date('2023-11-27'),
|
||||
endDate: new Date('2024-01-9'),
|
||||
endDate: new Date('2024-01-09'),
|
||||
component: Holiday2023
|
||||
},
|
||||
{
|
||||
|
||||
+11
-27
@@ -1,12 +1,11 @@
|
||||
import { writable, get, type Readable, derived } from 'svelte/store';
|
||||
import { persist, localStorage } from './persist';
|
||||
import { saveStatistics, countLines } from './stats';
|
||||
import { serializeState, deserializeState } from './serde';
|
||||
import { cmdKey, errorDebug, formatJSON } from './util';
|
||||
import { parse } from './mermaid';
|
||||
|
||||
import type { ErrorHash, MarkerData, State, ValidatedState } from '$lib/types';
|
||||
import { debounce } from 'lodash-es';
|
||||
import type { MermaidConfig } from 'mermaid';
|
||||
import { derived, get, writable, type Readable } from 'svelte/store';
|
||||
import { parse } from './mermaid';
|
||||
import { localStorage, persist } from './persist';
|
||||
import { deserializeState, serializeState } from './serde';
|
||||
import { errorDebug, formatJSON } from './util';
|
||||
|
||||
export const defaultState: State = {
|
||||
code: `flowchart TD
|
||||
@@ -134,7 +133,6 @@ export const updateCodeStore = (newState: Partial<State>): void => {
|
||||
});
|
||||
};
|
||||
|
||||
let prompted = false;
|
||||
export const updateCode = (
|
||||
code: string,
|
||||
{
|
||||
@@ -142,21 +140,7 @@ export const updateCode = (
|
||||
resetPanZoom = false
|
||||
}: { updateDiagram?: boolean; resetPanZoom?: boolean } = {}
|
||||
): void => {
|
||||
// console.log('updateCode', code);
|
||||
const lines = countLines(code);
|
||||
saveStatistics(code);
|
||||
errorDebug();
|
||||
if (lines > 50 && !prompted && get(stateStore).autoSync) {
|
||||
const turnOff = confirm(
|
||||
`Long diagram detected. Turn off Auto Sync? Use ${cmdKey} + Enter or click the sync logo to manually sync.`
|
||||
);
|
||||
prompted = true;
|
||||
if (turnOff) {
|
||||
updateCodeStore({
|
||||
autoSync: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
inputStateStore.update((state) => {
|
||||
if (resetPanZoom) {
|
||||
@@ -185,13 +169,13 @@ export const toggleDarkTheme = (dark: boolean): void => {
|
||||
});
|
||||
};
|
||||
|
||||
let urlDebounce: number;
|
||||
export const initURLSubscription = (): void => {
|
||||
const updateHash = debounce((hash) => {
|
||||
history.replaceState(undefined, '', `#${hash}`);
|
||||
}, 250);
|
||||
|
||||
stateStore.subscribe(({ serialized }) => {
|
||||
clearTimeout(urlDebounce);
|
||||
urlDebounce = window.setTimeout(() => {
|
||||
history.replaceState(undefined, '', `#${serialized}`);
|
||||
}, 250);
|
||||
updateHash(serialized);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+34
-22
@@ -47,33 +47,45 @@ export const countLines = (code: string): number => {
|
||||
return (code.match(/\n/g)?.length ?? 0) + 1;
|
||||
};
|
||||
|
||||
export const saveStatistics = (graph: string): void => {
|
||||
export const saveStatistics = (graph: string, renderTime: number): void => {
|
||||
const graphType = detectType(graph);
|
||||
if (!graphType) {
|
||||
return;
|
||||
}
|
||||
const length = countLines(graph);
|
||||
const lengthBucket =
|
||||
length < 10
|
||||
? '0-10'
|
||||
: length < 25
|
||||
? '10-25'
|
||||
: length < 50
|
||||
? '25-50'
|
||||
: length < 100
|
||||
? '50-100'
|
||||
: length < 200
|
||||
? '100-200'
|
||||
: length < 500
|
||||
? '200-500'
|
||||
: length < 700
|
||||
? '500-700'
|
||||
: length < 1000
|
||||
? '700-1000'
|
||||
: length < 1500
|
||||
? '1000-1500'
|
||||
: '1500+';
|
||||
logEvent('render', { graphType, length, lengthBucket });
|
||||
const lengthBucket = getBucket(length);
|
||||
const renderTimeMsBucket = getBucket(renderTime);
|
||||
logEvent('render', { graphType, length, lengthBucket, renderTimeMsBucket });
|
||||
};
|
||||
|
||||
const getBucket = (length: number): string => {
|
||||
return length < 10
|
||||
? '0-10'
|
||||
: length < 25
|
||||
? '10-25'
|
||||
: length < 50
|
||||
? '25-50'
|
||||
: length < 100
|
||||
? '50-100'
|
||||
: length < 200
|
||||
? '100-200'
|
||||
: length < 500
|
||||
? '200-500'
|
||||
: length < 700
|
||||
? '500-700'
|
||||
: length < 1000
|
||||
? '700-1000'
|
||||
: length < 1500
|
||||
? '1000-1500'
|
||||
: length < 2500
|
||||
? '1500-2500'
|
||||
: length < 4500
|
||||
? '2500-4500'
|
||||
: length < 7000
|
||||
? '4500-7000'
|
||||
: length < 10_000
|
||||
? '7000-10000'
|
||||
: '10000+';
|
||||
};
|
||||
|
||||
const minutesToMilliSeconds = (minutes: number): number => {
|
||||
|
||||
@@ -28,7 +28,7 @@ export const isMac = navigator.platform.toUpperCase().includes('MAC');
|
||||
export const cmdKey = isMac ? 'Cmd' : 'Ctrl';
|
||||
|
||||
let count = 0;
|
||||
export const errorDebug = (limit = 100) => {
|
||||
export const errorDebug = (limit = 1000) => {
|
||||
count += 1;
|
||||
if (count > limit) {
|
||||
console.log(count, limit);
|
||||
|
||||
Reference in New Issue
Block a user