mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
feat(core-blocks): CB-872 metadata editor properies groups
This commit is contained in:
@@ -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);
|
||||
|
||||
+7
-7
@@ -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'];
|
||||
+86
@@ -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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
+36
-67
@@ -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();
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
+28
-9
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user