diff --git a/webapp/common-react/@dbeaver/ui-kit/src/Combobox/Combobox.css b/webapp/common-react/@dbeaver/ui-kit/src/Combobox/Combobox.css index 1bc1431eaa..c1cfff460d 100644 --- a/webapp/common-react/@dbeaver/ui-kit/src/Combobox/Combobox.css +++ b/webapp/common-react/@dbeaver/ui-kit/src/Combobox/Combobox.css @@ -1,4 +1,5 @@ @import './_base.css'; +@import './_sizes.css'; @layer base { .dbv-kit-combobox { @@ -96,4 +97,25 @@ gap: 0.5rem; padding: 0.5rem; } + + /* ---------------------------- */ + /* Sizes */ + + .dbv-kit-combobox--small { + --dbv-kit-combobox-height: var(--dbv-kit-combobox-small-height); + --dbv-kit-combobox-padding: var(--dbv-kit-combobox-small-padding); + --dbv-kit-combobox-font-size: var(--dbv-kit-combobox-small-font-size); + } + + .dbv-kit-combobox--large { + --dbv-kit-combobox-height: var(--dbv-kit-combobox-large-height); + --dbv-kit-combobox-padding: var(--dbv-kit-combobox-large-padding); + --dbv-kit-combobox-font-size: var(--dbv-kit-combobox-large-font-size); + } + + .dbv-kit-combobox--xlarge { + --dbv-kit-combobox-height: var(--dbv-kit-combobox-xlarge-height); + --dbv-kit-combobox-padding: var(--dbv-kit-combobox-xlarge-padding); + --dbv-kit-combobox-font-size: var(--dbv-kit-combobox-xlarge-font-size); + } } diff --git a/webapp/common-react/@dbeaver/ui-kit/src/Combobox/Combobox.tsx b/webapp/common-react/@dbeaver/ui-kit/src/Combobox/Combobox.tsx index 5dd220498c..eebc10c90e 100644 --- a/webapp/common-react/@dbeaver/ui-kit/src/Combobox/Combobox.tsx +++ b/webapp/common-react/@dbeaver/ui-kit/src/Combobox/Combobox.tsx @@ -25,8 +25,12 @@ import { import clsx from 'clsx'; import './Combobox.css'; -export function ComboboxInput(props: AriaComboboxProps) { - return ; +export interface ComboboxProps extends Omit { + size?: 'small' | 'medium' | 'large' | 'xlarge'; +} + +export function ComboboxInput({size, ...props}: ComboboxProps) { + return ; } export interface ComboboxPopoverProps extends AriaComboboxPopoverProps { diff --git a/webapp/common-react/@dbeaver/ui-kit/src/Combobox/_sizes.css b/webapp/common-react/@dbeaver/ui-kit/src/Combobox/_sizes.css new file mode 100644 index 0000000000..08060c7164 --- /dev/null +++ b/webapp/common-react/@dbeaver/ui-kit/src/Combobox/_sizes.css @@ -0,0 +1,23 @@ +@layer base { + :root { + /* Small combobox */ + --dbv-kit-combobox-small-padding: calc(var(--dbv-kit-combobox-padding) * 0.5); + --dbv-kit-combobox-small-font-size: calc(var(--dbv-kit-combobox-font-size) * 0.875); + --dbv-kit-combobox-small-height: var(--dbv-kit-control-height-small); + + /* Medium combobox */ + --dbv-kit-combobox-medium-padding: var(--dbv-kit-combobox-padding); + --dbv-kit-combobox-medium-font-size: var(--dbv-kit-combobox-font-size); + --dbv-kit-combobox-medium-height: var(--dbv-kit-control-height-medium); + + /* Large combobox */ + --dbv-kit-combobox-large-padding: calc(var(--dbv-kit-combobox-padding) * 1.25); + --dbv-kit-combobox-large-font-size: calc(var(--dbv-kit-combobox-font-size) * 1.125); + --dbv-kit-combobox-large-height: var(--dbv-kit-control-height-large); + + /* Extra large combobox */ + --dbv-kit-combobox-xlarge-padding: calc(var(--dbv-kit-combobox-padding) * 1.5); + --dbv-kit-combobox-xlarge-font-size: calc(var(--dbv-kit-combobox-font-size) * 1.25); + --dbv-kit-combobox-xlarge-height: var(--dbv-kit-control-height-xlarge); + } +} diff --git a/webapp/packages/core-blocks/src/FormControls/Combobox.tsx b/webapp/packages/core-blocks/src/FormControls/Combobox.tsx index 4a3c6d44be..27039cec11 100644 --- a/webapp/packages/core-blocks/src/FormControls/Combobox.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Combobox.tsx @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { useCallback, useContext, useEffect, useId, useState } from 'react'; +import { useCallback, useContext, useId, useState } from 'react'; import { ComboboxInput, ComboboxItem, clsx, Spinner, ComboboxPopover, ComboboxDisclosure, ComboboxProvider } from '@dbeaver/ui-kit'; import { filterLayoutFakeProps, getLayoutProps } from '../Containers/filterLayoutFakeProps.js'; @@ -21,7 +21,7 @@ import './Combobox.css'; export type ComboboxBaseProps = Omit< React.InputHTMLAttributes, - 'onChange' | 'onSelect' | 'name' | 'value' | 'defaultValue' + 'onChange' | 'onSelect' | 'name' | 'value' | 'defaultValue' | 'size' > & ILayoutSizeProps & { items: TValue[]; @@ -35,6 +35,8 @@ export type ComboboxBaseProps = Omit< isDisabled?: (item: TValue) => boolean; inline?: boolean; allowCustomValue?: boolean; + size?: 'small' | 'medium' | 'large'; + onChange?: (value: string | null) => void; }; type ControlledProps = ComboboxBaseProps & { @@ -70,13 +72,15 @@ export const Combobox: ComboboxType = observer(function Combobox({ disabled, inline, description, + allowCustomValue = false, keySelector = v => v, valueSelector = v => v, iconSelector, titleSelector, isDisabled, + size, onSelect, - allowCustomValue = false, + onChange, ...rest }: ControlledProps | ObjectProps) { const layoutProps = getLayoutProps(rest); @@ -91,21 +95,11 @@ export const Combobox: ComboboxType = observer(function Combobox({ } const selectedItem = items.find((item, index) => keySelector(item, index) === selectedKey); - const [inputValue, setInputValue] = useState(null); + const [internalInputValue, setInternalInputValue] = useState(null); - const selectedValue = (function () { - if (selectedItem) { - return valueSelector(selectedItem); - } - - if (allowCustomValue) { - return String(selectedKey); - } - - return ''; - })(); + const inputValue = allowCustomValue ? controlledValue : internalInputValue; + const selectedValue = selectedItem ? valueSelector(selectedItem) : ''; const displayValue = inputValue ?? selectedValue; - const isSelectedExistingItem = items.some((item, index) => keySelector(item, index) === selectedKey); const filteredItems = items .map((item, index) => { @@ -116,7 +110,7 @@ export const Combobox: ComboboxType = observer(function Combobox({ const itemDisabled = isDisabled?.(item); const isVisible = - !displayValue || isSelectedExistingItem || !displayValue.trim() || itemValue.toLowerCase().includes(displayValue.trim().toLowerCase()); + allowCustomValue || inputValue === null || !inputValue.trim() || itemValue.toLowerCase().includes(inputValue.trim().toLowerCase()); return { item, @@ -133,10 +127,8 @@ export const Combobox: ComboboxType = observer(function Combobox({ const handleSelect = useCallback( (selectedValue: string | string[]) => { - const isItemExists = !!items.find((item, idx) => keySelector(item, idx) === selectedValue); - const isSameSelected = selectedValue === selectedKey; - - if (!allowCustomValue && (!isItemExists || isSameSelected)) { + const item = items.find((item, idx) => keySelector(item, idx) === selectedValue); + if (!item || selectedValue === selectedKey) { return; } @@ -150,7 +142,7 @@ export const Combobox: ComboboxType = observer(function Combobox({ context.change(selectedValue as string, name); } }, - [items, selectedKey, state, onSelect, allowCustomValue, context, keySelector, name], + [items, selectedKey, state, onSelect, context, keySelector, name], ); const icon = selectedItem && iconSelector?.(selectedItem); @@ -166,25 +158,26 @@ export const Combobox: ComboboxType = observer(function Combobox({ } } - function onChange(event: React.ChangeEvent) { - if (allowCustomValue) { - handleSelect(event.target.value); + function setInputValue(value: string | null) { + setInternalInputValue(value); + onChange?.(value); + } + + function handleBlur() { + if (!allowCustomValue) { + setInputValue(null); } } - function onBlur() { + function handleSetValueOnClick() { if (allowCustomValue) { - handleSelect(displayValue); - return; + return true; } - setInputValue(null); + return false; } - // Reset transient input when external selection changes - useEffect(() => { - setInputValue(null); - }, [selectedKey]); + const displayPopover = !allowCustomValue || items.length > 0; return ( @@ -215,51 +208,49 @@ export const Combobox: ComboboxType = observer(function Combobox({ className={clsx('theme-typography--caption tw:tracking-normal!', icon || loading ? 'tw:pl-8!' : '', 'tw:pr-6!')} title={title} id={inputId} - onBlur={onBlur} - onChange={onChange} + size={size} + onBlur={handleBlur} {...rest} /> {loading ? ( ) : ( - + displayPopover && ( + + ) )} {icon && {typeof icon === 'string' ? : icon}} - - {filteredItems.length > 0 ? ( - filteredItems.map(({ itemKey, itemValue, itemTitle, itemIcon, itemDisabled }) => ( - { - setInputValue(null); - return false; - }} - className={clsx({ - 'tw:cursor-pointer': !itemDisabled, - 'tw:cursor-not-allowed': itemDisabled, - })} - onClick={() => { - handleSelect(itemKey); - }} - > - {iconSelector && ( - - {itemIcon && typeof itemIcon === 'string' ? : itemIcon} - - )} - {itemValue} - - )) - ) : ( - {translate('combobox_no_results_placeholder')} - )} - + {displayPopover && ( + + {filteredItems.length > 0 ? ( + filteredItems.map(({ itemKey, itemValue, itemTitle, itemIcon, itemDisabled }) => ( + + {iconSelector && ( + + {itemIcon && typeof itemIcon === 'string' ? : itemIcon} + + )} + {itemValue} + + )) + ) : ( + {translate('combobox_no_results_placeholder')} + )} + + )} {description && {description}} diff --git a/webapp/packages/core-blocks/src/PropertiesTable/PropertiesTable.module.css b/webapp/packages/core-blocks/src/PropertiesTable/PropertiesTable.module.css index 732d4df26b..13929c8905 100644 --- a/webapp/packages/core-blocks/src/PropertiesTable/PropertiesTable.module.css +++ b/webapp/packages/core-blocks/src/PropertiesTable/PropertiesTable.module.css @@ -42,7 +42,7 @@ .propertiesHeaderAdd { flex: 0 0 auto; - padding: 0px 28px; + padding: 0px 24px; } .propertiesList { diff --git a/webapp/packages/core-blocks/src/PropertiesTable/PropertyItem.module.css b/webapp/packages/core-blocks/src/PropertiesTable/PropertyItem.module.css index 6c0f83a9fa..3934330022 100644 --- a/webapp/packages/core-blocks/src/PropertiesTable/PropertyItem.module.css +++ b/webapp/packages/core-blocks/src/PropertiesTable/PropertyItem.module.css @@ -5,8 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -.container, -.button { +.container { composes: theme-ripple from global; } @@ -14,6 +13,7 @@ box-sizing: border-box; display: inline-flex; padding: 0px 1px; + min-height: 36px; } .name, @@ -25,11 +25,6 @@ box-sizing: border-box; flex: 1; padding: 4px 0; - - & .shadowInput { - height: 24px; - padding: 0 36px 0 12px; - } } .value, @@ -42,54 +37,9 @@ width: 276px; } -.remove { - width: 24px; - height: 24px; - position: relative; - flex: 0 0 auto; - align-items: center; - display: flex; - opacity: 0; -} - -.remove { - margin: 0 2px; -} - -.container:hover .remove { - opacity: 1; -} - -.shadowInput { - composes: theme-background-surface from global; -} - -.name .shadowInput, -.value .shadowInput { - box-sizing: border-box; - font: inherit; - color: inherit; - width: 100%; - outline: none; - - &:global([readonly]), - &:not(:focus):not([data-focus='true']) { - background: transparent !important; - border: solid 2px transparent !important; - } -} - -.icon, -.iconOrImage { - height: 16px; - display: block; -} - -.button { - background: transparent; - outline: none; - padding: 4px; - cursor: pointer; +.actions { + margin: 0 4px; + min-width: 28px; } .error { diff --git a/webapp/packages/core-blocks/src/PropertiesTable/PropertyItem.tsx b/webapp/packages/core-blocks/src/PropertiesTable/PropertyItem.tsx index 33c9462afc..44e653fd56 100644 --- a/webapp/packages/core-blocks/src/PropertiesTable/PropertyItem.tsx +++ b/webapp/packages/core-blocks/src/PropertiesTable/PropertyItem.tsx @@ -7,10 +7,8 @@ */ import { observer } from 'mobx-react-lite'; import { useCallback, useLayoutEffect, useRef } from 'react'; +import { Input } from '@dbeaver/ui-kit'; -import { ShadowInput } from '../FormControls/ShadowInput.js'; -import { Icon } from '../Icon.js'; -import { IconOrImage } from '../IconOrImage.js'; import { useTranslate } from '../localization/useTranslate.js'; import { s } from '../s.js'; import { useS } from '../useS.js'; @@ -18,6 +16,7 @@ import type { IProperty } from './IProperty.js'; import classes from './PropertyItem.module.css'; import { Combobox } from '../FormControls/Combobox.js'; import { isNotNullDefined } from '@dbeaver/js-helpers'; +import { ActionIconButton } from '../ActionIconButton.js'; interface Props { property: IProperty; @@ -38,7 +37,7 @@ export const PropertyItem = observer(function PropertyItem({ property, va const keyInputRef = useRef(null); const handleKeyChange = useCallback((key: string) => onNameChange(property.id, key), [property]); - const handleValueChange = useCallback((value: string) => onValueChange(property.id, value), [property]); + const handleValueChange = useCallback((value: string | null) => onValueChange(property.id, value), [property]); const handleRemove = useCallback(() => onRemove(property.id), [property]); function handleRevert() { onValueChange(property.id, property.defaultValue ?? null); @@ -56,46 +55,52 @@ export const PropertyItem = observer(function PropertyItem({ property, va return ( - - {property.displayName || property.key} - + size="small" + onChange={e => handleKeyChange(e.target.value)} + /> - {!readOnly && property.validValues && property.validValues.length > 0 && ( - - )} - - - - + + + {edited && !isDeletable && ( + + )} + {isDeletable && ( + + )} - {isDeletable && ( - - - - - - )} );