Merge pull request #297 from dbeaver/feat/form-new-design

Feat/form new design
This commit is contained in:
Alexey
2021-02-25 19:09:35 +03:00
committed by GitHub
22 changed files with 1293 additions and 62 deletions
@@ -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;
}
}
`);
@@ -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<Props> = function ColoredContainer({ children, className }) {
return (
<div className={className}>
{children}
</div>
);
};
@@ -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<Props> = function Container({ children, className }) {
return (
<div className={className}>
{children}
</div>
);
};
@@ -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<Props> = function Grid({ children, className }) {
return (
<div className={className}>
{children}
</div>
);
};
@@ -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<Props> = function Group({ children, className }) {
return (
<div className={className}>
{children}
</div>
);
};
@@ -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<Props> = function GroupTitle({ children, className }) {
return (
<h2 className={className}>
{children}
</h2>
);
};
@@ -0,0 +1,9 @@
export interface ILayoutSizeProps {
small?: boolean;
medium?: boolean;
large?: boolean;
}
export interface IGridItemsLayoutProps {
itemMax?: boolean;
}
@@ -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<any, any>) {
const styles = useStyles(baseFormControlStylesNew, fieldCheckboxStyles);
if (autoHide && !isControlPresented(rest.name, rest.state)) {
return null;
}
return styled(styles)(
<field className={className} as="div">
<Checkbox
{...rest}
checked={checkedControlled}
/>
<field-label as="div">{children}</field-label>
</field>
);
};
@@ -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<TKey, TValue> = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'onSelect' | 'name' | 'value'> & {
propertyName?: string;
items: TValue[];
searchable?: boolean;
keySelector: (item: TValue) => TKey;
valueSelector: (item: TValue) => string;
onSwitch?: (state: boolean) => void;
};
type ControlledProps<TKey, TValue> = BaseProps<TKey, TValue> & {
name?: string;
value?: string;
onSelect?: (value: TKey, name: string | undefined, prev: TKey) => void;
onChange?: (value: string, name: string | undefined) => any;
state?: never;
};
type ObjectProps<TValue, TKey extends keyof TState, TState> = BaseProps<TState[TKey], TValue> & {
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 {
<TKey, TValue>(props: ControlledProps<TKey, TValue>): JSX.Element;
<TValue, TKey extends keyof TState, TState>(props: ObjectProps<TValue, TKey, TState>): 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<any, any> | ObjectProps<any, any, any>) {
const context = useContext(FormContext);
const ref = useRef<HTMLInputElement>(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<HTMLInputElement>) => {
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<HTMLButtonElement, 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))(
<field as="div" className={className}>
<field-label title={title} as='label'>{children}</field-label>
<input-box as="div">
<input
ref={ref}
role='new'
name={name}
title={title}
value={selectedItem ? valueSelector(selectedItem) : searchValue}
readOnly={readOnly}
onChange={handleChange}
{...rest}
/>
{(selectedItem && !readOnly && searchable) && (
<IconButton type="button" name="reject" viewBox="0 0 11 11" onClick={handleRemove} />
)}
<MenuButton {...menu} disabled={readOnly || disabled}><Icon name="arrow" viewBox="0 0 16 16" {...use({ focus: menu.visible })} /></MenuButton>
<Menu
{...menu}
aria-label={propertyName}
unstable_initialFocusRef={ref}
unstable_finalFocusRef={ref}
>
{filteredItems.map(item => (
<MenuItem key={keySelector(item)} id={keySelector(item)} type='button' {...menu} onClick={handleMenuSelect}>
{valueSelector(item)}
</MenuItem>
))}
</Menu>
</input-box>
</field>
);
});
@@ -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<React.InputHTMLAttributes<HTMLInputElement>, '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<TKey extends keyof TState, TState> = BaseProps & {
name: TKey;
state: TState;
onChange?: (value: string, name: TKey) => any;
autoHide?: boolean;
value?: never;
};
interface InputFieldType {
(props: ControlledProps): React.ReactElement<any, any> | null;
<TKey extends keyof TState, TState>(props: ObjectProps<TKey, TState>): React.ReactElement<any, any> | 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<any, any>) {
const styles = useStyles(baseFormControlStylesNew, INPUT_FIELD_STYLES);
const context = useContext(FormContext);
const handleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
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)(
<field as="div" className={className} {...use({ small, medium, large })}>
<field-label as='label'>{children} {required && '*'}</field-label>
<input
role='new'
{...rest}
name={name}
value={value ?? ''}
onChange={handleChange}
{...use({ mod })}
required={required}
/>
{description && (
<field-description as='div'>
{description}
</field-description>
)}
</field>
);
});
@@ -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<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'type' | 'value' | 'checked'> & {
mod?: Array<keyof typeof switchMod>;
label?: string;
description?: string;
};
type ControlledProps = BaseProps & {
checked?: boolean;
onChange?: (value: boolean, name?: string) => any;
state?: never;
autoHide?: never;
};
type ObjectProps<TKey extends keyof TState, TState> = BaseProps & {
name: TKey;
state: TState;
onChange?: (value: boolean, name: TKey) => any;
autoHide?: boolean;
checked?: never;
};
interface SwitchType {
(props: ControlledProps): React.ReactElement<any, any> | null;
<TKey extends keyof TState, TState>(props: ObjectProps<TKey, TState>): React.ReactElement<any, any> | null;
}
export const SwitchNew: SwitchType = observer(function SwitchNew({
name,
id,
label,
description,
state,
checked: checkedControlled,
className,
children,
onChange,
mod = [],
autoHide,
disabled,
...rest
}: ControlledProps | ObjectProps<any, any>) {
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<HTMLInputElement>) => {
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)(
<field as="div" className={className}>
<field-label as="div">{children}</field-label>
<switch-control as='div'>
<switch-control-track as='div' />
<switch-control-underlay as='div'>
<switch-control-thumb as='div' />
<switch-input
as='input'
{...rest}
type="checkbox"
id={id}
role="switch"
aria-checked={checked}
checked={checked}
disabled={disabled}
onChange={handleChange}
/>
</switch-control-underlay>
</switch-control>
<label htmlFor={id}>{label}</label>
<field-description as='div'>{description}</field-description>
</field>
);
});
@@ -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<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'onChange'> & ILayoutSizeProps & {
mod?: 'surface';
};
type ControlledProps = BaseProps & {
name?: string;
value?: string;
onChange?: (value: string, name?: string) => any;
state?: never;
};
type ObjectProps<TKey extends keyof TState, TState> = BaseProps & {
name: TKey;
state: TState;
onChange?: (value: string, name: TKey) => any;
value?: never;
};
interface TextareaType {
(props: ControlledProps): JSX.Element;
<TKey extends keyof TState, TState>(props: ObjectProps<TKey, TState>): JSX.Element;
}
export const TextareaNew: TextareaType = observer(function TextareaNew({
name,
value: controlledValue,
state,
children,
className,
small,
medium,
large,
mod,
onChange = () => {},
...rest
}: ControlledProps | ObjectProps<any, any>) {
const context = useContext(FormContext);
const handleChange = useCallback((event: React.ChangeEvent<HTMLTextAreaElement>) => {
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))(
<field as="div" className={className} {...use({ small, medium, large })}>
<field-label as='label'>{children}</field-label>
<textarea
{...rest}
role='new'
value={value ?? ''}
name={name}
onChange={handleChange}
{...use({ mod })}
/>
</field>
);
});
@@ -0,0 +1,39 @@
/*
* 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: 250px;
}
&[|medium] {
max-width: 450px;
}
&[|large] {
max-width: 650px;
}
}
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;
}
`;
@@ -0,0 +1,182 @@
/*
* 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 } from 'react';
import styled from 'reshadow';
import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
import { useStyles } from '@cloudbeaver/core-theming';
import { FieldCheckboxNew } from '../FormControls/Checkboxes/FieldCheckboxNew';
import { ComboboxNew } from '../FormControls/ComboboxNew';
import { FormFieldDescription } from '../FormControls/FormFieldDescription';
import { InputFieldNew } from '../FormControls/InputFieldNew';
import { isControlPresented } from '../FormControls/isControlPresented';
import { Link } from '../Link';
import { TextPlaceholder } from '../TextPlaceholder';
import { formStyles } from './formStyles';
const RESERVED_KEYWORDS = ['no', 'off', 'new-password'];
interface RenderFieldProps {
property: ObjectPropertyInfo;
state: Record<string, any>;
editable?: boolean;
autofillToken?: string;
disabled?: boolean;
readOnly?: boolean;
autoHide?: boolean;
showRememberTip?: boolean;
onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
}
const RenderField: React.FC<RenderFieldProps> = observer(function RenderField({
property,
state,
editable = true,
autofillToken = '',
disabled,
readOnly,
autoHide,
showRememberTip,
onFocus,
}) {
const href = property.features.includes('href');
const password = property.features.includes('password');
const checkbox = property.dataType === 'Boolean';
const combobox = property.validValues && property.validValues.length > 0;
let description: string | undefined;
if (href) {
return (
<FormFieldDescription label={property.displayName} raw>
<Link href={state[property.id!]} target='_blank' rel='noopener noreferrer'>{property.description}</Link>
</FormFieldDescription>
);
}
if (!editable) {
if (autoHide && !isControlPresented(property.id!, state)) {
return null;
}
return (
<FormFieldDescription label={property.displayName} raw>
{state[property.id!]}
</FormFieldDescription>
);
}
if (showRememberTip && password && property.value) {
description = 'Password saved';
}
if (checkbox) {
return (
<FieldCheckboxNew
name={property.id!}
state={state}
checkboxLabel={property.displayName}
title={property.description}
disabled={disabled}
mod='surface'
/>
);
}
if (combobox) {
return (
<ComboboxNew
name={property.id!}
state={state}
items={property.validValues!}
keySelector={value => value}
valueSelector={value => value}
defaultValue={property.defaultValue}
title={property.description}
disabled={disabled}
>
{property.displayName ?? ''}
</ComboboxNew>
);
}
return (
<InputFieldNew
type={password ? 'password' : 'text'}
name={property.id!}
state={state}
description={description}
disabled={disabled}
readOnly={readOnly}
autoHide={autoHide}
autoComplete={RESERVED_KEYWORDS.includes(autofillToken) ? autofillToken : `${autofillToken} ${property.id}`}
mod='surface'
onFocus={onFocus}
>
{property.displayName}
</InputFieldNew>
);
});
interface ObjectPropertyFormProps {
properties: ObjectPropertyInfo[] | undefined;
state: Record<string, string | number>;
editable?: boolean;
autofillToken?: string;
className?: string;
disabled?: boolean;
readOnly?: boolean;
autoHide?: boolean;
showRememberTip?: boolean;
onFocus?: (name: string) => void;
}
export const ObjectPropertyInfoFormNew: React.FC<ObjectPropertyFormProps> = observer(function ObjectPropertyInfoFormNew({
properties,
state,
editable = true,
autofillToken = '',
className,
disabled,
readOnly,
autoHide,
showRememberTip,
onFocus,
}) {
const style = useStyles(formStyles);
const handleFocus = useCallback((e: React.FocusEvent<HTMLInputElement>) => {
if (onFocus) {
onFocus(e.target.name);
}
}, [onFocus]);
if (!properties || properties.length === 0) {
return <TextPlaceholder>Properties empty</TextPlaceholder>;
}
return styled(style)(
<form-body as='div' className={className}>
{properties.map(property => (
<RenderField
key={property.id}
property={property}
state={state}
editable={editable}
autofillToken={autofillToken}
disabled={disabled}
readOnly={readOnly}
autoHide={autoHide}
showRememberTip={showRememberTip}
onFocus={handleFocus}
/>
))}
</form-body>
);
});
+13
View File
@@ -5,6 +5,8 @@ export * from './layout/AppLogo';
export * from './layout/TopAppBar';
export * from './Loader/Loader';
export * from './ObjectPropertyInfoForm/ObjectPropertyInfoForm';
export * from './ObjectPropertyInfoForm/ObjectPropertyInfoFormNew';
export * from './Placeholder/Placeholder';
export * from './Placeholder/PlaceholderContainer';
export * from './PropertiesTable';
@@ -24,14 +26,23 @@ 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/Grid';
export * from './Containers/Group';
export * from './Containers/GroupTitle';
export * from './Containers/ColoredContainer';
export * from './FormControls/FormBoxElement';
export * from './FormControls/Combobox';
export * from './FormControls/ComboboxNew';
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';
@@ -39,7 +50,9 @@ export * from './FormControls/RadioGroupContext';
export * from './FormControls/ShadowInput';
export * from './FormControls/SubmittingForm';
export * from './FormControls/Switch';
export * from './FormControls/SwitchNew';
export * from './FormControls/Textarea';
export * from './FormControls/TextareaNew';
export * from './ErrorMessage';
export * from './ExceptionMessage';
export * from './IconButton';
@@ -0,0 +1,70 @@
/*
* 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 styled from 'reshadow';
import { BASE_CONTAINERS_STYLES, Grid, InputFieldNew } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import type { ConnectionConfig } from '@cloudbeaver/core-sdk';
import { useStyles } from '@cloudbeaver/core-theming';
interface ParametersFormProps {
config: ConnectionConfig;
disabled?: boolean;
embedded?: boolean;
readOnly?: boolean;
}
export const ParametersFormNew = observer(function ParametersFormNew({
config,
embedded,
disabled,
readOnly,
}: ParametersFormProps) {
const translate = useTranslate();
return styled(useStyles(BASE_CONTAINERS_STYLES))(
<Grid>
{!embedded && (
<Grid horizontal>
<InputFieldNew
type="text"
name="host"
state={config}
disabled={disabled}
readOnly={readOnly}
large
required
>
{translate('customConnection_custom_host')}
</InputFieldNew>
<InputFieldNew
type="number"
name="port"
state={config}
disabled={disabled}
readOnly={readOnly}
>
{translate('customConnection_custom_port')}
</InputFieldNew>
</Grid>
)}
<InputFieldNew
type="text"
name="databaseName"
state={config}
disabled={disabled}
readOnly={readOnly}
full
>
{translate('customConnection_custom_database')}
</InputFieldNew>
</Grid>
);
});
@@ -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,50 @@
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);
}
}
textarea[role="new"] {
resize: vertical;
}
input[role="new"],
textarea[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 +92,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);
}
@@ -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";
@@ -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: $mdc-theme-surface;
$input-background-disabled: $mdc-theme-secondary;
$input-background-readonly: $mdc-theme-secondary;
@import 'mixins';
$theme-name: "light";
@import "mixins";
@@ -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);
@@ -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, 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';
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 <Loader />;
@@ -49,13 +37,15 @@ export const ObjectProperties = observer(function ObjectProperties({
return <TextPlaceholder>{translate('plugin_object_viewer_table_no_items')}</TextPlaceholder>;
}
return styled(style)(
<properties as="div">
<container as="div">
{dbObject.properties.map(v => (
<ObjectProperty key={v.id} objectProperty={v} />
))}
</container>
</properties>
return styled(styles)(
<ColoredContainer overflow>
<Group>
<Grid large>
{dbObject.properties.map(v => (
<ObjectProperty key={v.id} objectProperty={v} />
))}
</Grid>
</Group>
</ColoredContainer>
);
});
@@ -7,24 +7,11 @@
*/
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,
@@ -34,15 +21,30 @@ export const ObjectProperty = observer(function ObjectProperty({
return null;
}
return styled(propertyStyles)(
<property
className={className}
as="div"
{...use({ checkbox: matchType(objectProperty.dataType) === 'checkbox' })}
>
return (
<>
{matchType(objectProperty.dataType) === 'checkbox'
? <ObjectPropertyCheckbox objectProperty={objectProperty} />
: <ObjectPropertyInput objectProperty={objectProperty} />}
</property>
? (
<FieldCheckboxNew
className={className}
name={objectProperty.id}
value={getValue(objectProperty.value)}
readOnly
{...additionalProps(objectProperty)}
>
{objectProperty.displayName}
</FieldCheckboxNew>
)
: (
<InputFieldNew
className={className}
name={objectProperty.id}
value={getValue(objectProperty.value)}
readOnly
{...additionalProps(objectProperty)}
>{objectProperty.displayName}
</InputFieldNew>
)}
</>
);
});