feat(core-blocks): CB-872 metadata editor properies groups

This commit is contained in:
Naumov Alexey
2021-04-16 16:05:10 +03:00
parent 6addb2ec47
commit 7597159da2
15 changed files with 273 additions and 96 deletions
@@ -6,11 +6,16 @@
* you may not use this file except in compliance with the License.
*/
export type Layout = 'mixedControls';
export interface ILayoutSizeProps {
keepSize?: boolean;
tiny?: boolean;
small?: boolean;
medium?: boolean;
large?: boolean;
/**
* Used by components that want to change their size or other styles due to diffrent layout
*/
layout?: Layout;
fill?: boolean;
}
@@ -61,7 +61,14 @@ export const Checkbox: CheckboxType = observer(function Checkbox({
onChange,
...rest
}: CheckboxBaseProps & (ICheckboxControlledProps | ICheckboxObjectProps<any>)) {
const checkboxState = useCheckboxState({ value, checked, state, name, onChange });
const checkboxState = useCheckboxState({
value,
checked,
defaultChecked: rest.defaultChecked,
state,
name,
onChange,
});
if (autoHide && !isControlPresented(name, state)) {
return null;
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import styled, { css } from 'reshadow';
import styled, { css, use } from 'reshadow';
import { useStyles } from '@cloudbeaver/core-theming';
@@ -22,6 +22,14 @@ const fieldCheckboxStyles = css`
display: flex;
align-items: flex-end;
white-space: pre-wrap;
&[|layout='mixedControls'] layout {
/* same as input height */
height: 32px;
}
}
layout {
display: flex;
align-items: center;
}
field-label {
composes: theme-typography--body2 from global;
@@ -35,6 +43,7 @@ const fieldCheckboxStyles = css`
export const FieldCheckboxNew: CheckboxType = function FieldCheckboxNew({
children,
className,
layout,
...rest
}: CheckboxBaseProps & (ICheckboxControlledProps | ICheckboxObjectProps<any>)) {
const styles = useStyles(baseFormControlStylesNew, fieldCheckboxStyles);
@@ -44,9 +53,11 @@ export const FieldCheckboxNew: CheckboxType = function FieldCheckboxNew({
}
return styled(styles)(
<field className={className} as="div">
<Checkbox {...(rest as CheckboxBaseProps & ICheckboxControlledProps)} />
<field-label htmlFor={rest.value || rest.name} title={rest.title} as="label">{children}</field-label>
<field className={className} as="div" {...use({ layout })}>
<layout as='div'>
<Checkbox {...(rest as CheckboxBaseProps & ICheckboxControlledProps)} />
<field-label htmlFor={rest.value || rest.name} title={rest.title} as="label">{children}</field-label>
</layout>
</field>
);
};
@@ -103,7 +103,14 @@ export const Switch: SwitchType = observer(function Switch({
disabled,
...rest
}: IBaseProps & (ICheckboxControlledProps | ICheckboxObjectProps<any>)) {
const checkboxState = useCheckboxState({ value, checked, state, name, onChange });
const checkboxState = useCheckboxState({
value,
checked,
defaultChecked: rest.defaultChecked,
state,
name,
onChange,
});
const styles = useStyles(
baseFormControlStyles,
switchStyles,
@@ -106,7 +106,14 @@ export const SwitchNew: SwitchType = observer(function SwitchNew({
disabled,
...rest
}: IBaseProps & (ICheckboxControlledProps | ICheckboxObjectProps<any>)) {
const checkboxState = useCheckboxState({ value, checked, state, name, onChange });
const checkboxState = useCheckboxState({
value,
checked,
defaultChecked: rest.defaultChecked,
state,
name,
onChange,
});
const styles = useStyles(
baseFormControlStylesNew,
switchStyles,
@@ -11,11 +11,12 @@ import { useContext, useState } from 'react';
import { useObjectRef } from '../../useObjectRef';
import { FormContext } from '../FormContext';
export type CheckboxOnChangeEvent<T> =(value: boolean, name: T) => void;
export type CheckboxOnChangeEvent<T> = (value: boolean, name: T) => void;
export type CheckboxStateOptions<TKey extends string> = {
value: string | undefined;
checked: boolean | undefined;
defaultChecked: boolean | undefined;
} & (
{
state: undefined;
@@ -42,7 +43,7 @@ export function useCheckboxState<TKey extends string>(options: CheckboxStateOpti
let checked = optionsRef.checked ?? false;
if (state !== undefined && name !== undefined) {
if (state !== undefined && name !== undefined && name in state) {
const currentState = state[name as TKey];
if (typeof value === 'string' && Array.isArray(currentState)) {
@@ -52,6 +53,8 @@ export function useCheckboxState<TKey extends string>(options: CheckboxStateOpti
} else {
checked = !!currentState;
}
} else if (optionsRef.defaultChecked !== undefined) {
checked = optionsRef.defaultChecked;
}
return useObjectRef({
@@ -76,7 +76,15 @@ export const InputField: InputFieldType = observer(function InputField({
}
}, [state, name, context, onChange]);
const value = state ? state[name] : valueControlled;
let value: any = valueControlled;
if (state) {
if (name in state) {
value = state[name];
} else if (rest.defaultValue !== undefined) {
value = rest.defaultValue;
}
}
if (autoHide && !isControlPresented(name, state)) {
return null;
@@ -100,7 +100,15 @@ export const InputFieldNew: InputFieldType = observer(function InputFieldNew({
return null;
}
let value = state ? state[name] : valueControlled;
let value: any = valueControlled;
if (state) {
if (name in state) {
value = state[name];
} else if (rest.defaultValue !== undefined) {
value = rest.defaultValue;
}
}
if (mapState) {
value = mapState(value);
@@ -14,13 +14,13 @@ import { InputField } from '@cloudbeaver/core-blocks';
import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
import { useStyles } from '@cloudbeaver/core-theming';
import { FieldCheckbox } from '../FormControls/Checkboxes/FieldCheckbox';
import { Combobox } from '../FormControls/Combobox';
import { FormFieldDescription } from '../FormControls/FormFieldDescription';
import { FormGroup } from '../FormControls/FormGroup';
import { isControlPresented } from '../FormControls/isControlPresented';
import { Link } from '../Link';
import { TextPlaceholder } from '../TextPlaceholder';
import { FieldCheckbox } from '../../FormControls/Checkboxes/FieldCheckbox';
import { Combobox } from '../../FormControls/Combobox';
import { FormFieldDescription } from '../../FormControls/FormFieldDescription';
import { FormGroup } from '../../FormControls/FormGroup';
import { isControlPresented } from '../../FormControls/isControlPresented';
import { Link } from '../../Link';
import { TextPlaceholder } from '../../TextPlaceholder';
import { formStyles } from './formStyles';
const RESERVED_KEYWORDS = ['no', 'off', 'new-password'];
@@ -0,0 +1,86 @@
/*
* 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 React, { useCallback } from 'react';
import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
import type { ILayoutSizeProps } from '../../Containers/ILayoutSizeProps';
import { TextPlaceholder } from '../../TextPlaceholder';
import { RenderField } from './RenderField';
interface ObjectPropertyFormProps extends ILayoutSizeProps {
properties: ObjectPropertyInfo[] | undefined;
state: Record<string, string | number>;
category?: string | null;
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,
category,
editable = true,
className,
autofillToken = '',
disabled,
readOnly,
autoHide,
showRememberTip,
layout,
onFocus,
}) {
const handleFocus = useCallback((e: React.FocusEvent<HTMLInputElement>) => {
if (onFocus) {
onFocus(e.target.name);
}
}, [onFocus]);
if (!properties || properties.length === 0) {
return <TextPlaceholder>Properties empty</TextPlaceholder>;
}
let filteredProperties: ObjectPropertyInfo[] | null = null;
if (category !== undefined) {
if (category === null) {
filteredProperties = properties.filter(property => !property.category);
} else {
filteredProperties = properties.filter(property => property.category === category);
}
}
return (
<>
{(filteredProperties || properties).map(property => (
<RenderField
key={property.id}
className={className}
property={property}
state={state}
editable={editable}
autofillToken={autofillToken}
disabled={disabled}
readOnly={readOnly}
autoHide={autoHide}
showRememberTip={showRememberTip}
layout={layout}
onFocus={handleFocus}
/>
))}
</>
);
});
@@ -7,18 +7,16 @@
*/
import { observer } from 'mobx-react-lite';
import { useCallback } from 'react';
import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps';
import { FieldCheckboxNew } from '../FormControls/Checkboxes/FieldCheckboxNew';
import { ComboboxNew } from '../FormControls/ComboboxNew';
import { FormFieldDescriptionNew } from '../FormControls/FormFieldDescriptionNew';
import { InputFieldNew } from '../FormControls/InputFieldNew';
import { isControlPresented } from '../FormControls/isControlPresented';
import { Link } from '../Link';
import { TextPlaceholder } from '../TextPlaceholder';
import type { Layout } from '../../Containers/ILayoutSizeProps';
import { FieldCheckboxNew } from '../../FormControls/Checkboxes/FieldCheckboxNew';
import { ComboboxNew } from '../../FormControls/ComboboxNew';
import { FormFieldDescriptionNew } from '../../FormControls/FormFieldDescriptionNew';
import { InputFieldNew } from '../../FormControls/InputFieldNew';
import { isControlPresented } from '../../FormControls/isControlPresented';
import { Link } from '../../Link';
const RESERVED_KEYWORDS = ['no', 'off', 'new-password'];
@@ -32,10 +30,30 @@ interface RenderFieldProps {
autoHide?: boolean;
showRememberTip?: boolean;
onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
layout?: Layout;
className?: string;
}
const RenderField: React.FC<RenderFieldProps> = observer(function RenderField({
function isCheckbox(property: ObjectPropertyInfo) {
return property.dataType?.toLowerCase() === 'boolean';
}
function getDefaultValueFor(property: ObjectPropertyInfo) {
const checkbox = isCheckbox(property);
const value = property.value;
if (value === null || value === undefined) {
return checkbox ? false : '';
}
if (typeof value === 'string') {
return checkbox ? value === 'true' : value;
}
return value.displayName || value.value || JSON.stringify(value);
}
export const RenderField: React.FC<RenderFieldProps> = observer(function RenderField({
property,
state,
editable = true,
@@ -45,12 +63,14 @@ const RenderField: React.FC<RenderFieldProps> = observer(function RenderField({
autoHide,
showRememberTip,
onFocus,
layout,
className,
}) {
const href = property.features.includes('href');
const password = property.features.includes('password');
const checkbox = property.dataType === 'Boolean';
const checkbox = isCheckbox(property);
const combobox = property.validValues && property.validValues.length > 0;
const defaultValue = getDefaultValueFor(property);
let description: string | undefined;
if (href) {
@@ -81,8 +101,10 @@ const RenderField: React.FC<RenderFieldProps> = observer(function RenderField({
<FieldCheckboxNew
name={property.id!}
state={state}
defaultChecked={defaultValue}
title={property.description}
disabled={disabled || readOnly}
layout={layout}
className={className}
>
{property.displayName ?? ''}
@@ -101,6 +123,7 @@ const RenderField: React.FC<RenderFieldProps> = observer(function RenderField({
defaultValue={property.defaultValue}
title={property.description}
disabled={disabled}
layout={layout}
className={className}
>
{property.displayName ?? ''}
@@ -114,6 +137,7 @@ const RenderField: React.FC<RenderFieldProps> = observer(function RenderField({
title={property.description}
name={property.id!}
state={state}
defaultValue={defaultValue}
description={description}
disabled={disabled}
readOnly={readOnly}
@@ -121,65 +145,10 @@ const RenderField: React.FC<RenderFieldProps> = observer(function RenderField({
autoComplete={RESERVED_KEYWORDS.includes(autofillToken) ? autofillToken : `${autofillToken} ${property.id}`}
mod='surface'
className={className}
layout={layout}
onFocus={onFocus}
>
{property.displayName}
</InputFieldNew>
);
});
interface ObjectPropertyFormProps extends ILayoutSizeProps {
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,
className,
autofillToken = '',
disabled,
readOnly,
autoHide,
showRememberTip,
onFocus,
}) {
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 (
<>
{properties.map(property => (
<RenderField
key={property.id}
className={className}
property={property}
state={state}
editable={editable}
autofillToken={autofillToken}
disabled={disabled}
readOnly={readOnly}
autoHide={autoHide}
showRememberTip={showRememberTip}
onFocus={handleFocus}
/>
))}
</>
);
});
@@ -0,0 +1,46 @@
/*
* 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 { computed, IComputedValue } from 'mobx';
import { useMemo } from 'react';
import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
interface ICategoriesInfo {
categories: string[];
isUncategorizedExists: boolean;
}
export function useObjectPropertyCategories(properties: ObjectPropertyInfo[] | undefined) {
const categories: IComputedValue<ICategoriesInfo> = useMemo(() => computed(() => {
const result: ICategoriesInfo = {
categories: [],
isUncategorizedExists: false,
};
if (!properties) {
return result;
}
for (const property of properties) {
const category = property.category;
if (!category) {
result.isUncategorizedExists = true;
continue;
}
if (!result.categories.includes(category)) {
result.categories.push(category);
}
}
return result;
}), [properties]);
return categories.get();
}
+3 -2
View File
@@ -4,8 +4,9 @@ export * from './ItemList';
export * from './layout/AppLogo';
export * from './layout/TopAppBar';
export * from './Loader/Loader';
export * from './ObjectPropertyInfoForm/ObjectPropertyInfoForm';
export * from './ObjectPropertyInfoForm/ObjectPropertyInfoFormNew';
export * from './ObjectPropertyInfo/ObjectPropertyInfoForm/ObjectPropertyInfoForm';
export * from './ObjectPropertyInfo/ObjectPropertyInfoForm/ObjectPropertyInfoFormNew';
export * from './ObjectPropertyInfo/useObjectPropertyCategories';
export * from './Placeholder/Placeholder';
export * from './Placeholder/PlaceholderContainer';
@@ -11,13 +11,11 @@ import type { PropsWithChildren } from 'react';
import styled from 'reshadow';
import { useDatabaseObjectInfo } from '@cloudbeaver/core-app';
import { ColoredContainer, Group, Loader, TextPlaceholder } from '@cloudbeaver/core-blocks';
import { ColoredContainer, Loader, TextPlaceholder, useObjectPropertyCategories, GroupTitle, ObjectPropertyInfoFormNew, Group } 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';
type ObjectPropertiesProps = PropsWithChildren<{
objectId: string;
}>;
@@ -28,6 +26,7 @@ export const ObjectProperties = observer(function ObjectProperties({
const translate = useTranslate();
const { dbObject, isLoading } = useDatabaseObjectInfo(objectId);
const styles = useStyles(BASE_CONTAINERS_STYLES);
const { categories, isUncategorizedExists } = useObjectPropertyCategories(dbObject?.properties);
if (!dbObject?.properties && isLoading) {
return <Loader />;
@@ -38,12 +37,32 @@ export const ObjectProperties = observer(function ObjectProperties({
}
return styled(styles)(
<ColoredContainer overflow parent>
<Group gap large>
{dbObject.properties.map(v => (
<ObjectProperty key={v.id} objectProperty={v} small />
))}
</Group>
<ColoredContainer overflow parent gap>
{isUncategorizedExists && (
<Group gap large>
<ObjectPropertyInfoFormNew
properties={dbObject?.properties}
category={null}
state={{}}
layout="mixedControls"
small
readOnly
/>
</Group>
)}
{categories.map(category => (
<Group key={category} gap large>
<GroupTitle>{category}</GroupTitle>
<ObjectPropertyInfoFormNew
properties={dbObject?.properties}
category={category}
state={{}}
layout="mixedControls"
small
readOnly
/>
</Group>
))}
</ColoredContainer>
);
});