diff --git a/packages/api/src/controllers/files.js b/packages/api/src/controllers/files.js index e47b81f89..a05ee697c 100644 --- a/packages/api/src/controllers/files.js +++ b/packages/api/src/controllers/files.js @@ -66,6 +66,25 @@ module.exports = { } }, + favorites_meta: 'get', + async favorites() { + if (!hasPermission(`files/favorites/read`)) return []; + const dir = path.join(filesdir(), 'favorites'); + 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' }); + res.push({ + file, + folder: 'favorites', + ...JSON.parse(text), + }); + } + return res; + }, + markdownManifest_meta: 'get', async markdownManifest() { if (!hasPermission(`files/markdown/read`)) return []; diff --git a/packages/web/src/appobj/SavedFileAppObject.js b/packages/web/src/appobj/SavedFileAppObject.js index 23baaec71..80e04fbf7 100644 --- a/packages/web/src/appobj/SavedFileAppObject.js +++ b/packages/web/src/appobj/SavedFileAppObject.js @@ -44,7 +44,7 @@ function Menu({ data, menuExt = null }) { ); } -export function SavedFileAppObjectBase({ data, commonProps, format, icon, onLoad, menuExt = null }) { +export function SavedFileAppObjectBase({ data, commonProps, format, icon, onLoad, title = undefined, menuExt = null }) { const { file, folder } = data; const onClick = async () => { @@ -56,7 +56,7 @@ export function SavedFileAppObjectBase({ data, commonProps, format, icon, onLoad : Menu} @@ -90,7 +90,7 @@ export function SavedSqlFileAppObject({ data, commonProps }) { icon: 'img shell', tabComponent: 'ShellTab', }, - script.getScript() + { editor: script.getScript() } ); }; @@ -111,6 +111,8 @@ export function SavedSqlFileAppObject({ data, commonProps }) { initialData: data, // @ts-ignore savedFile: file, + savedFolder: 'sql', + savedFormat: 'text', }); }} /> @@ -135,9 +137,11 @@ export function SavedShellFileAppObject({ data, commonProps }) { tabComponent: 'ShellTab', props: { savedFile: file, + savedFolder: 'shell', + savedFormat: 'text', }, }, - data + { editor: data } ); }} /> @@ -171,10 +175,12 @@ export function SavedChartFileAppObject({ data, commonProps }) { conid: connection._id, database, savedFile: file, + savedFolder: 'charts', + savedFormat: 'json', }, tabComponent: 'ChartTab', }, - data + { editor: data } ); }} /> @@ -191,7 +197,9 @@ export function SavedMarkdownFileAppObject({ data, commonProps }) { icon: 'img markdown', tabComponent: 'MarkdownViewTab', props: { - file, + savedFile: file, + savedFolder: 'markdown', + savedFormat: 'text', }, }); }; @@ -209,9 +217,11 @@ export function SavedMarkdownFileAppObject({ data, commonProps }) { tabComponent: 'MarkdownEditorTab', props: { savedFile: file, + savedFolder: 'markdown', + savedFormat: 'text', }, }, - data + { editor: data } ); }} menuExt={Show page} @@ -219,7 +229,51 @@ export function SavedMarkdownFileAppObject({ data, commonProps }) { ); } -[SavedSqlFileAppObject, SavedShellFileAppObject, SavedChartFileAppObject, SavedMarkdownFileAppObject].forEach((fn) => { +export function FavoriteFileAppObject({ data, commonProps }) { + const { file, folder, icon, tabComponent, title, props, tabdata } = data; + const openNewTab = useOpenNewTab(); + + return ( + { + let tabdataNew = tabdata; + if (props.savedFile) { + const resp = await axios.post('files/load', { + folder: props.savedFolder, + file: props.savedFile, + format: props.savedFormat, + }); + tabdataNew = { + ...tabdata, + editor: resp.data, + }; + } + openNewTab( + { + title, + icon: icon || 'img favorite', + props, + tabComponent, + }, + tabdataNew + ); + }} + /> + ); +} + +[ + SavedSqlFileAppObject, + SavedShellFileAppObject, + SavedChartFileAppObject, + SavedMarkdownFileAppObject, + FavoriteFileAppObject, +].forEach((fn) => { // @ts-ignore fn.extractKey = (data) => data.file; }); diff --git a/packages/web/src/datagrid/DataGridCore.js b/packages/web/src/datagrid/DataGridCore.js index 18d9058d0..e41d90eb0 100644 --- a/packages/web/src/datagrid/DataGridCore.js +++ b/packages/web/src/datagrid/DataGridCore.js @@ -341,7 +341,7 @@ export default function DataGridCore(props) { tabComponent: 'FreeTableTab', props: {}, }, - getSelectedFreeData() + { editor: getSelectedFreeData() } ); }; @@ -354,8 +354,10 @@ export default function DataGridCore(props) { props: {}, }, { - data: getSelectedFreeData(), - config: { chartType: 'bar' }, + editor: { + data: getSelectedFreeData(), + config: { chartType: 'bar' }, + }, } ); }; diff --git a/packages/web/src/datagrid/SqlDataGridCore.js b/packages/web/src/datagrid/SqlDataGridCore.js index d1047435f..ed8a0ec82 100644 --- a/packages/web/src/datagrid/SqlDataGridCore.js +++ b/packages/web/src/datagrid/SqlDataGridCore.js @@ -92,10 +92,12 @@ export default function SqlDataGridCore(props) { }, }, { - config: { chartType: 'bar' }, - sql: display.getExportQuery((select) => { - select.orderBy = null; - }), + editor: { + config: { chartType: 'bar' }, + sql: display.getExportQuery((select) => { + select.orderBy = null; + }), + }, } ); } diff --git a/packages/web/src/icons.js b/packages/web/src/icons.js index 0622d3c44..8a7623f17 100644 --- a/packages/web/src/icons.js +++ b/packages/web/src/icons.js @@ -9,6 +9,7 @@ const iconNames = { 'icon export': 'mdi mdi-application-export', 'icon new-connection': 'mdi mdi-database-plus', 'icon tables': 'mdi mdi-table-multiple', + 'icon favorite': 'mdi mdi-star', 'icon database': 'mdi mdi-database', 'icon server': 'mdi mdi-server', @@ -65,6 +66,7 @@ const iconNames = { '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 favorite': 'mdi mdi-star color-yellow-7', 'img free-table': 'mdi mdi-table color-green-7', 'img macro': 'mdi mdi-hammer-wrench', diff --git a/packages/web/src/markdown/OpenChartLink.js b/packages/web/src/markdown/OpenChartLink.js index 17979c31a..481bb446e 100644 --- a/packages/web/src/markdown/OpenChartLink.js +++ b/packages/web/src/markdown/OpenChartLink.js @@ -23,7 +23,7 @@ export default function OpenChartLink({ file, children }) { savedFile: file, }, }, - resp.data + { editor: resp.data } ); }; diff --git a/packages/web/src/modals/ImportExportModal.js b/packages/web/src/modals/ImportExportModal.js index f83c66781..e554d5fb8 100644 --- a/packages/web/src/modals/ImportExportModal.js +++ b/packages/web/src/modals/ImportExportModal.js @@ -104,7 +104,7 @@ function GenerateSctriptButton({ modalState }) { icon: 'img shell', tabComponent: 'ShellTab', }, - code + { editor: code } ); modalState.close(); }; diff --git a/packages/web/src/modals/SaveTabModal.js b/packages/web/src/modals/SaveTabModal.js index 0a9f55e85..266c405d0 100644 --- a/packages/web/src/modals/SaveTabModal.js +++ b/packages/web/src/modals/SaveTabModal.js @@ -13,7 +13,12 @@ export default function SaveTabModal({ data, folder, format, modalState, tabid, changeTab(tabid, setOpenedTabs, (tab) => ({ ...tab, title: name, - props: { ...tab.props, savedFile: name }, + props: { + ...tab.props, + savedFile: name, + savedFolder: folder, + savedFormat: format, + }, })); const handleKeyboard = React.useCallback( diff --git a/packages/web/src/query/useNewQuery.js b/packages/web/src/query/useNewQuery.js index bbff4f7e6..ac2f33ca0 100644 --- a/packages/web/src/query/useNewQuery.js +++ b/packages/web/src/query/useNewQuery.js @@ -24,6 +24,6 @@ export default function useNewQuery() { database, }, }, - initialData + { editor: initialData } ); } diff --git a/packages/web/src/tabs/ChartTab.js b/packages/web/src/tabs/ChartTab.js index 40060510a..0cbc13381 100644 --- a/packages/web/src/tabs/ChartTab.js +++ b/packages/web/src/tabs/ChartTab.js @@ -74,3 +74,5 @@ export default function ChartTab({ tabVisible, toolbarPortalRef, conid, database ); } + +ChartTab.allowAddToFavorites = (props) => props.savedFile; diff --git a/packages/web/src/tabs/MarkdownViewTab.js b/packages/web/src/tabs/MarkdownViewTab.js index 94ec9ad9b..a10eeacbd 100644 --- a/packages/web/src/tabs/MarkdownViewTab.js +++ b/packages/web/src/tabs/MarkdownViewTab.js @@ -3,13 +3,17 @@ import axios from '../utility/axios'; import LoadingInfo from '../widgets/LoadingInfo'; import MarkdownExtendedView from '../markdown/MarkdownExtendedView'; -export default function MarkdownViewTab({ file }) { +export default function MarkdownViewTab({ savedFile }) { const [isLoading, setIsLoading] = React.useState(false); const [text, setText] = React.useState(null); const handleLoad = async () => { setIsLoading(true); - const resp = await axios.post('files/load', { folder: 'markdown', file, format: 'text' }); + const resp = await axios.post('files/load', { + folder: 'markdown', + file: savedFile, + format: 'text', + }); setText(resp.data); setIsLoading(false); }; @@ -28,3 +32,5 @@ export default function MarkdownViewTab({ file }) { return {text || ''}; } + +MarkdownViewTab.allowAddToFavorites = (props) => true; diff --git a/packages/web/src/tabs/QueryTab.js b/packages/web/src/tabs/QueryTab.js index bddc3daa3..76b3d9b1d 100644 --- a/packages/web/src/tabs/QueryTab.js +++ b/packages/web/src/tabs/QueryTab.js @@ -176,3 +176,5 @@ export default function QueryTab({ tabid, conid, database, initialArgs, tabVisib ); } + +QueryTab.allowAddToFavorites = (props) => props.savedFile; diff --git a/packages/web/src/tabs/ShellTab.js b/packages/web/src/tabs/ShellTab.js index 66bb4fee7..275064f33 100644 --- a/packages/web/src/tabs/ShellTab.js +++ b/packages/web/src/tabs/ShellTab.js @@ -139,3 +139,5 @@ export default function ShellTab({ tabid, tabVisible, toolbarPortalRef, ...other ); } + +ShellTab.allowAddToFavorites = (props) => props.savedFile; diff --git a/packages/web/src/tabs/TableDataTab.js b/packages/web/src/tabs/TableDataTab.js index cd67a3a33..86b41ba64 100644 --- a/packages/web/src/tabs/TableDataTab.js +++ b/packages/web/src/tabs/TableDataTab.js @@ -28,3 +28,4 @@ export default function TableDataTab({ conid, database, schemaName, pureName, ta } TableDataTab.matchingProps = ['conid', 'database', 'schemaName', 'pureName']; +TableDataTab.allowAddToFavorites = (props) => true; diff --git a/packages/web/src/tabs/ViewDataTab.js b/packages/web/src/tabs/ViewDataTab.js index 949e65d0a..8343ab1cf 100644 --- a/packages/web/src/tabs/ViewDataTab.js +++ b/packages/web/src/tabs/ViewDataTab.js @@ -55,4 +55,5 @@ export default function ViewDataTab({ conid, database, schemaName, pureName, tab ); } -ViewDataTab.matchingProps = ['conid', 'database', 'schemaName', 'pureName']; \ No newline at end of file +ViewDataTab.matchingProps = ['conid', 'database', 'schemaName', 'pureName']; +ViewDataTab.allowAddToFavorites = (props) => true; diff --git a/packages/web/src/utility/metadataLoaders.js b/packages/web/src/utility/metadataLoaders.js index d39921338..6d0077a99 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 favoritesLoader = () => ({ + url: 'files/favorites', + params: {}, + reloadTrigger: 'files-changed-favorites', +}); + const markdownManifestLoader = () => ({ url: 'files/markdown-manifest', params: {}, @@ -282,3 +288,10 @@ export function getMarkdownManifest(args) { export function useMarkdownManifest(args) { return useCore(markdownManifestLoader, args); } + +export function getFavorites(args) { + return getCore(favoritesLoader, args); +} +export function useFavorites(args) { + return useCore(favoritesLoader, args); +} diff --git a/packages/web/src/utility/useEditorData.js b/packages/web/src/utility/useEditorData.js index fe4f0dc58..25ec2cb6a 100644 --- a/packages/web/src/utility/useEditorData.js +++ b/packages/web/src/utility/useEditorData.js @@ -5,7 +5,7 @@ import { changeTab } from './common'; import { useSetOpenedTabs } from './globalState'; export default function useEditorData({ tabid, reloadToken = 0, loadFromArgs = null }) { - const localStorageKey = `tabdata_${tabid}`; + const localStorageKey = `tabdata_editor_${tabid}`; const setOpenedTabs = useSetOpenedTabs(); const changeCounterRef = React.useRef(0); const savedCounterRef = React.useRef(0); diff --git a/packages/web/src/utility/useOpenNewTab.js b/packages/web/src/utility/useOpenNewTab.js index 24d33f5e5..964100c79 100644 --- a/packages/web/src/utility/useOpenNewTab.js +++ b/packages/web/src/utility/useOpenNewTab.js @@ -46,7 +46,13 @@ export default function useOpenNewTab() { const tabid = uuidv1(); if (initialData) { - await localforage.setItem(`tabdata_${tabid}`, initialData); + for (const key of _.keys(initialData)) { + if (key == 'editor') { + await localforage.setItem(`tabdata_${key}_${tabid}`, initialData[key]); + } else { + localStorage.setItem(`tabdata_${key}_${tabid}`, JSON.stringify(initialData[key])); + } + } } setOpenedTabs((files) => [ ...(files || []).map((x) => ({ ...x, selected: false })), diff --git a/packages/web/src/widgets/FavoritesWidget.js b/packages/web/src/widgets/FavoritesWidget.js new file mode 100644 index 000000000..2bb45b8b2 --- /dev/null +++ b/packages/web/src/widgets/FavoritesWidget.js @@ -0,0 +1,57 @@ +import React from 'react'; +import _ from 'lodash'; + +import { AppObjectList } from '../appobj/AppObjectList'; +import { useOpenedTabs } from '../utility/globalState'; +import ClosedTabAppObject from '../appobj/ClosedTabAppObject'; +import { WidgetsInnerContainer } from './WidgetStyles'; +import { FavoriteFileAppObject } from '../appobj/SavedFileAppObject'; +import WidgetColumnBar, { WidgetColumnBarItem } from './WidgetColumnBar'; +import { useFavorites, useFiles } from '../utility/metadataLoaders'; +import useHasPermission from '../utility/useHasPermission'; + +function ClosedTabsList() { + const tabs = useOpenedTabs(); + + return ( + <> + + x.closedTime), + (x) => -x.closedTime + )} + AppObjectComponent={ClosedTabAppObject} + /> + + + ); +} + +function FavoritesList() { + const favorites = useFavorites(); + + return ( + <> + + + + + ); +} + +export default function FavoritesWidget() { + const hasPermission = useHasPermission(); + return ( + + + + + {hasPermission('files/favorites/read') && ( + + + + )} + + ); +} diff --git a/packages/web/src/widgets/FilesWidget.js b/packages/web/src/widgets/FilesWidget.js index c462782a6..3a76fe371 100644 --- a/packages/web/src/widgets/FilesWidget.js +++ b/packages/web/src/widgets/FilesWidget.js @@ -15,23 +15,6 @@ import WidgetColumnBar, { WidgetColumnBarItem } from './WidgetColumnBar'; import { useFiles } from '../utility/metadataLoaders'; import useHasPermission from '../utility/useHasPermission'; -function ClosedTabsList() { - const tabs = useOpenedTabs(); - - return ( - <> - - x.closedTime), - (x) => -x.closedTime - )} - AppObjectComponent={ClosedTabAppObject} - /> - - - ); -} function SavedSqlFilesList() { const files = useFiles({ folder: 'sql' }); @@ -85,9 +68,6 @@ export default function FilesWidget() { const hasPermission = useHasPermission(); return ( - - - {hasPermission('files/sql/read') && ( diff --git a/packages/web/src/widgets/Toolbar.js b/packages/web/src/widgets/Toolbar.js index 92ff997be..ba117009c 100644 --- a/packages/web/src/widgets/Toolbar.js +++ b/packages/web/src/widgets/Toolbar.js @@ -14,6 +14,9 @@ import { getDefaultFileFormat } from '../utility/fileformats'; import getElectron from '../utility/getElectron'; import AboutModal from '../modals/AboutModal'; import useOpenNewTab from '../utility/useOpenNewTab'; +import axios from '../utility/axios'; +import tabs from '../tabs'; +import uuidv1 from 'uuid/v1'; const ToolbarContainer = styled.div` display: flex; @@ -36,6 +39,8 @@ export default function ToolBar({ toolbarPortalRef }) { const electron = getElectron(); const markdownManifest = useMarkdownManifest(); + const currentTab = openedTabs.find((x) => x.selected); + React.useEffect(() => { window['dbgate_createNewConnection'] = modalState.open; window['dbgate_newQuery'] = newQuery; @@ -76,6 +81,31 @@ export default function ToolBar({ toolbarPortalRef }) { }); }; + const addToFavorite = () => { + const tabdata = {}; + + const re = new RegExp(`tabdata_(.*)_${currentTab.tabid}`); + for (const key in localStorage) { + const match = key.match(re); + if (!match) continue; + if (match[1] == 'editor') continue; + tabdata[match[1]] = JSON.parse(localStorage.getItem(key)); + } + + axios.post('files/save', { + folder: 'favorites', + file: uuidv1(), + format: 'json', + data: { + title: currentTab.title, + icon: currentTab.icon, + props: currentTab.props, + tabComponent: currentTab.tabComponent, + tabdata, + }, + }); + }; + function openTabFromButton(page) { if ( openedTabs.find( @@ -134,6 +164,14 @@ export default function ToolBar({ toolbarPortalRef }) { Import data + {!!currentTab && + tabs[currentTab.tabComponent] && + tabs[currentTab.tabComponent].allowAddToFavorites && + tabs[currentTab.tabComponent].allowAddToFavorites(currentTab.props) && ( + + Add to favorites + + )} {currentTheme == 'dark' ? 'Light mode' : 'Dark mode'} diff --git a/packages/web/src/widgets/WidgetContainer.js b/packages/web/src/widgets/WidgetContainer.js index 9fa204db5..ebd20cc78 100644 --- a/packages/web/src/widgets/WidgetContainer.js +++ b/packages/web/src/widgets/WidgetContainer.js @@ -2,6 +2,7 @@ import React from 'react'; import { useCurrentWidget } from '../utility/globalState'; import ArchiveWidget from './ArchiveWidget'; import DatabaseWidget from './DatabaseWidget'; +import FavoritesWidget from './FavoritesWidget'; import FilesWidget from './FilesWidget'; import PluginsWidget from './PluginsWidget'; @@ -11,5 +12,6 @@ export default function WidgetContainer() { if (currentWidget === 'file') return ; if (currentWidget === 'archive') return ; if (currentWidget === 'plugins') return ; + if (currentWidget === 'favorites') return ; return null; } diff --git a/packages/web/src/widgets/WidgetIconPanel.js b/packages/web/src/widgets/WidgetIconPanel.js index e00cbcd37..179409d8f 100644 --- a/packages/web/src/widgets/WidgetIconPanel.js +++ b/packages/web/src/widgets/WidgetIconPanel.js @@ -56,6 +56,11 @@ export default function WidgetIconPanel() { name: 'plugins', title: 'Extensions & Plugins', }, + { + icon: 'icon favorite', + name: 'favorites', + title: 'Favorites', + }, // { // icon: 'fa-cog', // name: 'settings',