From 831b942bea1e6fb2ccfc675b2663c1e6a324c7f7 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Wed, 24 Feb 2021 13:43:31 +0300 Subject: [PATCH 1/4] feat(core-theming): CB-724 add new styles for metadata editor --- .../src/Containers/BASE_CONTAINERS_STYLES.ts | 77 ++++++++++++ .../src/Containers/ColoredContainer.tsx | 23 ++++ .../core-blocks/src/Containers/Container.tsx | 23 ++++ .../core-blocks/src/Containers/Group.tsx | 22 ++++ .../src/Containers/GroupContainer.tsx | 21 ++++ .../core-blocks/src/Containers/GroupTitle.tsx | 19 +++ .../Checkboxes/FieldCheckboxNew.tsx | 52 ++++++++ .../src/FormControls/InputFieldNew.tsx | 112 ++++++++++++++++++ .../FormControls/baseFormControlStylesNew.ts | 42 +++++++ webapp/packages/core-blocks/src/index.ts | 8 ++ .../src/styles/_form-controls.scss | 39 +++++- .../core-theming/src/styles/_theme-dark.scss | 18 ++- .../core-theming/src/styles/_theme-light.scss | 12 +- .../core-theming/src/styles/_variables.scss | 7 +- .../ObjectInfoTab/ObjectProperties.tsx | 38 +++--- .../ObjectInfoTab/ObjectProperty.tsx | 52 ++++---- 16 files changed, 502 insertions(+), 63 deletions(-) create mode 100644 webapp/packages/core-blocks/src/Containers/BASE_CONTAINERS_STYLES.ts create mode 100644 webapp/packages/core-blocks/src/Containers/ColoredContainer.tsx create mode 100644 webapp/packages/core-blocks/src/Containers/Container.tsx create mode 100644 webapp/packages/core-blocks/src/Containers/Group.tsx create mode 100644 webapp/packages/core-blocks/src/Containers/GroupContainer.tsx create mode 100644 webapp/packages/core-blocks/src/Containers/GroupTitle.tsx create mode 100644 webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckboxNew.tsx create mode 100644 webapp/packages/core-blocks/src/FormControls/InputFieldNew.tsx create mode 100644 webapp/packages/core-blocks/src/FormControls/baseFormControlStylesNew.ts 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..ffbdf12dbd --- /dev/null +++ b/webapp/packages/core-blocks/src/Containers/BASE_CONTAINERS_STYLES.ts @@ -0,0 +1,77 @@ +/* + * 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 interface ISize { + small?: boolean; + medium?: boolean; + large?: boolean; +} + +export const BASE_CONTAINERS_STYLES = composes( + css` + GroupContainer { + composes: theme-background-surface from global; + } + ColoredContainer { + composes: theme-background-secondary from global; + } + `, + css` + Container, ColoredContainer { + display: flex; + flex-direction: column; + overflow: auto; + &[horizontal] { + flex-direction: row; + } + &[wrap] { + flex-wrap: wrap; + } + } + + GroupContainer { + box-sizing: border-box; + margin: 10px; + padding: 10px; + border-radius: 4px; + } + + Group { + display: grid; + grid-template-columns: minmax(min-content, 1fr); + grid-auto-rows: max-content; + &[horizontal] { + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + } + } + + GroupTitle { + composes: theme-typography--body1 from global; + font-weight: 400; + padding-left: 12px; + text-transform: uppercase; + opacity: 0.9; + } + + Container, ColoredContainer, GroupContainer, Group { + 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..d57b285d8c --- /dev/null +++ b/webapp/packages/core-blocks/src/Containers/ColoredContainer.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 { ISize } from './BASE_CONTAINERS_STYLES'; + +interface Props extends ISize { + className?: string; + horizontal?: boolean; + wrap?: 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..ce58ffd2cb --- /dev/null +++ b/webapp/packages/core-blocks/src/Containers/Container.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 { ISize } from './BASE_CONTAINERS_STYLES'; + +interface Props extends ISize { + className?: string; + horizontal?: boolean; + wrap?: boolean; +} + +export const Container: React.FC = function Container({ 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..4270ae9f62 --- /dev/null +++ b/webapp/packages/core-blocks/src/Containers/Group.tsx @@ -0,0 +1,22 @@ +/* + * 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 { ISize } from './BASE_CONTAINERS_STYLES'; + +interface Props extends ISize { + className?: string; + horizontal?: boolean; +} + +export const Group: React.FC = function Group({ children, className }) { + return ( +
+ {children} +
+ ); +}; diff --git a/webapp/packages/core-blocks/src/Containers/GroupContainer.tsx b/webapp/packages/core-blocks/src/Containers/GroupContainer.tsx new file mode 100644 index 0000000000..91db06b3e2 --- /dev/null +++ b/webapp/packages/core-blocks/src/Containers/GroupContainer.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 { ISize } from './BASE_CONTAINERS_STYLES'; + +interface Props extends ISize { + className?: string; +} + +export const GroupContainer: React.FC = function GroupContainer({ 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/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/InputFieldNew.tsx b/webapp/packages/core-blocks/src/FormControls/InputFieldNew.tsx new file mode 100644 index 0000000000..f296dce1d4 --- /dev/null +++ b/webapp/packages/core-blocks/src/FormControls/InputFieldNew.tsx @@ -0,0 +1,112 @@ +/* + * 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 } from 'reshadow'; +import { css } from 'reshadow'; + +import { useStyles } from '@cloudbeaver/core-theming'; + +import type { ISize } from '../Containers/BASE_CONTAINERS_STYLES'; +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'> & ISize & { + description?: string; + mod?: 'surface'; + full?: boolean; +}; + +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, + full, + 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/baseFormControlStylesNew.ts b/webapp/packages/core-blocks/src/FormControls/baseFormControlStylesNew.ts new file mode 100644 index 0000000000..982086cb9c --- /dev/null +++ b/webapp/packages/core-blocks/src/FormControls/baseFormControlStylesNew.ts @@ -0,0 +1,42 @@ +/* + * 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'; + +export const baseFormControlStylesNew = css` + field { + box-sizing: border-box; + padding: 12px; + min-height: 54px; + max-width: 100%; + &[|small] { + max-width: 240px; + } + &[|medium] { + max-width: 440px; + } + &[|large] { + max-width: 640px; + } + &[|full] { + grid-column: 1/-1; + } + } + + field-label { + composes: theme-typography--body1 from global; + box-sizing: border-box; + line-height: 16px; + font-weight: 500; + } + + field-description { + composes: theme-typography--caption from global; + padding-top: 8px; + } +`; diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index ba13371b5c..a0654482a4 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -24,14 +24,22 @@ export * from './Tree/TreeNode/TreeNodeStyles'; export * from './Button'; export * from './FormControls/Checkboxes/Checkbox'; export * from './FormControls/Checkboxes/FieldCheckbox'; +export * from './FormControls/Checkboxes/FieldCheckboxNew'; export * from './FormControls/Checkboxes/CheckboxMarkup'; export * from './FormControls/FormBox'; +export * from './Containers/BASE_CONTAINERS_STYLES'; +export * from './Containers/Container'; +export * from './Containers/GroupContainer'; +export * from './Containers/Group'; +export * from './Containers/GroupTitle'; +export * from './Containers/ColoredContainer'; export * from './FormControls/FormBoxElement'; export * from './FormControls/Combobox'; export * from './FormControls/FormContext'; export * from './FormControls/FormFieldDescription'; export * from './FormControls/FormGroup'; export * from './FormControls/InputField'; +export * from './FormControls/InputFieldNew'; export * from './FormControls/InputGroup'; export * from './FormControls/Radio'; export * from './FormControls/RadioGroup'; diff --git a/webapp/packages/core-theming/src/styles/_form-controls.scss b/webapp/packages/core-theming/src/styles/_form-controls.scss index bcdb2305f9..4cf5d711ab 100644 --- a/webapp/packages/core-theming/src/styles/_form-controls.scss +++ b/webapp/packages/core-theming/src/styles/_form-controls.scss @@ -7,7 +7,7 @@ */ @mixin form-controls() { - input { + input:not([role="new"]) { @include mdc-theme-prop(color, on-secondary, false); @include mdc-theme-prop(background-color, secondary, false); box-sizing: border-box; @@ -28,12 +28,45 @@ opacity: 0.8; } - &[use|mod=surface] { + &[use|mod="surface"] { @include mdc-theme-prop(color, on-surface, false); @include mdc-theme-prop(background-color, surface, false); } } + input[role="new"] { + @include mdc-theme-prop(color, on-secondary, false); + @include mdc-theme-prop(background-color, input-background, false); + box-sizing: border-box; + padding: 9px 12px; + overflow: hidden; + text-overflow: ellipsis; + width: 100%; + border: 2px solid; + outline: none; + border-radius: 3px; + @include mdc-theme-prop(border-color, background, false); + &:focus { + @include mdc-theme-prop(border-color, primary, false); + } + + &:global([type="checkbox"]) { + height: 16px; + width: 16px; + } + + &:global([readonly]) { + @include mdc-theme-prop(border-color, input-border-readonly, false); + @include mdc-theme-prop(background-color, input-background-readonly, false); + opacity: 0.8; + } + &:global([disabled]) { + @include mdc-theme-prop(border-color, input-border-disabled, false); + @include mdc-theme-prop(background-color, input-background-disabled, false); + opacity: 0.7; + } + } + label { display: block; line-height: 15px; @@ -54,7 +87,7 @@ font-size: 14px; padding: 4px; border-radius: 3px; - border: solid 1px $input-border-color; + border: solid 1px $input-border; @include mdc-theme-prop(color, on-surface, false); @include mdc-theme-prop(background-color, surface, false); } diff --git a/webapp/packages/core-theming/src/styles/_theme-dark.scss b/webapp/packages/core-theming/src/styles/_theme-dark.scss index 7ec22d79ed..78f4e50942 100644 --- a/webapp/packages/core-theming/src/styles/_theme-dark.scss +++ b/webapp/packages/core-theming/src/styles/_theme-dark.scss @@ -18,7 +18,13 @@ $mdc-theme-error: #e94948; $color-positive: #01cca3; $color-on-positive: #fff; $color-negative: #fd1d48; -$input-border-color: #585958; + +$input-border: #585958; +$input-border-disabled: darken($input-border, 10%); +$input-border-readonly: darken($input-border, 5%); +$input-background: $mdc-theme-secondary; +$input-background-disabled: $input-background; +$input-background-readonly: $input-background; $mdc-theme-text-colors: ( dark: ( @@ -26,17 +32,17 @@ $mdc-theme-text-colors: ( secondary: rgba(white, 0.7), hint: rgba(white, 0.5), disabled: rgba(white, 0.5), - icon: rgba(white, 0.5) + icon: rgba(white, 0.5), ), light: ( primary: rgba(black, 0.87), secondary: rgba(black, 0.54), hint: rgba(black, 0.38), disabled: rgba(black, 0.38), - icon: rgba(black, 0.38) - ) + icon: rgba(black, 0.38), + ), ); -$theme-name: 'dark'; +$theme-name: "dark"; -@import 'mixins'; +@import "mixins"; diff --git a/webapp/packages/core-theming/src/styles/_theme-light.scss b/webapp/packages/core-theming/src/styles/_theme-light.scss index cd6960972b..437917f4d8 100644 --- a/webapp/packages/core-theming/src/styles/_theme-light.scss +++ b/webapp/packages/core-theming/src/styles/_theme-light.scss @@ -17,8 +17,14 @@ $mdc-theme-background: #dedede; $color-positive: #01cca3; $color-on-positive: #fff; $color-negative: #fd1d48; -$input-border-color: #dedede; -$theme-name: 'light'; +$input-border: #dedede; +$input-border-disabled: transparent; +$input-border-readonly: transparent; +$input-background: transparent; +$input-background-disabled: $mdc-theme-secondary; +$input-background-readonly: $mdc-theme-secondary; -@import 'mixins'; +$theme-name: "light"; + +@import "mixins"; diff --git a/webapp/packages/core-theming/src/styles/_variables.scss b/webapp/packages/core-theming/src/styles/_variables.scss index 7206f5fe06..2677b508f9 100644 --- a/webapp/packages/core-theming/src/styles/_variables.scss +++ b/webapp/packages/core-theming/src/styles/_variables.scss @@ -10,7 +10,12 @@ $mdc-theme-extra-property-values: ( positive: $color-positive, on-positive: $color-on-positive, negative: $color-negative, - input-border-color: $input-border-color, + input-border: $input-border, + input-border-readonly: $input-border-readonly, + input-border-disabled: $input-border-disabled, + input-background: $input-background, + input-background-disabled: $input-background-disabled, + input-background-readonly: $input-background-readonly, ); $mdc-theme-property-values: map-merge($mdc-theme-property-values, $mdc-theme-extra-property-values); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperties.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperties.tsx index 8c8b01734f..285e43303a 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperties.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperties.tsx @@ -8,28 +8,16 @@ import { observer } from 'mobx-react-lite'; import type { PropsWithChildren } from 'react'; -import styled, { css } from 'reshadow'; +import styled from 'reshadow'; import { useDatabaseObjectInfo } from '@cloudbeaver/core-app'; -import { Loader, TextPlaceholder } from '@cloudbeaver/core-blocks'; +import { ColoredContainer, Group, GroupContainer, Loader, TextPlaceholder } from '@cloudbeaver/core-blocks'; +import { BASE_CONTAINERS_STYLES } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; import { useStyles } from '@cloudbeaver/core-theming'; import { ObjectProperty } from './ObjectProperty'; -const tabStyles = css` - properties { - display: flex; - flex: 1; - } - container { - display: flex; - flex-direction: column; - box-sizing: border-box; - padding-top: 8px; - } -`; - type ObjectPropertiesProps = PropsWithChildren<{ objectId: string; }>; @@ -38,8 +26,8 @@ export const ObjectProperties = observer(function ObjectProperties({ objectId, }: ObjectPropertiesProps) { const translate = useTranslate(); - const style = useStyles(tabStyles); const { dbObject, isLoading } = useDatabaseObjectInfo(objectId); + const styles = useStyles(BASE_CONTAINERS_STYLES); if (!dbObject?.properties && isLoading) { return ; @@ -49,13 +37,15 @@ export const ObjectProperties = observer(function ObjectProperties({ return {translate('plugin_object_viewer_table_no_items')}; } - return styled(style)( - - - {dbObject.properties.map(v => ( - - ))} - - + return styled(styles)( + + + + {dbObject.properties.map(v => ( + + ))} + + + ); }); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperty.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperty.tsx index 9034d8aeae..905398b18a 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperty.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperty.tsx @@ -7,42 +7,42 @@ */ import { observer } from 'mobx-react-lite'; -import styled, { css, use } from 'reshadow'; -import { - ObjectPropertyCheckbox, - ObjectPropertyInput, - ObjectPropertyProps -} from '../formControls'; -import { filterProperty, matchType } from '../helpers'; +import { FieldCheckboxNew, InputFieldNew } from '@cloudbeaver/core-blocks'; -const propertyStyles = css` - property { - composes: theme-typography--body2 from global; - display: flex; - box-sizing: border-box; - padding: 8px 8px; - width: 600px; - } -`; +import type { ObjectPropertyProps } from '../formControls'; +import { additionalProps, filterProperty, getValue, matchType } from '../helpers'; export const ObjectProperty = observer(function ObjectProperty({ objectProperty, - className, }: ObjectPropertyProps) { if (!objectProperty || !filterProperty(objectProperty)) { return null; } - return styled(propertyStyles)( - + return ( + <> {matchType(objectProperty.dataType) === 'checkbox' - ? - : } - + ? ( + + {objectProperty.displayName} + + ) + : ( + {objectProperty.displayName} + + )} + ); }); From 66cceac0b8eb3bcc5e561bd1a9807fc68036ef25 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Wed, 24 Feb 2021 15:45:14 +0300 Subject: [PATCH 2/4] feat(core-theming): CB-724 remove GroupContainer --- .../src/Containers/BASE_CONTAINERS_STYLES.ts | 8 ++++---- .../src/Containers/{GroupContainer.tsx => Grid.tsx} | 3 ++- webapp/packages/core-blocks/src/Containers/Group.tsx | 1 - webapp/packages/core-blocks/src/index.ts | 2 +- .../ObjectInfoTab/ObjectProperties.tsx | 10 +++++----- .../ObjectInfoTab/ObjectProperty.tsx | 1 - 6 files changed, 12 insertions(+), 13 deletions(-) rename webapp/packages/core-blocks/src/Containers/{GroupContainer.tsx => Grid.tsx} (80%) diff --git a/webapp/packages/core-blocks/src/Containers/BASE_CONTAINERS_STYLES.ts b/webapp/packages/core-blocks/src/Containers/BASE_CONTAINERS_STYLES.ts index ffbdf12dbd..71a410d90c 100644 --- a/webapp/packages/core-blocks/src/Containers/BASE_CONTAINERS_STYLES.ts +++ b/webapp/packages/core-blocks/src/Containers/BASE_CONTAINERS_STYLES.ts @@ -18,7 +18,7 @@ export interface ISize { export const BASE_CONTAINERS_STYLES = composes( css` - GroupContainer { + Group { composes: theme-background-surface from global; } ColoredContainer { @@ -38,14 +38,14 @@ export const BASE_CONTAINERS_STYLES = composes( } } - GroupContainer { + Group { box-sizing: border-box; margin: 10px; padding: 10px; border-radius: 4px; } - Group { + Grid { display: grid; grid-template-columns: minmax(min-content, 1fr); grid-auto-rows: max-content; @@ -62,7 +62,7 @@ export const BASE_CONTAINERS_STYLES = composes( opacity: 0.9; } - Container, ColoredContainer, GroupContainer, Group { + Container, ColoredContainer, Group, Grid { flex-grow: 1; &[small] { max-width: 250px; diff --git a/webapp/packages/core-blocks/src/Containers/GroupContainer.tsx b/webapp/packages/core-blocks/src/Containers/Grid.tsx similarity index 80% rename from webapp/packages/core-blocks/src/Containers/GroupContainer.tsx rename to webapp/packages/core-blocks/src/Containers/Grid.tsx index 91db06b3e2..d5d65dac1c 100644 --- a/webapp/packages/core-blocks/src/Containers/GroupContainer.tsx +++ b/webapp/packages/core-blocks/src/Containers/Grid.tsx @@ -10,9 +10,10 @@ import type { ISize } from './BASE_CONTAINERS_STYLES'; interface Props extends ISize { className?: string; + horizontal?: boolean; } -export const GroupContainer: React.FC = function GroupContainer({ children, className }) { +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 index 4270ae9f62..f3f52e51dc 100644 --- a/webapp/packages/core-blocks/src/Containers/Group.tsx +++ b/webapp/packages/core-blocks/src/Containers/Group.tsx @@ -10,7 +10,6 @@ import type { ISize } from './BASE_CONTAINERS_STYLES'; interface Props extends ISize { className?: string; - horizontal?: boolean; } export const Group: React.FC = function Group({ children, className }) { diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index a0654482a4..6c06caf269 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -29,7 +29,7 @@ export * from './FormControls/Checkboxes/CheckboxMarkup'; export * from './FormControls/FormBox'; export * from './Containers/BASE_CONTAINERS_STYLES'; export * from './Containers/Container'; -export * from './Containers/GroupContainer'; +export * from './Containers/Grid'; export * from './Containers/Group'; export * from './Containers/GroupTitle'; export * from './Containers/ColoredContainer'; diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperties.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperties.tsx index 285e43303a..e325abc4d2 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperties.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperties.tsx @@ -11,7 +11,7 @@ import type { PropsWithChildren } from 'react'; import styled from 'reshadow'; import { useDatabaseObjectInfo } from '@cloudbeaver/core-app'; -import { ColoredContainer, Group, GroupContainer, Loader, TextPlaceholder } from '@cloudbeaver/core-blocks'; +import { ColoredContainer, Grid, Group, Loader, TextPlaceholder } from '@cloudbeaver/core-blocks'; import { BASE_CONTAINERS_STYLES } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; import { useStyles } from '@cloudbeaver/core-theming'; @@ -39,13 +39,13 @@ export const ObjectProperties = observer(function ObjectProperties({ return styled(styles)( - - + + {dbObject.properties.map(v => ( ))} - - + + ); }); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperty.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperty.tsx index 905398b18a..9ef82455cd 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperty.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperty.tsx @@ -37,7 +37,6 @@ export const ObjectProperty = observer(function ObjectProperty({ {objectProperty.displayName} From b79a726f30411eff0c17c53731605d3f1a6da3d4 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Thu, 25 Feb 2021 13:15:47 +0300 Subject: [PATCH 3/4] feat(core-theming): CB-724 add new form controls --- .../src/Containers/BASE_CONTAINERS_STYLES.ts | 9 +- .../src/Containers/ColoredContainer.tsx | 1 + .../core-blocks/src/Containers/Container.tsx | 1 + .../core-blocks/src/Containers/Grid.tsx | 1 + .../src/FormControls/ComboboxNew.tsx | 255 ++++++++++++++++++ .../src/FormControls/SwitchNew.tsx | 168 ++++++++++++ .../src/FormControls/TextareaNew.tsx | 96 +++++++ .../FormControls/baseFormControlStylesNew.ts | 6 +- .../ObjectPropertyInfoFormNew.tsx | 182 +++++++++++++ webapp/packages/core-blocks/src/index.ts | 5 + .../Options/ParametersFormNew.tsx | 76 ++++++ .../src/styles/_form-controls.scss | 7 +- .../ObjectInfoTab/ObjectProperties.tsx | 2 +- 13 files changed, 802 insertions(+), 7 deletions(-) create mode 100644 webapp/packages/core-blocks/src/FormControls/ComboboxNew.tsx create mode 100644 webapp/packages/core-blocks/src/FormControls/SwitchNew.tsx create mode 100644 webapp/packages/core-blocks/src/FormControls/TextareaNew.tsx create mode 100644 webapp/packages/core-blocks/src/ObjectPropertyInfoForm/ObjectPropertyInfoFormNew.tsx create mode 100644 webapp/packages/core-connections/src/ConnectionForm/Options/ParametersFormNew.tsx diff --git a/webapp/packages/core-blocks/src/Containers/BASE_CONTAINERS_STYLES.ts b/webapp/packages/core-blocks/src/Containers/BASE_CONTAINERS_STYLES.ts index 71a410d90c..c624d9fbf7 100644 --- a/webapp/packages/core-blocks/src/Containers/BASE_CONTAINERS_STYLES.ts +++ b/webapp/packages/core-blocks/src/Containers/BASE_CONTAINERS_STYLES.ts @@ -29,13 +29,15 @@ export const BASE_CONTAINERS_STYLES = composes( Container, ColoredContainer { display: flex; flex-direction: column; - overflow: auto; &[horizontal] { flex-direction: row; } &[wrap] { flex-wrap: wrap; } + &[overflow] { + overflow: auto; + } } Group { @@ -50,7 +52,10 @@ export const BASE_CONTAINERS_STYLES = composes( grid-template-columns: minmax(min-content, 1fr); grid-auto-rows: max-content; &[horizontal] { - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + } + &[center] { + margin: 0 auto; } } diff --git a/webapp/packages/core-blocks/src/Containers/ColoredContainer.tsx b/webapp/packages/core-blocks/src/Containers/ColoredContainer.tsx index d57b285d8c..87e3cb5bd1 100644 --- a/webapp/packages/core-blocks/src/Containers/ColoredContainer.tsx +++ b/webapp/packages/core-blocks/src/Containers/ColoredContainer.tsx @@ -12,6 +12,7 @@ interface Props extends ISize { className?: string; horizontal?: boolean; wrap?: boolean; + overflow?: boolean; } export const ColoredContainer: React.FC = function ColoredContainer({ children, className }) { diff --git a/webapp/packages/core-blocks/src/Containers/Container.tsx b/webapp/packages/core-blocks/src/Containers/Container.tsx index ce58ffd2cb..981cd3ba60 100644 --- a/webapp/packages/core-blocks/src/Containers/Container.tsx +++ b/webapp/packages/core-blocks/src/Containers/Container.tsx @@ -12,6 +12,7 @@ interface Props extends ISize { className?: string; horizontal?: boolean; wrap?: boolean; + overflow?: boolean; } export const Container: React.FC = function Container({ children, className }) { diff --git a/webapp/packages/core-blocks/src/Containers/Grid.tsx b/webapp/packages/core-blocks/src/Containers/Grid.tsx index d5d65dac1c..953222e512 100644 --- a/webapp/packages/core-blocks/src/Containers/Grid.tsx +++ b/webapp/packages/core-blocks/src/Containers/Grid.tsx @@ -11,6 +11,7 @@ import type { ISize } from './BASE_CONTAINERS_STYLES'; interface Props extends ISize { className?: string; horizontal?: boolean; + center?: boolean; } export const Grid: React.FC = function Grid({ children, className }) { 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/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..ea10f638b7 --- /dev/null +++ b/webapp/packages/core-blocks/src/FormControls/TextareaNew.tsx @@ -0,0 +1,96 @@ +/* + * 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 { ISize } from '../Containers/BASE_CONTAINERS_STYLES'; +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'> & ISize & { + mod?: 'surface'; + long?: boolean; +}; + +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, + mod, + long, + 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} +