resizable widgets

This commit is contained in:
Jan Prochazka
2020-11-05 12:17:34 +01:00
parent fd9fa0c95a
commit 7888cf6714
6 changed files with 114 additions and 40 deletions

View File

@@ -12,6 +12,7 @@ import {
WidgetsOuterContainer, WidgetsOuterContainer,
WidgetTitle, WidgetTitle,
} from './WidgetStyles'; } from './WidgetStyles';
import WidgetColumnBar, { WidgetColumnBarItem } from './WidgetColumnBar';
import savedSqlFileAppObject from '../appobj/savedSqlFileAppObject'; import savedSqlFileAppObject from '../appobj/savedSqlFileAppObject';
import { useArchiveFiles, useArchiveFolders } from '../utility/metadataLoaders'; import { useArchiveFiles, useArchiveFolders } from '../utility/metadataLoaders';
import archiveFolderAppObject from '../appobj/archiveFolderAppObject'; import archiveFolderAppObject from '../appobj/archiveFolderAppObject';
@@ -22,7 +23,6 @@ import axios from '../utility/axios';
function ArchiveFolderList() { function ArchiveFolderList() {
const folders = useArchiveFolders(); const folders = useArchiveFolders();
const inputRef = React.useRef(null);
const [filter, setFilter] = React.useState(''); const [filter, setFilter] = React.useState('');
const setArchive = useSetCurrentArchive(); const setArchive = useSetCurrentArchive();
@@ -33,9 +33,8 @@ function ArchiveFolderList() {
return ( return (
<> <>
<WidgetTitle inputRef={inputRef}>Archive folder</WidgetTitle>
<SearchBoxWrapper> <SearchBoxWrapper>
<SearchInput inputRef={inputRef} placeholder="Search archive folders" filter={filter} setFilter={setFilter} /> <SearchInput placeholder="Search archive folders" filter={filter} setFilter={setFilter} />
<InlineButton onClick={handleRefreshFolders}>Refresh</InlineButton> <InlineButton onClick={handleRefreshFolders}>Refresh</InlineButton>
</SearchBoxWrapper> </SearchBoxWrapper>
<WidgetsInnerContainer> <WidgetsInnerContainer>
@@ -53,7 +52,6 @@ function ArchiveFolderList() {
function ArchiveFilesList() { function ArchiveFilesList() {
const folder = useCurrentArchive(); const folder = useCurrentArchive();
const files = useArchiveFiles({ folder }); const files = useArchiveFiles({ folder });
const inputRef = React.useRef(null);
const [filter, setFilter] = React.useState(''); const [filter, setFilter] = React.useState('');
const handleRefreshFiles = () => { const handleRefreshFiles = () => {
axios.post('archive/refresh-files', { folder }); axios.post('archive/refresh-files', { folder });
@@ -61,9 +59,8 @@ function ArchiveFilesList() {
return ( return (
<> <>
<WidgetTitle inputRef={inputRef}>Archive files</WidgetTitle>
<SearchBoxWrapper> <SearchBoxWrapper>
<SearchInput inputRef={inputRef} placeholder="Search archive files" filter={filter} setFilter={setFilter} /> <SearchInput placeholder="Search archive files" filter={filter} setFilter={setFilter} />
<InlineButton onClick={handleRefreshFiles}>Refresh</InlineButton> <InlineButton onClick={handleRefreshFiles}>Refresh</InlineButton>
</SearchBoxWrapper> </SearchBoxWrapper>
<WidgetsInnerContainer> <WidgetsInnerContainer>
@@ -82,13 +79,13 @@ function ArchiveFilesList() {
export default function ArchiveWidget() { export default function ArchiveWidget() {
return ( return (
<WidgetsMainContainer> <WidgetColumnBar>
<WidgetsOuterContainer> <WidgetColumnBarItem title="Archive folders" name="folders" height="50%">
<ArchiveFolderList /> <ArchiveFolderList />
</WidgetsOuterContainer> </WidgetColumnBarItem>
<WidgetsOuterContainer> <WidgetColumnBarItem title="Archive files" name="files">
<ArchiveFilesList /> <ArchiveFilesList />
</WidgetsOuterContainer> </WidgetColumnBarItem>
</WidgetsMainContainer> </WidgetColumnBar>
); );
} }

View File

@@ -26,6 +26,7 @@ import axios from '../utility/axios';
import LoadingInfo from './LoadingInfo'; import LoadingInfo from './LoadingInfo';
import SearchInput from './SearchInput'; import SearchInput from './SearchInput';
import ErrorInfo from './ErrorInfo'; import ErrorInfo from './ErrorInfo';
import WidgetColumnBar, { WidgetColumnBarItem } from './WidgetColumnBar';
function SubDatabaseList({ data }) { function SubDatabaseList({ data }) {
const setDb = useSetCurrentDatabase(); const setDb = useSetCurrentDatabase();
@@ -60,14 +61,12 @@ function ConnectionList() {
axios.post('server-connections/refresh', { conid }); axios.post('server-connections/refresh', { conid });
} }
}; };
const inputRef = React.useRef(null);
const [filter, setFilter] = React.useState(''); const [filter, setFilter] = React.useState('');
return ( return (
<> <>
<WidgetTitle inputRef={inputRef}>Connections</WidgetTitle>
<SearchBoxWrapper> <SearchBoxWrapper>
<SearchInput placeholder="Search connection" filter={filter} setFilter={setFilter} inputRef={inputRef} /> <SearchInput placeholder="Search connection" filter={filter} setFilter={setFilter} />
<InlineButton onClick={handleRefreshConnections}>Refresh</InlineButton> <InlineButton onClick={handleRefreshConnections}>Refresh</InlineButton>
</SearchBoxWrapper> </SearchBoxWrapper>
@@ -103,7 +102,6 @@ function SqlObjectList({ conid, database }) {
const inputRef = React.useRef(null); const inputRef = React.useRef(null);
return ( return (
<> <>
<WidgetTitle inputRef={inputRef}>Tables, views, functions</WidgetTitle>
<SearchBoxWrapper> <SearchBoxWrapper>
<SearchInput inputRef={inputRef} placeholder="Search tables or objects" filter={filter} setFilter={setFilter} /> <SearchInput inputRef={inputRef} placeholder="Search tables or objects" filter={filter} setFilter={setFilter} />
<InlineButton onClick={handleRefreshDatabase}>Refresh</InlineButton> <InlineButton onClick={handleRefreshDatabase}>Refresh</InlineButton>
@@ -128,12 +126,7 @@ function SqlObjectListWrapper() {
const db = useCurrentDatabase(); const db = useCurrentDatabase();
if (!db) { if (!db) {
return ( return <ErrorInfo message="Database not selected" icon="fas fa-exclamation-circle blue" />;
<>
<WidgetTitle>Tables, views, functions</WidgetTitle>
<ErrorInfo message="Database not selected" icon="fas fa-exclamation-circle blue" />
</>
);
} }
const { name, connection } = db; const { name, connection } = db;
@@ -144,13 +137,13 @@ function SqlObjectListWrapper() {
export default function DatabaseWidget() { export default function DatabaseWidget() {
return ( return (
<WidgetsMainContainer> <WidgetColumnBar>
<WidgetsOuterContainer> <WidgetColumnBarItem title="Connections" name="connections" height="50%">
<ConnectionList /> <ConnectionList />
</WidgetsOuterContainer> </WidgetColumnBarItem>
<WidgetsOuterContainer> <WidgetColumnBarItem title="Tables, views, functions" name="dbObjects">
<SqlObjectListWrapper /> <SqlObjectListWrapper />
</WidgetsOuterContainer> </WidgetColumnBarItem>
</WidgetsMainContainer> </WidgetColumnBar>
); );
} }

View File

@@ -13,13 +13,13 @@ import {
WidgetTitle, WidgetTitle,
} from './WidgetStyles'; } from './WidgetStyles';
import savedSqlFileAppObject from '../appobj/savedSqlFileAppObject'; import savedSqlFileAppObject from '../appobj/savedSqlFileAppObject';
import WidgetColumnBar, { WidgetColumnBarItem } from './WidgetColumnBar';
function ClosedTabsList() { function ClosedTabsList() {
const tabs = useOpenedTabs(); const tabs = useOpenedTabs();
return ( return (
<> <>
<WidgetTitle>Recently closed tabs</WidgetTitle>
<WidgetsInnerContainer> <WidgetsInnerContainer>
<AppObjectList <AppObjectList
list={_.sortBy( list={_.sortBy(
@@ -38,7 +38,6 @@ function SavedSqlFilesList() {
return ( return (
<> <>
<WidgetTitle>Saved SQL files</WidgetTitle>
<WidgetsInnerContainer> <WidgetsInnerContainer>
<AppObjectList list={files} makeAppObj={savedSqlFileAppObject()} /> <AppObjectList list={files} makeAppObj={savedSqlFileAppObject()} />
</WidgetsInnerContainer> </WidgetsInnerContainer>
@@ -48,13 +47,13 @@ function SavedSqlFilesList() {
export default function FilesWidget() { export default function FilesWidget() {
return ( return (
<WidgetsMainContainer> <WidgetColumnBar>
<WidgetsOuterContainer> <WidgetColumnBarItem title="Recently closed tabs" name="closedTabs" height="50%">
<ClosedTabsList /> <ClosedTabsList />
</WidgetsOuterContainer> </WidgetColumnBarItem>
<WidgetsOuterContainer> <WidgetColumnBarItem title="Saved SQL files" name="sqlFiles">
<SavedSqlFilesList /> <SavedSqlFilesList />
</WidgetsOuterContainer> </WidgetColumnBarItem>
</WidgetsMainContainer> </WidgetColumnBar>
); );
} }

View File

@@ -14,6 +14,7 @@ const SplitterMainBase = styled.div`
bottom: 0; bottom: 0;
`; `;
// @ts-ignore
const VerticalMainContainer = styled(SplitterMainBase)` const VerticalMainContainer = styled(SplitterMainBase)`
flex: 1; flex: 1;
display: flex; display: flex;

View File

@@ -0,0 +1,72 @@
import React from 'react';
import _ from 'lodash';
import {
SearchBoxWrapper,
WidgetsInnerContainer,
WidgetsMainContainer,
WidgetsOuterContainer,
WidgetTitle,
} from './WidgetStyles';
import { VerticalSplitHandle, useSplitterDrag } from './Splitter';
import useDimensions from '../utility/useDimensions';
export function WidgetColumnBarItem({ title, children, name, height = undefined }) {
return <></>;
}
function WidgetContainer({ widget, visible, splitterVisible, parentHeight, initialSize = undefined }) {
const [size, setSize] = React.useState(null);
const handleResizeDown = useSplitterDrag('clientY', (diff) => setSize((v) => v + diff));
React.useEffect(() => {
if (_.isString(initialSize) && initialSize.endsWith('px')) setSize(parseInt(initialSize.slice(0, -2)));
else if (_.isString(initialSize) && initialSize.endsWith('%'))
setSize((parentHeight * parseFloat(initialSize.slice(0, -1))) / 100);
else setSize(parentHeight / 3);
}, [parentHeight]);
if (!visible) return null;
return (
<>
<WidgetsOuterContainer style={splitterVisible ? { height: size } : null}>
{widget.props.children}
</WidgetsOuterContainer>
{splitterVisible && <VerticalSplitHandle onMouseDown={handleResizeDown} />}
</>
);
}
export default function WidgetColumnBar({ children }) {
const childArray = _.isArray(children) ? children : [children];
const [refNode, dimensions] = useDimensions();
const [collapsedWidgets, setCollapsedWidgets] = React.useState(() =>
childArray.filter((x) => x.props.collapsed).map((x) => x.props.key)
);
const toggleCollapsed = (name) => {
if (collapsedWidgets.includes(name)) setCollapsedWidgets(collapsedWidgets.filter((x) => x != name));
else setCollapsedWidgets([...collapsedWidgets, name]);
};
return (
<WidgetsMainContainer ref={refNode}>
{childArray.map((widget, index) => {
if (!widget) return null;
return (
<>
<WidgetTitle onClick={() => toggleCollapsed(widget.props.name)}>{widget.props.title}</WidgetTitle>
<WidgetContainer
parentHeight={dimensions && dimensions.height}
visible={!collapsedWidgets.includes(widget.props.name)}
widget={widget}
key={widget.props.name}
initialSize={widget.props.height}
splitterVisible={!!childArray.slice(index + 1).find((x) => !collapsedWidgets.includes(x.props.name))}
/>
</>
);
})}
</WidgetsMainContainer>
);
}

View File

@@ -19,7 +19,6 @@ export const WidgetsMainContainer = styled.div`
`; `;
const StyledWidgetsOuterContainer = styled.div` const StyledWidgetsOuterContainer = styled.div`
flex: 1 1 0;
overflow: hidden; overflow: hidden;
width: ${(props) => props.leftPanelWidth}px; width: ${(props) => props.leftPanelWidth}px;
position: relative; position: relative;
@@ -27,9 +26,20 @@ const StyledWidgetsOuterContainer = styled.div`
display: flex; display: flex;
`; `;
export function WidgetsOuterContainer({ children }) { export function WidgetsOuterContainer({ children, style = undefined, refNode = undefined }) {
const leftPanelWidth = useLeftPanelWidth(); const leftPanelWidth = useLeftPanelWidth();
return <StyledWidgetsOuterContainer leftPanelWidth={leftPanelWidth}>{children}</StyledWidgetsOuterContainer>; return (
<StyledWidgetsOuterContainer
ref={refNode}
leftPanelWidth={leftPanelWidth}
style={{
...style,
flex: style && (style.height != null || style.width != null) ? undefined : '1 1 0',
}}
>
{children}
</StyledWidgetsOuterContainer>
);
} }
export const StyledWidgetsInnerContainer = styled.div` export const StyledWidgetsInnerContainer = styled.div`
@@ -53,14 +63,16 @@ const StyledWidgetTitle = styled.div`
font-weight: bold; font-weight: bold;
text-transform: uppercase; text-transform: uppercase;
background-color: gray; background-color: gray;
border: 1px solid #aaa;
// background-color: #CEC; // background-color: #CEC;
`; `;
export function WidgetTitle({ children, inputRef = undefined }) { export function WidgetTitle({ children, inputRef = undefined, onClick = undefined }) {
return ( return (
<StyledWidgetTitle <StyledWidgetTitle
onClick={() => { onClick={() => {
if (inputRef && inputRef.current) inputRef.current.focus(); if (inputRef && inputRef.current) inputRef.current.focus();
if (onClick) onClick();
}} }}
> >
{children} {children}