dbeaver/pro#5394 feat: null and boolean formatter for base grid (#3359)

* dbeaver/pro#5394 feat: null and boolean formatter for base grid

* feat: reuse base grid formatters

* chore: try to adjust workflow concurrency

* chore: trigger workflow

* chore: trigger workflow cancel

* chore: fix label styles

---------

Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
This commit is contained in:
Alexey
2025-04-09 15:06:32 +08:00
committed by GitHub
co-authored by mr-anton-t
parent e1a05496cb
commit 9c3cc17bd9
39 changed files with 508 additions and 44 deletions
@@ -4,6 +4,7 @@
"sideEffects": [],
"exports": {
".": "./lib/index.js",
"./*.css": "./src/*.css",
"./*": "./lib/*.js"
},
"scripts": {
@@ -32,6 +33,7 @@
"typescript": "^5"
},
"dependencies": {
"@dbeaver/ui-kit": "workspace:^",
"react": "^19",
"react-data-grid": "file:./artifacts",
"react-dom": "^19"
@@ -1,3 +1,4 @@
@import url('@dbeaver/ui-kit/index.css');
@import url('react-data-grid/lib/styles.css') layer(components);
@layer components.rdg.MeasuringCell {
@@ -5,3 +6,24 @@
max-width: var(--rdg-max-auto-size, 350px);
}
}
@layer components.rdg.Cell {
@media (prefers-color-scheme: dark) {
.rdg-cell-frozen {
&:nth-last-child(1 of &) {
box-shadow: none;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
background-color: rgba(255, 255, 255, 0.02);
}
}
}
}
}
@@ -0,0 +1,15 @@
import { NullFormatter } from './NullFormatter.js';
import { Checkbox } from '@dbeaver/ui-kit';
interface Props {
value: boolean | null;
className?: string;
onClick?: React.MouseEventHandler<HTMLElement>;
}
export function BooleanFormatter({ value, className, onClick }: Props) {
if (value === null) {
return <NullFormatter className={className} onClick={onClick} />;
}
return <Checkbox size="small" checked={value} className={className} onClick={onClick} />;
}
@@ -0,0 +1,21 @@
import { NullFormatter } from './NullFormatter.js';
interface Props {
value: Date | null;
type?: 'time' | 'date' | 'datetime';
onClick?: React.MouseEventHandler<HTMLElement>;
}
export function DateFormatter({ value, type = 'datetime', onClick }: Props) {
if (value === null) {
return <NullFormatter />;
}
return (
<div onClick={onClick}>
{value.toLocaleString(undefined, {
dateStyle: type === 'date' ? 'long' : undefined,
timeStyle: type === 'time' ? 'long' : undefined,
})}
</div>
);
}
@@ -0,0 +1,12 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
.nullValue {
text-transform: uppercase;
opacity: 0.65;
}
@@ -0,0 +1,15 @@
import styles from './NullFormatter.module.css';
import { clsx } from '@dbeaver/ui-kit';
interface Props {
className?: string;
onClick?: React.MouseEventHandler<HTMLElement>;
}
export function NullFormatter({ className, onClick }: Props) {
return (
<span className={clsx(styles['nullValue'], className)} onClick={onClick}>
[NULL]
</span>
);
}
@@ -0,0 +1,18 @@
import { NullFormatter } from './NullFormatter.js';
interface Props {
value: number | null;
className?: string;
onClick?: React.MouseEventHandler<HTMLElement>;
}
export function NumberFormatter({ value, className, onClick }: Props) {
if (value === null) {
return <NullFormatter className={className} onClick={onClick} />;
}
return (
<div style={{ textAlign: 'right' }} className={className} onClick={onClick}>
{value.toLocaleString()}
</div>
);
}
@@ -4,3 +4,8 @@ export * from './DataGridCellInnerContext.js';
export * from './IGridReactiveValue.js';
export * from './useCreateGridReactiveValue.js';
export * from './useGridReactiveValue.js';
export * from './formatters/NullFormatter.js';
export * from './formatters/BooleanFormatter.js';
export * from './formatters/NumberFormatter.js';
export * from './formatters/DateFormatter.js';
@@ -12,5 +12,10 @@
},
"include": [
"src"
],
"references": [
{
"path": "../ui-kit"
}
]
}
+3 -1
View File
@@ -5,7 +5,9 @@
"description": "",
"license": "Apache-2.0",
"exports": {
".": "./lib/index.js"
".": "./lib/index.js",
"./*.css": "./src/*.css",
"./*": "./lib/*.js"
},
"scripts": {
"build": "tsc -b",
@@ -9,6 +9,7 @@ import { Button as AriaButton, type ButtonProps as AriaKitButtonProps } from '@a
import clsx from 'clsx';
import './Button.css';
import type { ControlSize } from '../types/controls.js';
import { componentProviderWrapper } from '../componentProviderWrapper.js';
export interface ButtonProps extends Omit<AriaKitButtonProps, 'clickOnEnter' | 'clickOnSpace'> {
variant?: 'primary' | 'secondary' | 'danger';
@@ -17,7 +18,7 @@ export interface ButtonProps extends Omit<AriaKitButtonProps, 'clickOnEnter' | '
loader?: React.ReactNode;
}
export function Button({ className, variant = 'primary', size = 'medium', loading, loader, children, onClick, ...props }: ButtonProps) {
export function ButtonBase({ className, variant = 'primary', size = 'medium', loading, loader, children, onClick, ...props }: ButtonProps) {
const classToApply = clsx('dbv-kit-button', `dbv-kit-button--${variant}`, `dbv-kit-button--${size}`, className);
if (loading) {
@@ -41,7 +42,9 @@ export interface ButtonIconProps extends React.HTMLAttributes<HTMLSpanElement> {
*
* @param props.placement This property is needed to adjust icon placement inside a button. The icon with placement="start" will cut the inline-start padding. placement="end" will affect the padding-inline-end accordingly. This property supports RTL and LTR, so you don't need to think about it.
*/
Button.Icon = function ButtonIcon({ className, children, placement }: ButtonIconProps) {
ButtonBase.Icon = function ButtonBaseIcon({ className, children, placement }: ButtonIconProps) {
const classToApply = clsx('dbv-kit-button__icon', placement && `dbv-kit-button__icon--${placement}`, className);
return <span className={classToApply}>{children}</span>;
};
export const Button = componentProviderWrapper('Button', ButtonBase);
@@ -11,6 +11,7 @@ import { useState } from 'react';
import clsx from 'clsx';
import './Checkbox.css';
import type { ControlSize } from '../types/controls.js';
import { componentProviderWrapper } from '../componentProviderWrapper.js';
export interface CheckboxProps extends Omit<AriaKitCheckboxProps, 'render' | 'size'> {
size?: ControlSize;
@@ -19,7 +20,7 @@ export interface CheckboxProps extends Omit<AriaKitCheckboxProps, 'render' | 'si
indeterminateIcon?: React.ReactNode;
}
export function Checkbox({ children, className, icon, indeterminate, indeterminateIcon, size = 'medium', ...props }: CheckboxProps) {
export function CheckboxBase({ children, className, icon, indeterminate, indeterminateIcon, size = 'medium', ...props }: CheckboxProps) {
const [innerChecked, setInnerChecked] = useState(props.defaultChecked ?? false);
const checked = props.checked ?? innerChecked;
@@ -70,3 +71,5 @@ export function Checkbox({ children, className, icon, indeterminate, indetermina
</label>
);
}
export const Checkbox = componentProviderWrapper('Checkbox', CheckboxBase);
@@ -0,0 +1,19 @@
import { createContext } from 'react';
import type { ButtonBase } from './Button/Button.js';
import type { CheckboxBase } from './Checkbox/Checkbox.js';
import type { Radio } from './Radio/Radio.js';
import type { InputBase } from './Input/Input.js';
import type { RadioGroup } from './Radio/RadioGroup.js';
import type { Select } from './Select/Select.js';
import type { SelectField } from './Select/SelectField.js';
export interface IComponentProvider {
Button?: typeof ButtonBase;
Input?: typeof InputBase;
Checkbox?: typeof CheckboxBase;
Radio?: typeof Radio;
RadioGroup?: typeof RadioGroup;
Select?: typeof Select;
SelectField?: typeof SelectField;
}
export const ComponentProvider = createContext<IComponentProvider>({});
@@ -9,13 +9,14 @@
import { type ComponentPropsWithRef } from 'react';
import './Input.css';
import clsx from 'clsx';
import { componentProviderWrapper } from '../componentProviderWrapper.js';
export interface InputProps extends Omit<ComponentPropsWithRef<'input'>, 'size'> {
size?: 'small' | 'medium' | 'large' | 'xlarge';
label?: React.ReactNode;
}
export const Input: React.FC<InputProps> = function Input({ size, className, ...props }) {
export const InputBase: React.FC<InputProps> = function Input({ size, className, ...props }) {
const classNameToApply = clsx(`dbv-kit-input`, `dbv-kit-input--${size ?? 'medium'}`, className);
return (
<label className="dbv-kit-input-wrapper">
@@ -24,3 +25,5 @@ export const Input: React.FC<InputProps> = function Input({ size, className, ...
</label>
);
};
export const Input = componentProviderWrapper('Input', InputBase);
@@ -0,0 +1,13 @@
import { use } from 'react';
import { ComponentProvider, type IComponentProvider } from './ComponentProvider.js';
export function componentProviderWrapper<T extends keyof IComponentProvider>(
key: T,
component: Required<IComponentProvider>[T],
): Required<IComponentProvider>[T] {
return function componentProviderWrapperInner(props: Required<IComponentProvider>[T] extends React.FC<infer P> ? P : never) {
const provider = use(ComponentProvider);
const Component = provider[key] || component;
return <Component {...(props as any)} />;
} as Required<IComponentProvider>[T];
}
+4 -3
View File
@@ -18,9 +18,9 @@ export {
type FocusTrapRegionProps,
} from '@ariakit/react';
export { Button, type ButtonProps } from './Button/Button.js';
export { Checkbox, type CheckboxProps } from './Checkbox/Checkbox.js';
export { Input, type InputProps } from './Input/Input.js';
export { Button, ButtonBase, type ButtonProps } from './Button/Button.js';
export { Checkbox, CheckboxBase, type CheckboxProps } from './Checkbox/Checkbox.js';
export { Input, InputBase, type InputProps } from './Input/Input.js';
export {
Select,
useSelectContext,
@@ -34,3 +34,4 @@ export {
export { SelectField, type SelectFieldProps, type SelectItem } from './Select/SelectField.js';
export { Radio, RadioGroup, useRadioContext, useRadioStore, type RadioProviderProps, type RadioProps, type RadioGroupProps } from './Radio/index.js';
export * from './utils/clsx.js';
export * from './ComponentProvider.js';
+2 -1
View File
@@ -341,6 +341,7 @@ __metadata:
resolution: "@dbeaver/react-data-grid@workspace:@dbeaver/react-data-grid"
dependencies:
"@dbeaver/tsconfig": "workspace:^"
"@dbeaver/ui-kit": "workspace:^"
"@types/react": "npm:^19"
"@types/react-dom": "npm:^19"
react: "npm:^19"
@@ -361,7 +362,7 @@ __metadata:
languageName: unknown
linkType: soft
"@dbeaver/ui-kit@workspace:@dbeaver/ui-kit":
"@dbeaver/ui-kit@workspace:@dbeaver/ui-kit, @dbeaver/ui-kit@workspace:^":
version: 0.0.0-use.local
resolution: "@dbeaver/ui-kit@workspace:@dbeaver/ui-kit"
dependencies: