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