mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
fix: combobox and driver properties
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,12 @@ import {
|
||||
import clsx from 'clsx';
|
||||
import './Combobox.css';
|
||||
|
||||
export function ComboboxInput(props: AriaComboboxProps) {
|
||||
return <Combobox {...props} className={clsx('dbv-kit-combobox', props.className)} autoSelect />;
|
||||
export interface ComboboxProps extends Omit<AriaComboboxProps, 'size'> {
|
||||
size?: 'small' | 'medium' | 'large' | 'xlarge';
|
||||
}
|
||||
|
||||
export function ComboboxInput({size, ...props}: ComboboxProps) {
|
||||
return <Combobox {...props} className={clsx('dbv-kit-combobox', `dbv-kit-combobox--${size ?? 'medium'}`, props.className)} autoSelect />;
|
||||
}
|
||||
|
||||
export interface ComboboxPopoverProps extends AriaComboboxPopoverProps {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<TKey, TValue> = Omit<
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
'onChange' | 'onSelect' | 'name' | 'value' | 'defaultValue'
|
||||
'onChange' | 'onSelect' | 'name' | 'value' | 'defaultValue' | 'size'
|
||||
> &
|
||||
ILayoutSizeProps & {
|
||||
items: TValue[];
|
||||
@@ -35,6 +35,8 @@ export type ComboboxBaseProps<TKey, TValue> = Omit<
|
||||
isDisabled?: (item: TValue) => boolean;
|
||||
inline?: boolean;
|
||||
allowCustomValue?: boolean;
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
onChange?: (value: string | null) => void;
|
||||
};
|
||||
|
||||
type ControlledProps<TKey, TValue> = ComboboxBaseProps<TKey, TValue> & {
|
||||
@@ -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<any, any> | ObjectProps<any, any, any>) {
|
||||
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<string | null>(null);
|
||||
const [internalInputValue, setInternalInputValue] = useState<string | null>(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<HTMLInputElement>) {
|
||||
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 (
|
||||
<Field {...layoutProps} className={clsx(className, inline && 'tw:flex tw:items-center')}>
|
||||
@@ -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 ? (
|
||||
<Spinner size="small" className="tw:absolute tw:right-2 tw:top-[50%] tw:-translate-y-1/2" />
|
||||
) : (
|
||||
<ComboboxDisclosure
|
||||
disabled={disabled || loading || readOnly}
|
||||
className="tw:absolute tw:right-2 tw:top-[50%] tw:-translate-y-1/2 tw:*:fill-none! tw:cursor-pointer"
|
||||
/>
|
||||
displayPopover && (
|
||||
<ComboboxDisclosure
|
||||
disabled={disabled || loading || readOnly}
|
||||
className="tw:absolute tw:right-2 tw:top-[50%] tw:-translate-y-1/2 tw:*:fill-none! tw:cursor-pointer"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{icon && <div className="tw:absolute tw:left-3 tw:w-4 tw:h-4">{typeof icon === 'string' ? <IconOrImage icon={icon} /> : icon}</div>}
|
||||
<ComboboxPopover className="theme-text-on-surface theme-background-surface theme-typography--caption">
|
||||
{filteredItems.length > 0 ? (
|
||||
filteredItems.map(({ itemKey, itemValue, itemTitle, itemIcon, itemDisabled }) => (
|
||||
<ComboboxItem
|
||||
key={itemKey}
|
||||
value={itemKey}
|
||||
disabled={itemDisabled}
|
||||
title={itemTitle}
|
||||
setValueOnClick={() => {
|
||||
setInputValue(null);
|
||||
return false;
|
||||
}}
|
||||
className={clsx({
|
||||
'tw:cursor-pointer': !itemDisabled,
|
||||
'tw:cursor-not-allowed': itemDisabled,
|
||||
})}
|
||||
onClick={() => {
|
||||
handleSelect(itemKey);
|
||||
}}
|
||||
>
|
||||
{iconSelector && (
|
||||
<div className="tw:w-4 tw:h-4 tw:shrink-0">
|
||||
{itemIcon && typeof itemIcon === 'string' ? <IconOrImage icon={itemIcon} /> : itemIcon}
|
||||
</div>
|
||||
)}
|
||||
<div className="tw:truncate">{itemValue}</div>
|
||||
</ComboboxItem>
|
||||
))
|
||||
) : (
|
||||
<div className="tw:p-2">{translate('combobox_no_results_placeholder')}</div>
|
||||
)}
|
||||
</ComboboxPopover>
|
||||
{displayPopover && (
|
||||
<ComboboxPopover className="theme-text-on-surface theme-background-surface theme-typography--caption">
|
||||
{filteredItems.length > 0 ? (
|
||||
filteredItems.map(({ itemKey, itemValue, itemTitle, itemIcon, itemDisabled }) => (
|
||||
<ComboboxItem
|
||||
key={itemKey}
|
||||
value={itemKey}
|
||||
disabled={itemDisabled}
|
||||
title={itemTitle}
|
||||
setValueOnClick={handleSetValueOnClick}
|
||||
className={clsx({
|
||||
'tw:cursor-pointer': !itemDisabled,
|
||||
'tw:cursor-not-allowed': itemDisabled,
|
||||
})}
|
||||
>
|
||||
{iconSelector && (
|
||||
<div className="tw:w-4 tw:h-4 tw:shrink-0">
|
||||
{itemIcon && typeof itemIcon === 'string' ? <IconOrImage icon={itemIcon} /> : itemIcon}
|
||||
</div>
|
||||
)}
|
||||
<div className="tw:truncate">{itemValue}</div>
|
||||
</ComboboxItem>
|
||||
))
|
||||
) : (
|
||||
<div className="tw:p-2">{translate('combobox_no_results_placeholder')}</div>
|
||||
)}
|
||||
</ComboboxPopover>
|
||||
)}
|
||||
</div>
|
||||
</ComboboxProvider>
|
||||
{description && <FieldDescription>{description}</FieldDescription>}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
.propertiesHeaderAdd {
|
||||
flex: 0 0 auto;
|
||||
padding: 0px 28px;
|
||||
padding: 0px 24px;
|
||||
}
|
||||
|
||||
.propertiesList {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Props>(function PropertyItem({ property, va
|
||||
const keyInputRef = useRef<HTMLInputElement>(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<Props>(function PropertyItem({ property, va
|
||||
return (
|
||||
<div className={s(styles, { container: true })}>
|
||||
<div className={s(styles, { name: true, error })} title={property.description}>
|
||||
<ShadowInput
|
||||
<Input
|
||||
ref={keyInputRef}
|
||||
className={s(styles, { shadowInput: true })}
|
||||
value={property.displayName || property.key}
|
||||
type="text"
|
||||
name={property.id}
|
||||
placeholder={keyPlaceholder}
|
||||
readOnly={!isDeletable}
|
||||
autoComplete="none"
|
||||
onChange={handleKeyChange}
|
||||
>
|
||||
{property.displayName || property.key}
|
||||
</ShadowInput>
|
||||
size="small"
|
||||
onChange={e => handleKeyChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className={s(styles, { value: true })} title={String(propertyValue)}>
|
||||
{!readOnly && property.validValues && property.validValues.length > 0 && (
|
||||
<Combobox
|
||||
value={value}
|
||||
defaultValue={valuePlaceholder}
|
||||
name={`${property.id}_value`}
|
||||
placeholder={valuePlaceholder}
|
||||
items={property.validValues}
|
||||
readOnly={readOnly}
|
||||
tiny
|
||||
fill
|
||||
allowCustomValue
|
||||
onSelect={handleValueChange}
|
||||
/>
|
||||
)}
|
||||
<div className={s(styles, { remove: true })} title={translate('core_blocks_properties_table_item_reset')}>
|
||||
<button hidden={!edited || isDeletable} className={s(styles, { button: true })} type="button" onClick={handleRevert}>
|
||||
<IconOrImage className={s(styles, { iconOrImage: true })} icon="/icons/data_revert_all_sm.svg" viewBox="0 0 16 16" />
|
||||
</button>
|
||||
<Combobox
|
||||
value={value}
|
||||
defaultValue={valuePlaceholder}
|
||||
name={`${property.id}_value`}
|
||||
placeholder={valuePlaceholder}
|
||||
items={property.validValues || []}
|
||||
readOnly={readOnly}
|
||||
size="small"
|
||||
tiny
|
||||
fill
|
||||
allowCustomValue
|
||||
onChange={handleValueChange}
|
||||
/>
|
||||
<div className={s(styles, { actions: true })}>
|
||||
{edited && !isDeletable && (
|
||||
<ActionIconButton
|
||||
title={translate('core_blocks_properties_table_item_reset')}
|
||||
name="/icons/data_revert_all_sm.svg"
|
||||
viewBox="0 0 16 16"
|
||||
type="button"
|
||||
onClick={handleRevert}
|
||||
/>
|
||||
)}
|
||||
{isDeletable && (
|
||||
<ActionIconButton
|
||||
title={translate('core_blocks_properties_table_item_remove')}
|
||||
name="reject"
|
||||
viewBox="0 0 11 11"
|
||||
type="button"
|
||||
onClick={handleRemove}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isDeletable && (
|
||||
<div className={s(styles, { remove: true })} title={translate('core_blocks_properties_table_item_remove')}>
|
||||
<button className={s(styles, { button: true })} type="button" onClick={handleRemove}>
|
||||
<Icon className={s(styles, { icon: true })} name="reject" viewBox="0 0 11 11" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user