diff --git a/webapp/packages/core-blocks/src/Containers/BASE_CONTAINERS_STYLES.ts b/webapp/packages/core-blocks/src/Containers/BASE_CONTAINERS_STYLES.ts new file mode 100644 index 0000000000..d0ca7e73c1 --- /dev/null +++ b/webapp/packages/core-blocks/src/Containers/BASE_CONTAINERS_STYLES.ts @@ -0,0 +1,82 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { css } from 'reshadow'; + +import { composes } from '@cloudbeaver/core-theming'; + +export const BASE_CONTAINERS_STYLES = composes( + css` + Group { + composes: theme-background-surface from global; + } + ColoredContainer { + composes: theme-background-secondary from global; + } + `, + css` + Container, ColoredContainer { + display: flex; + flex-direction: column; + &[horizontal] { + flex-direction: row; + } + &[wrap] { + flex-wrap: wrap; + } + &[overflow] { + overflow: auto; + } + } + + Group { + box-sizing: border-box; + margin: 10px; + padding: 10px; + border-radius: 4px; + } + + Grid { + display: grid; + grid-template-columns: minmax(min-content, 1fr); + grid-auto-rows: max-content; + &[horizontal] { + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + } + &[center] { + margin: 0 auto; + } + } + + Grid { + & > [itemMax] { + grid-column: 1/-1; + } + } + + GroupTitle { + composes: theme-typography--body1 from global; + font-weight: 400; + padding-left: 12px; + text-transform: uppercase; + opacity: 0.9; + } + + Container, ColoredContainer, Group, Grid { + flex-grow: 1; + &[small] { + max-width: 250px; + } + &[medium] { + max-width: 450px; + } + &[large] { + max-width: 650px; + } + } + `); diff --git a/webapp/packages/core-blocks/src/Containers/ColoredContainer.tsx b/webapp/packages/core-blocks/src/Containers/ColoredContainer.tsx new file mode 100644 index 0000000000..813097debe --- /dev/null +++ b/webapp/packages/core-blocks/src/Containers/ColoredContainer.tsx @@ -0,0 +1,24 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import type { ILayoutSizeProps } from './ILayoutContainerProps'; + +interface Props extends ILayoutSizeProps { + className?: string; + horizontal?: boolean; + wrap?: boolean; + overflow?: boolean; +} + +export const ColoredContainer: React.FC = function ColoredContainer({ children, className }) { + return ( +
+ {children} +
+ ); +}; diff --git a/webapp/packages/core-blocks/src/Containers/Container.tsx b/webapp/packages/core-blocks/src/Containers/Container.tsx new file mode 100644 index 0000000000..feeb561817 --- /dev/null +++ b/webapp/packages/core-blocks/src/Containers/Container.tsx @@ -0,0 +1,24 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import type { ILayoutSizeProps } from './ILayoutContainerProps'; + +interface Props extends ILayoutSizeProps { + className?: string; + horizontal?: boolean; + wrap?: boolean; + overflow?: boolean; +} + +export const Container: React.FC = function Container({ children, className }) { + return ( +
+ {children} +
+ ); +}; diff --git a/webapp/packages/core-blocks/src/Containers/Grid.tsx b/webapp/packages/core-blocks/src/Containers/Grid.tsx new file mode 100644 index 0000000000..535d37429d --- /dev/null +++ b/webapp/packages/core-blocks/src/Containers/Grid.tsx @@ -0,0 +1,23 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import type { ILayoutSizeProps } from './ILayoutContainerProps'; + +interface Props extends ILayoutSizeProps { + className?: string; + horizontal?: boolean; + center?: boolean; +} + +export const Grid: React.FC = function Grid({ children, className }) { + return ( +
+ {children} +
+ ); +}; diff --git a/webapp/packages/core-blocks/src/Containers/Group.tsx b/webapp/packages/core-blocks/src/Containers/Group.tsx new file mode 100644 index 0000000000..28f81c271a --- /dev/null +++ b/webapp/packages/core-blocks/src/Containers/Group.tsx @@ -0,0 +1,21 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import type { ILayoutSizeProps } from './ILayoutContainerProps'; + +interface Props extends ILayoutSizeProps { + className?: string; +} + +export const Group: React.FC = function Group({ children, className }) { + return ( +
+ {children} +
+ ); +}; diff --git a/webapp/packages/core-blocks/src/Containers/GroupTitle.tsx b/webapp/packages/core-blocks/src/Containers/GroupTitle.tsx new file mode 100644 index 0000000000..ba105b14cf --- /dev/null +++ b/webapp/packages/core-blocks/src/Containers/GroupTitle.tsx @@ -0,0 +1,19 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +interface Props { + className?: string; +} + +export const GroupTitle: React.FC = function GroupTitle({ children, className }) { + return ( +

+ {children} +

+ ); +}; diff --git a/webapp/packages/core-blocks/src/Containers/ILayoutContainerProps.ts b/webapp/packages/core-blocks/src/Containers/ILayoutContainerProps.ts new file mode 100644 index 0000000000..b0b91658cb --- /dev/null +++ b/webapp/packages/core-blocks/src/Containers/ILayoutContainerProps.ts @@ -0,0 +1,9 @@ +export interface ILayoutSizeProps { + small?: boolean; + medium?: boolean; + large?: boolean; +} + +export interface IGridItemsLayoutProps { + itemMax?: boolean; +} diff --git a/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckboxNew.tsx b/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckboxNew.tsx new file mode 100644 index 0000000000..5a1a1f2781 --- /dev/null +++ b/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckboxNew.tsx @@ -0,0 +1,52 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import styled, { css } from 'reshadow'; + +import { useStyles } from '@cloudbeaver/core-theming'; + +import { baseFormControlStylesNew } from '../baseFormControlStylesNew'; +import { isControlPresented } from '../isControlPresented'; +import { Checkbox, CheckboxType, CheckboxControlledProps, CheckboxObjectProps } from './Checkbox'; + +const fieldCheckboxStyles = css` + Checkbox { + margin: -10px; + } + field { + display: flex; + align-items: center; + } + field-label { + padding-left: 10px; + } +`; + +export const FieldCheckboxNew: CheckboxType = function FieldCheckboxNew({ + checked: checkedControlled, + children, + className, + autoHide, + ...rest +}: CheckboxControlledProps | CheckboxObjectProps) { + const styles = useStyles(baseFormControlStylesNew, fieldCheckboxStyles); + + if (autoHide && !isControlPresented(rest.name, rest.state)) { + return null; + } + + return styled(styles)( + + + {children} + + ); +}; diff --git a/webapp/packages/core-blocks/src/FormControls/ComboboxNew.tsx b/webapp/packages/core-blocks/src/FormControls/ComboboxNew.tsx new file mode 100644 index 0000000000..2e3094a6fb --- /dev/null +++ b/webapp/packages/core-blocks/src/FormControls/ComboboxNew.tsx @@ -0,0 +1,255 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react-lite'; +import { + useLayoutEffect, useCallback, useState, useRef, useContext +} from 'react'; +import { + useMenuState, + Menu, + MenuItem, + MenuButton +} from 'reakit/Menu'; +import styled, { css, use } from 'reshadow'; + +import { useStyles, composes } from '@cloudbeaver/core-theming'; + +import { IconButton } from '../IconButton'; +import { Icon } from '../Icons/Icon'; +import { baseFormControlStylesNew } from './baseFormControlStylesNew'; +import { FormContext } from './FormContext'; + +const styles = composes( + css` + Menu { + composes: theme-text-on-surface theme-background-surface from global; + } + MenuItem { + composes: theme-ripple from global; + } + `, + css` + field input { + margin: 0; + } + field-label { + display: block; + padding-bottom: 5px; + } + input { + padding-right: 20px; + } + MenuButton { + position: absolute; + right: 0; + background: transparent; + outline: none; + padding: 4px; + cursor: pointer; + &:hover { + opacity: 0.7; + } + } + + Menu { + composes: theme-typography--caption theme-elevation-z3 from global; + display: flex; + flex-direction: column; + width: 100%; + max-height: 150px; + overflow: auto; + outline: none; + padding: 4px 0; + z-index: 999; + + & MenuItem { + background: transparent; + display: block; + padding: 4px 36px; + text-align: left; + outline: none; + color: inherit; + cursor: pointer; + } + } + Icon { + height: 16px; + display: block; + } + MenuButton Icon { + transform: rotate(90deg); + + &[|focus] { + transform: rotate(-90deg); + } + } + input-box { + flex: 1; + position: relative; + display: flex; + align-items: center; + } + ` +); + +type BaseProps = Omit, 'onChange' | 'onSelect' | 'name' | 'value'> & { + propertyName?: string; + items: TValue[]; + searchable?: boolean; + keySelector: (item: TValue) => TKey; + valueSelector: (item: TValue) => string; + onSwitch?: (state: boolean) => void; +}; + +type ControlledProps = BaseProps & { + name?: string; + value?: string; + onSelect?: (value: TKey, name: string | undefined, prev: TKey) => void; + onChange?: (value: string, name: string | undefined) => any; + state?: never; +}; + +type ObjectProps = BaseProps & { + name: TKey; + state: TState; + onSelect?: (value: TState[TKey], name: TKey | undefined, prev: TState[TKey]) => void; + onChange?: (value: string, name: TKey | undefined) => any; + value?: never; +}; + +interface ComboboxType { + (props: ControlledProps): JSX.Element; + (props: ObjectProps): JSX.Element; +} + +export const ComboboxNew: ComboboxType = observer(function ComboboxNew({ + value: controlledValue, + name, + state, + propertyName, + items, + children, + title, + className, + searchable, + readOnly, + disabled, + keySelector = v => v, + valueSelector = v => v, + onChange = () => {}, + onSelect, + onSwitch, + ...rest +}: ControlledProps | ObjectProps) { + const context = useContext(FormContext); + const ref = useRef(null); + const menu = useMenuState({ + placement: 'bottom-end', + currentId: null, + gutter: 4, + }); + const [searchValue, setSearchValue] = useState(''); + let value: string | number | readonly string[] | undefined = controlledValue; + + if (state) { + if (name in state) { + value = state[name]; + } else if (rest.defaultValue !== undefined) { + value = rest.defaultValue; + } + } + + const handleChange = useCallback( + (event: React.ChangeEvent) => { + const value = event.target.value; + onChange(value, name); + setSearchValue(value); + }, + [name, onChange] + ); + + const handleRemove = useCallback( + () => { + menu.hide(); + if (state) { + state[name] = null; + } + if (onSelect) { + onSelect(null, name, value); + } + if (context) { + context.onChange(null, name); + } + setSearchValue(''); + }, + [value, state, name, menu, context, onSelect] + ); + + const handleMenuSelect = useCallback( + (event: React.MouseEvent) => { + menu.hide(); + const id = event.currentTarget.id; + if (state) { + state[name] = id; + } + if (onSelect) { + onSelect(id, name, value); + } + if (context) { + context.onChange(id, name); + } + }, + [value, state, name, menu, context, onSelect] + ); + + useLayoutEffect(() => onSwitch?.(menu.visible), [onSwitch, menu.visible]); + + const selectedItem = items.find(item => keySelector(item) === value); + const filteredItems = items.filter( + item => selectedItem || !searchValue || valueSelector(item).toUpperCase().includes(searchValue.toUpperCase()) + ); + + if (ref.current === document.activeElement && !selectedItem && searchValue) { + menu.show(); + } + + return styled(useStyles(baseFormControlStylesNew, styles))( + + {children} + + + {(selectedItem && !readOnly && searchable) && ( + + )} + + + {filteredItems.map(item => ( + + {valueSelector(item)} + + ))} + + + + ); +}); diff --git a/webapp/packages/core-blocks/src/FormControls/InputFieldNew.tsx b/webapp/packages/core-blocks/src/FormControls/InputFieldNew.tsx new file mode 100644 index 0000000000..83b381df46 --- /dev/null +++ b/webapp/packages/core-blocks/src/FormControls/InputFieldNew.tsx @@ -0,0 +1,109 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react-lite'; +import { useCallback, useContext } from 'react'; +import styled, { use, css } from 'reshadow'; + +import { useStyles } from '@cloudbeaver/core-theming'; + +import type { ILayoutSizeProps } from '../Containers/ILayoutContainerProps'; +import { baseFormControlStylesNew } from './baseFormControlStylesNew'; +import { FormContext } from './FormContext'; +import { isControlPresented } from './isControlPresented'; + +const INPUT_FIELD_STYLES = css` + field-label { + display: block; + padding-bottom: 5px; + } +`; + +type BaseProps = Omit, 'onChange' | 'name' | 'value'> & ILayoutSizeProps & { + description?: string; + mod?: 'surface'; +}; + +type ControlledProps = BaseProps & { + name?: string; + value?: string; + onChange?: (value: string, name?: string) => any; + state?: never; + autoHide?: never; +}; + +type ObjectProps = BaseProps & { + name: TKey; + state: TState; + onChange?: (value: string, name: TKey) => any; + autoHide?: boolean; + value?: never; +}; + +interface InputFieldType { + (props: ControlledProps): React.ReactElement | null; + (props: ObjectProps): React.ReactElement | null; +} + +export const InputFieldNew: InputFieldType = observer(function InputFieldNew({ + name, + value: valueControlled, + required, + state, + children, + className, + description, + mod, + small, + medium, + large, + autoHide, + onChange, + ...rest +}: ControlledProps | ObjectProps) { + const styles = useStyles(baseFormControlStylesNew, INPUT_FIELD_STYLES); + const context = useContext(FormContext); + + const handleChange = useCallback((event: React.ChangeEvent) => { + if (state) { + state[name] = event.target.value; + } + if (onChange) { + onChange(event.target.value, name); + } + if (context) { + context.onChange(event.target.value, name); + } + }, [state, name, context, onChange]); + + const value = state ? state[name] : valueControlled; + + if (autoHide && !isControlPresented(name, state)) { + return null; + } + + return styled(styles)( + + {children} {required && '*'} + + {description && ( + + {description} + + )} + + ); +}); diff --git a/webapp/packages/core-blocks/src/FormControls/SwitchNew.tsx b/webapp/packages/core-blocks/src/FormControls/SwitchNew.tsx new file mode 100644 index 0000000000..0ee232fb3e --- /dev/null +++ b/webapp/packages/core-blocks/src/FormControls/SwitchNew.tsx @@ -0,0 +1,168 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react-lite'; +import { useCallback, useContext } from 'react'; +import styled, { css, use } from 'reshadow'; + +import { useStyles, composes } from '@cloudbeaver/core-theming'; + +import { baseFormControlStylesNew } from './baseFormControlStylesNew'; +import { FormContext } from './FormContext'; +import { isControlPresented } from './isControlPresented'; + +const switchStyles = composes( + css` + switch-control { + composes: theme-switch from global; + } + switch-control-track { + composes: theme-switch__track from global; + } + switch-input { + composes: theme-switch_native-control from global; + } + switch-control-underlay { + composes: theme-switch__thumb-underlay from global; + } + switch-control-thumb { + composes: theme-switch__thumb from global; + } + radio-ripple { + composes: theme-radio_ripple from global; + } + `, + css` + field-label { + display: block; + padding-bottom: 10px; + } + ` +); + +const switchMod = { + primary: composes( + css` + switch-control { + composes: theme-switch_primary from global; + } + ` + ), +}; + +const switchState = { + disabled: composes( + css` + switch-control { + composes: theme-switch--disabled from global; + } + ` + ), + checked: composes( + css` + switch-control { + composes: theme-switch--checked from global; + } + ` + ), +}; + +type BaseProps = Omit, 'onChange' | 'type' | 'value' | 'checked'> & { + mod?: Array; + label?: string; + description?: string; +}; + +type ControlledProps = BaseProps & { + checked?: boolean; + onChange?: (value: boolean, name?: string) => any; + + state?: never; + autoHide?: never; +}; + +type ObjectProps = BaseProps & { + name: TKey; + state: TState; + onChange?: (value: boolean, name: TKey) => any; + autoHide?: boolean; + + checked?: never; +}; + +interface SwitchType { + (props: ControlledProps): React.ReactElement | null; + (props: ObjectProps): React.ReactElement | null; +} + +export const SwitchNew: SwitchType = observer(function SwitchNew({ + name, + id, + label, + description, + state, + checked: checkedControlled, + className, + children, + onChange, + mod = [], + autoHide, + disabled, + ...rest +}: ControlledProps | ObjectProps) { + const context = useContext(FormContext); + const checked = state ? state[name] : checkedControlled; + const styles = useStyles( + baseFormControlStylesNew, + switchStyles, + ...mod.map(mod => switchMod[mod]), + disabled && switchState.disabled, + checked && switchState.checked + ); + + const handleChange = useCallback((event: React.ChangeEvent) => { + if (state) { + state[name] = event.target.checked; + } + if (onChange) { + onChange(event.target.checked, name); + } + if (context) { + context.onChange(event.target.checked, name); + } + }, [state, name, context, onChange]); + + if (autoHide && !isControlPresented(name, state)) { + return null; + } + + return styled(styles)( + + {children} + + + + + + + + + {description} + + ); +}); diff --git a/webapp/packages/core-blocks/src/FormControls/TextareaNew.tsx b/webapp/packages/core-blocks/src/FormControls/TextareaNew.tsx new file mode 100644 index 0000000000..b7164bf9a0 --- /dev/null +++ b/webapp/packages/core-blocks/src/FormControls/TextareaNew.tsx @@ -0,0 +1,94 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react-lite'; +import { useCallback, useContext } from 'react'; +import styled, { css, use } from 'reshadow'; + +import { useStyles } from '@cloudbeaver/core-theming'; + +import type { ILayoutSizeProps } from '../Containers/ILayoutContainerProps'; +import { baseFormControlStylesNew } from './baseFormControlStylesNew'; +import { FormContext } from './FormContext'; + +const styles = css` + textarea { + line-height: 19px; + } + field-label { + display: block; + padding-bottom: 5px; + } +`; + +type BaseProps = Omit, 'onChange'> & ILayoutSizeProps & { + mod?: 'surface'; +}; + +type ControlledProps = BaseProps & { + name?: string; + value?: string; + onChange?: (value: string, name?: string) => any; + state?: never; +}; + +type ObjectProps = BaseProps & { + name: TKey; + state: TState; + onChange?: (value: string, name: TKey) => any; + value?: never; +}; + +interface TextareaType { + (props: ControlledProps): JSX.Element; + (props: ObjectProps): JSX.Element; +} + +export const TextareaNew: TextareaType = observer(function TextareaNew({ + name, + value: controlledValue, + state, + children, + className, + small, + medium, + large, + mod, + onChange = () => {}, + ...rest +}: ControlledProps | ObjectProps) { + const context = useContext(FormContext); + + const handleChange = useCallback((event: React.ChangeEvent) => { + if (state) { + state[name] = event.target.value; + } + if (onChange) { + onChange(event.target.value, name); + } + if (context) { + context.onChange(event.target.value, name); + } + }, [state, name, onChange]); + + const value = state ? state[name] : controlledValue; + + return styled(useStyles(baseFormControlStylesNew, styles))( + + {children} +