diff --git a/packages/api/src/controllers/config.js b/packages/api/src/controllers/config.js
index 884cdf6b8..7baa33201 100644
--- a/packages/api/src/controllers/config.js
+++ b/packages/api/src/controllers/config.js
@@ -3,16 +3,16 @@ const currentVersion = require('../currentVersion');
module.exports = {
get_meta: 'get',
async get() {
- const toolbarButtons = process.env.TOOLBAR;
- const toolbar = toolbarButtons
- ? toolbarButtons.split(',').map((name) => ({
- name,
- icon: process.env[`ICON_${name}`],
- title: process.env[`TITLE_${name}`],
- page: process.env[`PAGE_${name}`],
- }))
- : null;
- const startupPages = process.env.STARTUP_PAGES ? process.env.STARTUP_PAGES.split(',') : [];
+ // const toolbarButtons = process.env.TOOLBAR;
+ // const toolbar = toolbarButtons
+ // ? toolbarButtons.split(',').map((name) => ({
+ // name,
+ // icon: process.env[`ICON_${name}`],
+ // title: process.env[`TITLE_${name}`],
+ // page: process.env[`PAGE_${name}`],
+ // }))
+ // : null;
+ // const startupPages = process.env.STARTUP_PAGES ? process.env.STARTUP_PAGES.split(',') : [];
const permissions = process.env.PERMISSIONS ? process.env.PERMISSIONS.split(',') : null;
const singleDatabase =
process.env.SINGLE_CONNECTION && process.env.SINGLE_DATABASE
@@ -24,8 +24,8 @@ module.exports = {
return {
runAsPortal: !!process.env.CONNECTIONS,
- toolbar,
- startupPages,
+ // toolbar,
+ // startupPages,
singleDatabase,
permissions,
...currentVersion,
diff --git a/packages/api/src/controllers/files.js b/packages/api/src/controllers/files.js
index 2e2752e1f..3d42962ae 100644
--- a/packages/api/src/controllers/files.js
+++ b/packages/api/src/controllers/files.js
@@ -5,6 +5,10 @@ const hasPermission = require('../utility/hasPermission');
const socket = require('../utility/socket');
const scheduler = require('./scheduler');
+const markdownAutorunRegex = /<\!--.*@autorun\s*(\n.*-->|-->)/s;
+const markdownButtonRegex = /<\!--.*@button\s+([^\n]+)(\n.*-->|-->)/s;
+const markdownIconRegex = /<\!--.*@icon\s+([^\n]+)(\n.*-->|-->)/s;
+
function serialize(format, data) {
if (format == 'text') return data;
if (format == 'json') return JSON.stringify(data);
@@ -61,4 +65,29 @@ module.exports = {
scheduler.reload();
}
},
+
+ markdownManifest_meta: 'get',
+ async markdownManifest() {
+ if (!hasPermission(`files/markdown/write`)) return {};
+ const dir = path.join(filesdir(), 'markdown');
+ if (!(await fs.exists(dir))) return {};
+ const files = await fs.readdir(dir);
+ const res = [];
+ for (const file of files) {
+ const filePath = path.join(dir, file);
+ const text = await fs.readFile(filePath, { encoding: 'utf-8' });
+ const autorun = text.match(markdownAutorunRegex);
+ const button = text.match(markdownButtonRegex);
+ const icon = text.match(markdownIconRegex);
+ if (autorun || button) {
+ res.push({
+ file,
+ autorun: !!autorun,
+ button: button ? button[1].trim() : undefined,
+ icon: icon ? icon[1].trim() : undefined,
+ });
+ }
+ }
+ return res;
+ },
};
diff --git a/packages/api/src/main.js b/packages/api/src/main.js
index 16cacd4f0..890293004 100644
--- a/packages/api/src/main.js
+++ b/packages/api/src/main.js
@@ -72,9 +72,9 @@ function start(argument = null) {
useController(app, '/files', files);
useController(app, '/scheduler', scheduler);
- if (process.env.PAGES_DIRECTORY) {
- app.use('/pages', express.static(process.env.PAGES_DIRECTORY));
- }
+ // if (process.env.PAGES_DIRECTORY) {
+ // app.use('/pages', express.static(process.env.PAGES_DIRECTORY));
+ // }
app.use('/runners/data', express.static(rundir()));
diff --git a/packages/web/src/icons.js b/packages/web/src/icons.js
index eeadb9242..0622d3c44 100644
--- a/packages/web/src/icons.js
+++ b/packages/web/src/icons.js
@@ -26,6 +26,7 @@ const iconNames = {
'icon account': 'mdi mdi-account',
'icon sql-file': 'mdi mdi-file',
'icon web': 'mdi mdi-web',
+ 'icon home': 'mdi mdi-home',
'icon edit': 'mdi mdi-pencil',
'icon delete': 'mdi mdi-delete',
@@ -40,6 +41,7 @@ const iconNames = {
'icon error': 'mdi mdi-close-circle',
'icon ok': 'mdi mdi-check-circle',
'icon markdown': 'mdi mdi-application',
+ 'icon preview': 'mdi mdi-file-find',
'icon run': 'mdi mdi-play',
'icon chevron-down': 'mdi mdi-chevron-down',
@@ -62,6 +64,7 @@ const iconNames = {
'img shell': 'mdi mdi-flash color-blue-7',
'img chart': 'mdi mdi-chart-bar color-magenta-7',
'img markdown': 'mdi mdi-application color-red-7',
+ 'img preview': 'mdi mdi-file-find color-red-7',
'img free-table': 'mdi mdi-table color-green-7',
'img macro': 'mdi mdi-hammer-wrench',
diff --git a/packages/web/src/markdown/MarkdownExtendedView.js b/packages/web/src/markdown/MarkdownExtendedView.js
index 56af4b579..08c397112 100644
--- a/packages/web/src/markdown/MarkdownExtendedView.js
+++ b/packages/web/src/markdown/MarkdownExtendedView.js
@@ -2,6 +2,7 @@ import React from 'react';
import Markdown from 'markdown-to-jsx';
import styled from 'styled-components';
import OpenChartLink from './OpenChartLink';
+import MarkdownLink from './MarkdownLink';
const Wrapper = styled.div`
padding: 10px;
@@ -18,6 +19,7 @@ export default function MarkdownExtendedView({ children }) {
OpenChartLink: {
component: OpenChartLink,
},
+ a: MarkdownLink,
},
}}
>
diff --git a/packages/web/src/markdown/MarkdownLink.js b/packages/web/src/markdown/MarkdownLink.js
new file mode 100644
index 000000000..d201cf64e
--- /dev/null
+++ b/packages/web/src/markdown/MarkdownLink.js
@@ -0,0 +1,13 @@
+import React from 'react';
+import useTheme from '../theme/useTheme';
+import { StyledThemedLink } from '../widgets/FormStyledButton';
+
+export default function MarkdownLink({ href, title, children }) {
+ const theme = useTheme();
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/packages/web/src/markdown/MarkdownToolbar.js b/packages/web/src/markdown/MarkdownToolbar.js
index c59c2b9be..108d325ce 100644
--- a/packages/web/src/markdown/MarkdownToolbar.js
+++ b/packages/web/src/markdown/MarkdownToolbar.js
@@ -2,7 +2,7 @@ import React from 'react';
import useHasPermission from '../utility/useHasPermission';
import ToolbarButton from '../widgets/ToolbarButton';
-export default function MarkdownToolbar({ save }) {
+export default function MarkdownToolbar({ save, showPreview }) {
const hasPermission = useHasPermission();
return (
@@ -12,6 +12,9 @@ export default function MarkdownToolbar({ save }) {
Save
)}
+
+ Preview
+
>
);
}
diff --git a/packages/web/src/markdown/OpenChartLink.js b/packages/web/src/markdown/OpenChartLink.js
index 66119b4a2..1158c53f6 100644
--- a/packages/web/src/markdown/OpenChartLink.js
+++ b/packages/web/src/markdown/OpenChartLink.js
@@ -1,18 +1,9 @@
import React from 'react';
import { useCurrentDatabase, useSetOpenedTabs } from '../utility/globalState';
-import styled from 'styled-components';
import { openNewTab } from '../utility/common';
import axios from '../utility/axios';
import useTheme from '../theme/useTheme';
-
-const StyledLink = styled.a`
- text-decoration: none;
- cursor: pointer;
- color: ${(props) => props.theme.main_background_blue[7]};
- &:hover {
- text-decoration: underline;
- }
-`;
+import { StyledThemedLink } from '../widgets/FormStyledButton';
export default function OpenChartLink({ file, children }) {
const setOpenedTabs = useSetOpenedTabs();
@@ -37,8 +28,8 @@ export default function OpenChartLink({ file, children }) {
};
return (
-
+
{children}
-
+
);
}
diff --git a/packages/web/src/modals/AboutModal.js b/packages/web/src/modals/AboutModal.js
index 8b4272e66..10f239b4f 100644
--- a/packages/web/src/modals/AboutModal.js
+++ b/packages/web/src/modals/AboutModal.js
@@ -9,6 +9,7 @@ import moment from 'moment';
import styled from 'styled-components';
import getElectron from '../utility/getElectron';
import useTheme from '../theme/useTheme';
+import { StyledThemedLink } from '../widgets/FormStyledButton';
const Container = styled.div`
display: flex;
@@ -24,15 +25,6 @@ const StyledValue = styled.span`
font-weight: bold;
`;
-const StyledLink = styled.a`
- text-decoration: none;
- cursor: pointer;
- color: ${(props) => props.theme.main_background_blue[7]};
- &:hover {
- text-decoration: underline;
- }
-`;
-
function Line({ label, children }) {
return (
@@ -48,13 +40,13 @@ function Link({ label, children, href }) {
{label}:{' '}
{electron ? (
- electron.shell.openExternal(href)}>
+ electron.shell.openExternal(href)}>
{children}
-
+
) : (
-
+
{children}
-
+
)}
);
diff --git a/packages/web/src/tabs/MarkdownEditorTab.js b/packages/web/src/tabs/MarkdownEditorTab.js
index 23c09a48b..ee49ae539 100644
--- a/packages/web/src/tabs/MarkdownEditorTab.js
+++ b/packages/web/src/tabs/MarkdownEditorTab.js
@@ -8,15 +8,42 @@ import useEditorData from '../utility/useEditorData';
import SaveTabModal from '../modals/SaveTabModal';
import useModalState from '../modals/useModalState';
import LoadingInfo from '../widgets/LoadingInfo';
+import { useOpenedTabs, useSetOpenedTabs } from '../utility/globalState';
+import { openNewTab } from '../utility/common';
export default function MarkdownEditorTab({ tabid, tabVisible, toolbarPortalRef, ...other }) {
- const { editorData, setEditorData, isLoading } = useEditorData({ tabid });
+ const { editorData, setEditorData, isLoading, saveToStorage } = useEditorData({ tabid });
const saveFileModalState = useModalState();
+ const openedTabs = useOpenedTabs();
+ const setOpenedTabs = useSetOpenedTabs();
const handleKeyDown = (data, hash, keyString, keyCode, event) => {
if (keyCode == keycodes.f5) {
event.preventDefault();
- // handlePreview();
+ showPreview();
+ }
+ };
+
+ const showPreview = async () => {
+ await saveToStorage();
+ const existing = (openedTabs || []).find((x) => x.props && x.props.sourceTabId == tabid && x.closedTime == null);
+ if (existing) {
+ setOpenedTabs((tabs) =>
+ tabs.map((x) => ({
+ ...x,
+ selected: x.tabid == existing.tabid,
+ }))
+ );
+ } else {
+ const thisTab = (openedTabs || []).find((x) => x.tabid == tabid);
+ openNewTab(setOpenedTabs, {
+ title: thisTab.title,
+ icon: 'img preview',
+ tabComponent: 'MarkdownPreviewTab',
+ props: {
+ sourceTabId: tabid,
+ },
+ });
}
};
@@ -40,7 +67,10 @@ export default function MarkdownEditorTab({ tabid, tabVisible, toolbarPortalRef,
{toolbarPortalRef &&
toolbarPortalRef.current &&
tabVisible &&
- ReactDOM.createPortal(, toolbarPortalRef.current)}
+ ReactDOM.createPortal(
+ ,
+ toolbarPortalRef.current
+ )}
{
+ if (tabVisible) setReloadToken((x) => x + 1);
+ }, [tabVisible]);
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ return {editorData || ''};
+}
diff --git a/packages/web/src/tabs/index.js b/packages/web/src/tabs/index.js
index 4d3632510..0f330b2f2 100644
--- a/packages/web/src/tabs/index.js
+++ b/packages/web/src/tabs/index.js
@@ -10,6 +10,7 @@ import PluginTab from './PluginTab';
import ChartTab from './ChartTab';
import MarkdownEditorTab from './MarkdownEditorTab';
import MarkdownViewTab from './MarkdownViewTab';
+import MarkdownPreviewTab from './MarkdownPreviewTab';
export default {
TableDataTab,
@@ -24,4 +25,5 @@ export default {
ChartTab,
MarkdownEditorTab,
MarkdownViewTab,
+ MarkdownPreviewTab,
};
diff --git a/packages/web/src/utility/metadataLoaders.js b/packages/web/src/utility/metadataLoaders.js
index e3f46cc73..d39921338 100644
--- a/packages/web/src/utility/metadataLoaders.js
+++ b/packages/web/src/utility/metadataLoaders.js
@@ -46,6 +46,12 @@ const configLoader = () => ({
reloadTrigger: 'config-changed',
});
+const markdownManifestLoader = () => ({
+ url: 'files/markdown-manifest',
+ params: {},
+ reloadTrigger: 'files-changed-markdown',
+});
+
// const sqlObjectListLoader = ({ conid, database }) => ({
// url: 'metadata/list-objects',
// params: { conid, database },
@@ -269,3 +275,10 @@ export function getFiles(args) {
export function useFiles(args) {
return useCore(filesLoader, args);
}
+
+export function getMarkdownManifest(args) {
+ return getCore(markdownManifestLoader, args);
+}
+export function useMarkdownManifest(args) {
+ return useCore(markdownManifestLoader, args);
+}
diff --git a/packages/web/src/utility/useEditorData.js b/packages/web/src/utility/useEditorData.js
index 2e200d863..fe4f0dc58 100644
--- a/packages/web/src/utility/useEditorData.js
+++ b/packages/web/src/utility/useEditorData.js
@@ -4,7 +4,7 @@ import localforage from 'localforage';
import { changeTab } from './common';
import { useSetOpenedTabs } from './globalState';
-export default function useEditorData({ tabid, loadFromArgs = null }) {
+export default function useEditorData({ tabid, reloadToken = 0, loadFromArgs = null }) {
const localStorageKey = `tabdata_${tabid}`;
const setOpenedTabs = useSetOpenedTabs();
const changeCounterRef = React.useRef(0);
@@ -57,19 +57,20 @@ export default function useEditorData({ tabid, loadFromArgs = null }) {
React.useEffect(() => {
initialLoad();
- }, []);
+ }, [reloadToken]);
const saveToStorage = React.useCallback(async () => {
if (valueRef.current == null) return;
try {
await localforage.setItem(localStorageKey, valueRef.current);
+ localStorage.removeItem(localStorageKey);
savedCounterRef.current = changeCounterRef.current;
} catch (err) {
console.error(err);
}
}, [localStorageKey, valueRef]);
- const saveToStorageFallback = React.useCallback(() => {
+ const saveToStorageSync = React.useCallback(() => {
if (valueRef.current == null) return;
if (savedCounterRef.current == changeCounterRef.current) return; // all saved
// on window unload must be synchronous actions, save to local storage instead
@@ -86,10 +87,10 @@ export default function useEditorData({ tabid, loadFromArgs = null }) {
};
React.useEffect(() => {
- window.addEventListener('beforeunload', saveToStorageFallback);
+ window.addEventListener('beforeunload', saveToStorageSync);
return () => {
saveToStorage();
- window.removeEventListener('beforeunload', saveToStorageFallback);
+ window.removeEventListener('beforeunload', saveToStorageSync);
};
}, []);
@@ -99,5 +100,7 @@ export default function useEditorData({ tabid, loadFromArgs = null }) {
isLoading,
initialData: initialDataRef.current,
errorMessage,
+ saveToStorage,
+ saveToStorageSync,
};
}
diff --git a/packages/web/src/widgets/FormStyledButton.js b/packages/web/src/widgets/FormStyledButton.js
index 2b4f7ce5a..987d46bdd 100644
--- a/packages/web/src/widgets/FormStyledButton.js
+++ b/packages/web/src/widgets/FormStyledButton.js
@@ -39,8 +39,7 @@ const makeStyle = (base) => styled(base)`
const ButtonInput = makeStyle(styled.input``);
const FormStyledLabelBase = makeStyle(styled.label``);
-export const FormStyledLabel = styled(FormStyledLabelBase)`
-`;
+export const FormStyledLabel = styled(FormStyledLabelBase)``;
export default function FormStyledButton({
onClick = undefined,
@@ -67,3 +66,12 @@ export default function FormStyledButton({
/>
);
}
+
+export const StyledThemedLink = styled.a`
+ text-decoration: none;
+ cursor: pointer;
+ color: ${(props) => props.theme.main_background_blue[7]};
+ &:hover {
+ text-decoration: underline;
+ }
+`;
diff --git a/packages/web/src/widgets/Toolbar.js b/packages/web/src/widgets/Toolbar.js
index 312abb736..c9aae96d1 100644
--- a/packages/web/src/widgets/Toolbar.js
+++ b/packages/web/src/widgets/Toolbar.js
@@ -4,7 +4,7 @@ import ConnectionModal from '../modals/ConnectionModal';
import styled from 'styled-components';
import ToolbarButton, { ToolbarButtonExternalImage } from './ToolbarButton';
import useNewQuery from '../query/useNewQuery';
-import { useConfig } from '../utility/metadataLoaders';
+import { useConfig, useMarkdownManifest } from '../utility/metadataLoaders';
import { useSetOpenedTabs, useOpenedTabs, useCurrentTheme, useSetCurrentTheme } from '../utility/globalState';
import { openNewTab } from '../utility/common';
import useNewFreeTable from '../freetable/useNewFreeTable';
@@ -25,7 +25,7 @@ export default function ToolBar({ toolbarPortalRef }) {
const newQuery = useNewQuery();
const newFreeTable = useNewFreeTable();
const config = useConfig();
- const toolbar = config.toolbar || [];
+ // const toolbar = config.toolbar || [];
const setOpenedTabs = useSetOpenedTabs();
const openedTabs = useOpenedTabs();
const showModal = useShowModal();
@@ -33,6 +33,7 @@ export default function ToolBar({ toolbarPortalRef }) {
const setCurrentTheme = useSetCurrentTheme();
const extensions = useExtensions();
const electron = getElectron();
+ const markdownManifest = useMarkdownManifest();
React.useEffect(() => {
window['dbgate_createNewConnection'] = modalState.open;
@@ -74,47 +75,47 @@ export default function ToolBar({ toolbarPortalRef }) {
});
};
- function openTabFromButton(button) {
- if (openedTabs.find((x) => x.tabComponent == 'InfoPageTab' && x.props && x.props.page == button.page)) {
+ function openTabFromButton(page) {
+ if (
+ openedTabs.find(
+ (x) => x.tabComponent == 'MarkdownViewTab' && x.props && x.props.file == page.file && x.closedTime == null
+ )
+ ) {
setOpenedTabs((tabs) =>
tabs.map((tab) => ({
...tab,
- selected: tab.tabComponent == 'InfoPageTab' && tab.props && tab.props.page == button.page,
- closedTime: undefined,
+ selected: tab.tabComponent == 'MarkdownViewTab' && tab.props && tab.props.file == page.file,
}))
);
} else {
openNewTab(setOpenedTabs, {
- title: button.title,
- tabComponent: 'InfoPageTab',
- icon: button.icon,
+ title: page.button || page.file,
+ tabComponent: 'MarkdownViewTab',
+ icon: page.icon || 'img markdown',
props: {
- page: button.page,
+ file: page.file,
},
});
}
}
React.useEffect(() => {
- if (config.startupPages) {
- for (const page of config.startupPages) {
- const button = toolbar.find((x) => x.name == page);
- if (button) {
- openTabFromButton(button);
- }
- }
+ for (const page of (markdownManifest || []).filter((x) => x.autorun)) {
+ openTabFromButton(page);
}
- }, config && config.startupPages);
+ }, [markdownManifest]);
return (
{!electron && }
- {toolbar.map((button) => (
- openTabFromButton(button)} icon={button.icon}>
- {button.title}
-
- ))}
+ {(markdownManifest || [])
+ .filter((x) => x.button)
+ .map((x) => (
+ openTabFromButton(x)} icon={x.icon || 'icon markdown'}>
+ {x.button}
+
+ ))}
{config.runAsPortal == false && (
Add connection