From 52f567fdedbbb3fc7e73da95286ef7cb75d6ed46 Mon Sep 17 00:00:00 2001 From: Sychev Andrey <44414066+SychevAndrey@users.noreply.github.com> Date: Fri, 23 May 2025 20:21:37 +0400 Subject: [PATCH] dbeaver/pro#5553 Add Sorting functionality to base grid (#3456) * dbeaver/pro#5553 feat: add column sorting functionality to DataGrid * dbeaver/pro#5553 feat: enhance column sorting functionality to support multiple selection * dbeaver/pro#5553 feat: add keyboard navigation to order button * dbeaver/pro#5553 feat: add constraints handling logic for column sorting in DataGrid * Revert "dbeaver/pro#5553 feat: add constraints handling logic for column sorting in DataGrid" This reverts commit 6028a48b13a2d02aa6f154912c46d40db55757be. * dbeaver/pro#5553 feat: add icon generation script to UIKit, add sample icons * dbeaver/pro#5553 refactor: use inner react-data-grid API for column sorting * dbeaver/pro#5553 feat: add IconCross component and update icon-related files * dbeaver/pro#5553 feat: replace Button with IconButton in OrderButton component * dbeaver/pro#5553 style: update foreground color variables for IconButton component * dbeaver/pro#5553 refactor: add normalization rules to layers, use IconButton in Common DialogHeader checked if new IconButton works well in light and dark mode by using in CommonDialogHeader * dbeaver/pro#5553 style: format code and update dependencies in CommonDialogHeader and DataGridTable components * dbeaver/pro#5553 style: move onColumnSort prop to correct position in DataGridTable component * dbeaver/pro#5553 refactor: remove icons component generation, use svg sprite * dbeaver/pro#5553 refactor: update OrderButton component and inline icons sprite in Body we decided to go with this solution for now just adding icons to a sprite and use them by id. Imports by ?url don't work in VSCode if try to use them in href, so we need to find another solution or stick to that approach. * dbeaver/pro#5553 refactor: update column sorting and sortable logic in DataGrid components * dbeaver/pro#5553 style: format whitespace in Body component * dbeaver/pro#5553 feat: add new sorting icons for ascending, descending, and unsorted states * dbeaver/pro#5553 refactor:styling for header cell and order button components * dbeaver/pro#5553 refactor: update sorting logic in DataGrid and OrderButton components * dbeaver/pro#5553 refactor: value deps * dbeaver/pro#5553 feat: update sorting icons for ascending, descending, and unsorted states * dbeaver/pro#5553 style: update aria-label and title for OrderButton component * dbeaver/pro#5553 fix: double focus * dbeaver/pro#5553 fix: after merge * dbeaver/pro#5553 refactor: removed outline offset for sort button * dbeaver/pro#5553 refactor: update Tailwind CSS import to use layer directive --------- Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com> Co-authored-by: Alexey --- .../@dbeaver/react-data-grid/src/DataGrid.css | 4 + .../@dbeaver/react-data-grid/src/DataGrid.tsx | 5 +- .../src/DataGridHeaderCellContext.ts | 3 + .../src/mapRenderHeaderCell.tsx | 4 +- .../renderers/HeaderCellContentRenderer.tsx | 33 +++++++- .../src/renderers/OrderButton.tsx | 42 ++++++++++ .../@dbeaver/ui-kit/.ladle/global.css | 2 +- .../ui-kit/assets/icons/icon-sort-asc.svg | 8 ++ .../ui-kit/assets/icons/icon-sort-asc_m.svg | 8 ++ .../ui-kit/assets/icons/icon-sort-asc_sm.svg | 8 ++ .../ui-kit/assets/icons/icon-sort-desc.svg | 8 ++ .../ui-kit/assets/icons/icon-sort-desc_m.svg | 8 ++ .../ui-kit/assets/icons/icon-sort-desc_sm.svg | 8 ++ .../assets/icons/icon-sort-unsorted.svg | 7 ++ .../assets/icons/icon-sort-unsorted_m.svg | 7 ++ .../assets/icons/icon-sort-unsorted_sm.svg | 7 ++ .../@dbeaver/ui-kit/assets/icons/icons.svg | 28 +++++++ common-react/@dbeaver/ui-kit/package.json | 3 +- .../ui-kit/scripts/icon-index-template.js | 16 ++++ .../ui-kit/src/IconButton/IconButton.css | 9 +-- .../ui-kit/src/IconButton/IconButton.tsx | 2 +- .../@dbeaver/ui-kit/src/IconButton/_base.css | 4 +- .../IconButton/icon-button.stories.tsx | 76 +++++++++---------- webapp/packages/core-app/package.json | 1 + webapp/packages/core-app/src/Body.tsx | 4 +- webapp/packages/core-app/tsconfig.json | 3 + .../CommonDialogHeader.module.css | 5 -- .../CommonDialog/CommonDialogHeader.tsx | 11 +-- .../packages/core-theming/src/ThemeService.ts | 1 + .../core-theming/src/styles/UiCheckbox.css | 1 - .../core-theming/src/styles/UiIconButton.css | 21 +++++ .../src/styles/main/light.theme.scss | 36 ++++----- .../src/styles/main/normalize.pure.css | 3 +- .../src/DataGrid/DataGridTable.tsx | 57 +++++++++++++- .../TableColumnHeader/OrderButton.tsx | 58 -------------- .../TableColumnHeader/TableColumnHeader.tsx | 14 ---- .../src/baseConfigurationPlugin.ts | 2 + .../packages/product-default/src/styles.css | 4 +- webapp/yarn.lock | 1 + 39 files changed, 357 insertions(+), 165 deletions(-) create mode 100644 common-react/@dbeaver/react-data-grid/src/renderers/OrderButton.tsx create mode 100644 common-react/@dbeaver/ui-kit/assets/icons/icon-sort-asc.svg create mode 100644 common-react/@dbeaver/ui-kit/assets/icons/icon-sort-asc_m.svg create mode 100644 common-react/@dbeaver/ui-kit/assets/icons/icon-sort-asc_sm.svg create mode 100644 common-react/@dbeaver/ui-kit/assets/icons/icon-sort-desc.svg create mode 100644 common-react/@dbeaver/ui-kit/assets/icons/icon-sort-desc_m.svg create mode 100644 common-react/@dbeaver/ui-kit/assets/icons/icon-sort-desc_sm.svg create mode 100644 common-react/@dbeaver/ui-kit/assets/icons/icon-sort-unsorted.svg create mode 100644 common-react/@dbeaver/ui-kit/assets/icons/icon-sort-unsorted_m.svg create mode 100644 common-react/@dbeaver/ui-kit/assets/icons/icon-sort-unsorted_sm.svg create mode 100644 common-react/@dbeaver/ui-kit/assets/icons/icons.svg create mode 100644 common-react/@dbeaver/ui-kit/scripts/icon-index-template.js create mode 100644 webapp/packages/core-theming/src/styles/UiIconButton.css delete mode 100644 webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/OrderButton.tsx diff --git a/common-react/@dbeaver/react-data-grid/src/DataGrid.css b/common-react/@dbeaver/react-data-grid/src/DataGrid.css index d6d795b020..0471601630 100644 --- a/common-react/@dbeaver/react-data-grid/src/DataGrid.css +++ b/common-react/@dbeaver/react-data-grid/src/DataGrid.css @@ -8,6 +8,10 @@ } @layer components.rdg.Cell { + .rdg { + --dbv-kit-icon-btn-outline-width: 1px; + } + @media (prefers-color-scheme: dark) { .rdg-cell-frozen { &:nth-last-child(1 of &) { diff --git a/common-react/@dbeaver/react-data-grid/src/DataGrid.tsx b/common-react/@dbeaver/react-data-grid/src/DataGrid.tsx index f253bf9eae..af69084940 100644 --- a/common-react/@dbeaver/react-data-grid/src/DataGrid.tsx +++ b/common-react/@dbeaver/react-data-grid/src/DataGrid.tsx @@ -49,8 +49,10 @@ export const DataGrid = forwardRef(function DataGrid headerText, getHeaderOrder, getHeaderResizable, + columnSortable, getHeaderHeight, getHeaderPinned, + columnSortingState, getHeaderDnD, cell, cellText, @@ -67,6 +69,7 @@ export const DataGrid = forwardRef(function DataGrid onScrollToBottom, onFocus, onCellChange, + onColumnSort, children, className, onCellKeyDown, @@ -141,7 +144,7 @@ export const DataGrid = forwardRef(function DataGrid - + boolean; getHeaderDnD?: (colIdx: number) => boolean; onHeaderReorder?: (from: number, to: number) => void; + columnSortingState?: IGridReactiveValue<'asc' | 'desc' | undefined | null, [colIdx: number]>; + columnSortable?: IGridReactiveValue; + onColumnSort?: (colIdx: number, order: 'asc' | 'desc' | null, isMultiple: boolean) => void; } export const DataGridCellHeaderContext = createContext(null); diff --git a/common-react/@dbeaver/react-data-grid/src/mapRenderHeaderCell.tsx b/common-react/@dbeaver/react-data-grid/src/mapRenderHeaderCell.tsx index 6c98d86727..01287f73c9 100644 --- a/common-react/@dbeaver/react-data-grid/src/mapRenderHeaderCell.tsx +++ b/common-react/@dbeaver/react-data-grid/src/mapRenderHeaderCell.tsx @@ -3,7 +3,7 @@ import type { IInnerRow } from './IInnerRow.js'; import { HeaderCellContentRenderer } from './renderers/HeaderCellContentRenderer.js'; export function mapRenderHeaderCell(colIdx: number) { - return function RenderHeaderCell({}: RenderHeaderCellProps) { - return ; + return function RenderHeaderCell({ tabIndex }: RenderHeaderCellProps) { + return ; }; } diff --git a/common-react/@dbeaver/react-data-grid/src/renderers/HeaderCellContentRenderer.tsx b/common-react/@dbeaver/react-data-grid/src/renderers/HeaderCellContentRenderer.tsx index 3833e6d18c..275d038cd8 100644 --- a/common-react/@dbeaver/react-data-grid/src/renderers/HeaderCellContentRenderer.tsx +++ b/common-react/@dbeaver/react-data-grid/src/renderers/HeaderCellContentRenderer.tsx @@ -1,17 +1,25 @@ -import { memo, use } from 'react'; +import { memo, use, useRef } from 'react'; import { useDrag, useDrop, type DnDStoreProvider } from '@dbeaver/react-dnd'; import { DataGridCellHeaderContext } from '../DataGridHeaderCellContext.js'; import { useGridReactiveValue } from '../useGridReactiveValue.js'; import { HeaderDnDContext } from '../useHeaderDnD.js'; +import { OrderButton } from './OrderButton.js'; interface Props { colIdx: number; + tabIndex?: number; } -export const HeaderCellContentRenderer = memo(function HeaderCellContentRenderer({ colIdx }: Props) { +export const HeaderCellContentRenderer = memo(function HeaderCellContentRenderer({ colIdx, tabIndex }: Props) { const dndHeaderContext = use(HeaderDnDContext); const cellHeaderContext = use(DataGridCellHeaderContext); const headerElement = useGridReactiveValue(cellHeaderContext?.headerElement, colIdx); const getHeaderText = useGridReactiveValue(headerElement ? undefined : cellHeaderContext?.headerText, colIdx); + const isColumnSortable = useGridReactiveValue(cellHeaderContext?.columnSortable, colIdx); + const onColumnSort = cellHeaderContext?.onColumnSort; + const sortingState = useGridReactiveValue(cellHeaderContext?.columnSortingState, colIdx); + + const orderButtonRef = useRef(null); + const containerRef = useRef(null); const draggable = dndHeaderContext?.getCanDrag?.(colIdx) ?? false; const drag = useDrag({ @@ -47,9 +55,26 @@ export const HeaderCellContentRenderer = memo(function HeaderCellContentRenderer }, }); + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Tab' && !e.shiftKey && isColumnSortable && onColumnSort && orderButtonRef.current !== document.activeElement) { + e.preventDefault(); + e.stopPropagation(); + containerRef.current?.parentElement?.setAttribute('aria-selected', 'false'); + orderButtonRef.current?.focus(); + } + }; + return ( -
- {headerElement ?? getHeaderText ?? ''} +
+ {headerElement ?? getHeaderText ?? ''} + {isColumnSortable && onColumnSort && }
); }); diff --git a/common-react/@dbeaver/react-data-grid/src/renderers/OrderButton.tsx b/common-react/@dbeaver/react-data-grid/src/renderers/OrderButton.tsx new file mode 100644 index 0000000000..c152bbcd59 --- /dev/null +++ b/common-react/@dbeaver/react-data-grid/src/renderers/OrderButton.tsx @@ -0,0 +1,42 @@ +import { clsx, IconButton } from '@dbeaver/ui-kit'; + +interface OrderButtonProps { + colIdx: number; + sortState?: 'asc' | 'desc' | null; + onSort: (attributePosition: number, order: 'asc' | 'desc' | null, isMultiple: boolean) => void; + tabIndex?: number; + ref: React.Ref; +} + +export function OrderButton({ colIdx, sortState, onSort, tabIndex, ref }: OrderButtonProps) { + function handleClick(e: React.MouseEvent) { + const nextSortState = sortState === 'asc' ? 'desc' : sortState === 'desc' ? null : 'asc'; + onSort(colIdx, nextSortState, e.ctrlKey || e.metaKey); + } + + const svgSortAsc = '#icon-sort-asc'; + const svgSortDesc = '#icon-sort-desc'; + const svgSortUnknown = '#icon-sort-unknown'; + + const iconSrc = sortState === 'asc' ? svgSortAsc : sortState === 'desc' ? svgSortDesc : svgSortUnknown; + + return ( + + + + + + ); +} diff --git a/common-react/@dbeaver/ui-kit/.ladle/global.css b/common-react/@dbeaver/ui-kit/.ladle/global.css index 45f728e2bc..388b137073 100644 --- a/common-react/@dbeaver/ui-kit/.ladle/global.css +++ b/common-react/@dbeaver/ui-kit/.ladle/global.css @@ -1,4 +1,4 @@ -@import 'tailwindcss' prefix(tw); +@import 'tailwindcss' layer(base) prefix(tw); @layer theme { main { diff --git a/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-asc.svg b/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-asc.svg new file mode 100644 index 0000000000..f6bba7ac1d --- /dev/null +++ b/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-asc.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-asc_m.svg b/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-asc_m.svg new file mode 100644 index 0000000000..4c30067186 --- /dev/null +++ b/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-asc_m.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-asc_sm.svg b/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-asc_sm.svg new file mode 100644 index 0000000000..b234902b13 --- /dev/null +++ b/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-asc_sm.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-desc.svg b/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-desc.svg new file mode 100644 index 0000000000..0c7611a56d --- /dev/null +++ b/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-desc.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-desc_m.svg b/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-desc_m.svg new file mode 100644 index 0000000000..40bdfc53ac --- /dev/null +++ b/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-desc_m.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-desc_sm.svg b/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-desc_sm.svg new file mode 100644 index 0000000000..39a674aa15 --- /dev/null +++ b/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-desc_sm.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-unsorted.svg b/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-unsorted.svg new file mode 100644 index 0000000000..ad371b9ccc --- /dev/null +++ b/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-unsorted.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-unsorted_m.svg b/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-unsorted_m.svg new file mode 100644 index 0000000000..7ceba2dca7 --- /dev/null +++ b/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-unsorted_m.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-unsorted_sm.svg b/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-unsorted_sm.svg new file mode 100644 index 0000000000..46b4769f2f --- /dev/null +++ b/common-react/@dbeaver/ui-kit/assets/icons/icon-sort-unsorted_sm.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/common-react/@dbeaver/ui-kit/assets/icons/icons.svg b/common-react/@dbeaver/ui-kit/assets/icons/icons.svg new file mode 100644 index 0000000000..ecb80697f5 --- /dev/null +++ b/common-react/@dbeaver/ui-kit/assets/icons/icons.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/common-react/@dbeaver/ui-kit/package.json b/common-react/@dbeaver/ui-kit/package.json index a1b3c22a37..3c499ed0c1 100644 --- a/common-react/@dbeaver/ui-kit/package.json +++ b/common-react/@dbeaver/ui-kit/package.json @@ -7,7 +7,8 @@ "exports": { ".": "./lib/index.js", "./*.css": "./src/*.css", - "./*": "./lib/*.js" + "./*": "./lib/*.js", + "./assets/icons/*.svg": "./assets/icons/*.svg" }, "scripts": { "build": "tsc -b", diff --git a/common-react/@dbeaver/ui-kit/scripts/icon-index-template.js b/common-react/@dbeaver/ui-kit/scripts/icon-index-template.js new file mode 100644 index 0000000000..9447d3bc25 --- /dev/null +++ b/common-react/@dbeaver/ui-kit/scripts/icon-index-template.js @@ -0,0 +1,16 @@ +export default function template(files) { + const exportLines = files.map((file) => { + const { path } = file; + + const fileName = path.split('/').pop(); + const componentNameWithExt = fileName.replace(/\.tsx$/, '.js'); + const componentName = fileName.replace(/\.tsx$/, ''); + + return `export { default as ${componentName} } from './${componentNameWithExt}';`; + }); + + + const header = '/* This file is automatically generated by the build:icons script. Do not modify it manually.*/\n\n '; + + return header + exportLines.join('\n'); +}; \ No newline at end of file diff --git a/common-react/@dbeaver/ui-kit/src/IconButton/IconButton.css b/common-react/@dbeaver/ui-kit/src/IconButton/IconButton.css index d67a0284b1..0708d50de6 100644 --- a/common-react/@dbeaver/ui-kit/src/IconButton/IconButton.css +++ b/common-react/@dbeaver/ui-kit/src/IconButton/IconButton.css @@ -7,6 +7,7 @@ min-width: var(--dbv-kit-icon-btn-size); width: var(--dbv-kit-icon-btn-size); height: var(--dbv-kit-icon-btn-size); + font-size: calc(var(--dbv-kit-icon-btn-size) * 0.75); border-radius: var(--dbv-kit-icon-btn-border-radius); border-color: var(--dbv-kit-icon-btn-border-color); @@ -56,14 +57,6 @@ } } - .dbv-kit-icon-button__icon { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - } - /* Size variants */ .dbv-kit-icon-button--small { --dbv-kit-icon-btn-size: var(--dbv-kit-icon-btn-small-size); diff --git a/common-react/@dbeaver/ui-kit/src/IconButton/IconButton.tsx b/common-react/@dbeaver/ui-kit/src/IconButton/IconButton.tsx index a8188d3677..b16bb171cc 100644 --- a/common-react/@dbeaver/ui-kit/src/IconButton/IconButton.tsx +++ b/common-react/@dbeaver/ui-kit/src/IconButton/IconButton.tsx @@ -24,7 +24,7 @@ export function IconButtonBase({ className, variant = 'primary', size = 'medium' return ( - {children} + {children} ); } diff --git a/common-react/@dbeaver/ui-kit/src/IconButton/_base.css b/common-react/@dbeaver/ui-kit/src/IconButton/_base.css index bc50d2d385..1fa0b6e336 100644 --- a/common-react/@dbeaver/ui-kit/src/IconButton/_base.css +++ b/common-react/@dbeaver/ui-kit/src/IconButton/_base.css @@ -17,8 +17,8 @@ /* Default colors */ --dbv-kit-icon-btn-foreground: var(--tw-color-gray-600); - --dbv-kit-icon-btn-foreground-hover: var(--dbv-kit-color-600); - --dbv-kit-icon-btn-foreground-active: var(--dbv-kit-color-700); + --dbv-kit-icon-btn-foreground-hover: var(--tw-color-gray-600); + --dbv-kit-icon-btn-foreground-active: var(--tw-color-gray-700); --dbv-kit-icon-btn-background: transparent; --dbv-kit-icon-btn-background-hover: var(--tw-color-gray-100); --dbv-kit-icon-btn-background-active: var(--tw-color-gray-200); diff --git a/common-react/@dbeaver/ui-kit/src/stories/components/IconButton/icon-button.stories.tsx b/common-react/@dbeaver/ui-kit/src/stories/components/IconButton/icon-button.stories.tsx index 64abd23f07..f649e369c9 100644 --- a/common-react/@dbeaver/ui-kit/src/stories/components/IconButton/icon-button.stories.tsx +++ b/common-react/@dbeaver/ui-kit/src/stories/components/IconButton/icon-button.stories.tsx @@ -91,23 +91,23 @@ export const IconButtons = () => {

Primary

- - + + - - + + - - + + - - + +
@@ -116,23 +116,23 @@ export const IconButtons = () => {

Secondary

- - + + - - + + - - + + - - + +
@@ -141,23 +141,23 @@ export const IconButtons = () => {

Danger

- - + + - - + + - - + + - - + +
@@ -166,41 +166,41 @@ export const IconButtons = () => {

Disabled

- - + + - - + + - - + +

Loading

- - + + - - + + - - + + - - + +
diff --git a/webapp/packages/core-app/package.json b/webapp/packages/core-app/package.json index d7db7108b3..c84aa419b3 100644 --- a/webapp/packages/core-app/package.json +++ b/webapp/packages/core-app/package.json @@ -33,6 +33,7 @@ "@cloudbeaver/core-ui": "workspace:*", "@cloudbeaver/core-utils": "workspace:*", "@cloudbeaver/core-version": "workspace:*", + "@dbeaver/ui-kit": "workspace:^", "mobx": "^6", "mobx-react-lite": "^4", "react": "^19", diff --git a/webapp/packages/core-app/src/Body.tsx b/webapp/packages/core-app/src/Body.tsx index 03fcc3698b..613d167119 100644 --- a/webapp/packages/core-app/src/Body.tsx +++ b/webapp/packages/core-app/src/Body.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import { useAppVersion } from '@cloudbeaver/core-version'; import style from './Body.module.css'; import { useAppHeight } from './useAppHeight.js'; import { useClientActivity } from './useClientActivity.js'; +import icons from '@dbeaver/ui-kit/assets/icons/icons.svg?raw'; export const Body = observer(function Body() { // const serverConfigLoader = useResource(Body, ServerConfigResource, undefined); @@ -70,6 +71,7 @@ export const Body = observer(function Body() { +
diff --git a/webapp/packages/core-app/tsconfig.json b/webapp/packages/core-app/tsconfig.json index dd3d13ad71..9419181103 100644 --- a/webapp/packages/core-app/tsconfig.json +++ b/webapp/packages/core-app/tsconfig.json @@ -7,6 +7,9 @@ "composite": true }, "references": [ + { + "path": "../../../common-react/@dbeaver/ui-kit" + }, { "path": "../core-blocks" }, diff --git a/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogHeader.module.css b/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogHeader.module.css index cd70decec4..c79dd0c8cc 100644 --- a/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogHeader.module.css +++ b/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogHeader.module.css @@ -57,8 +57,3 @@ text-overflow: ellipsis; white-space: nowrap; } - -.reject { - cursor: pointer; - flex-shrink: 0; -} diff --git a/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogHeader.tsx b/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogHeader.tsx index 88df8bf1a0..310e6684ab 100644 --- a/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogHeader.tsx +++ b/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogHeader.tsx @@ -1,13 +1,14 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { ActionIconButton } from '../../ActionIconButton.js'; +import { IconButton } from '@dbeaver/ui-kit'; +import { Icon } from '../../Icon.js'; import { IconOrImage } from '../../IconOrImage.js'; import { useTranslate } from '../../localization/useTranslate.js'; import { s } from '../../s.js'; @@ -46,9 +47,9 @@ export const CommonDialogHeader = observer(function CommonDialogHeader({

{translate(title)}

{onReject && ( -
- -
+ + + )}
{subTitle &&
{typeof subTitle === 'string' ? translate(subTitle) : subTitle}
} diff --git a/webapp/packages/core-theming/src/ThemeService.ts b/webapp/packages/core-theming/src/ThemeService.ts index b824358c46..dd28f8c316 100644 --- a/webapp/packages/core-theming/src/ThemeService.ts +++ b/webapp/packages/core-theming/src/ThemeService.ts @@ -20,6 +20,7 @@ import './styles/main/fonts.pure.css'; import './styles/main/normalize.pure.css'; import './styles/main/typography.pure.scss'; import './styles/UiCheckbox.css'; +import './styles/UiIconButton.css'; import { DEFAULT_THEME_ID, themes } from './themes.js'; import { ThemeSettingsService } from './ThemeSettingsService.js'; import type { ClassCollection } from './themeUtils.js'; diff --git a/webapp/packages/core-theming/src/styles/UiCheckbox.css b/webapp/packages/core-theming/src/styles/UiCheckbox.css index 67a23e1cb4..6514157773 100644 --- a/webapp/packages/core-theming/src/styles/UiCheckbox.css +++ b/webapp/packages/core-theming/src/styles/UiCheckbox.css @@ -1,4 +1,3 @@ -@import url('@dbeaver/ui-kit/index.css'); @layer components { body { --dbv-kit-checkbox-background: var(--theme-surface); diff --git a/webapp/packages/core-theming/src/styles/UiIconButton.css b/webapp/packages/core-theming/src/styles/UiIconButton.css new file mode 100644 index 0000000000..a39d1f855a --- /dev/null +++ b/webapp/packages/core-theming/src/styles/UiIconButton.css @@ -0,0 +1,21 @@ +@layer components { + .dbv-kit-icon-button { + --dbv-kit-color-primary-500: var(--theme-primary); + --dbv-kit-icon-btn-outline-color: var(--theme-primary); + + --dbv-kit-icon-btn-primary-foreground: var(--theme-primary); + --dbv-kit-icon-btn-primary-foreground-hover: var(--theme-primary); + --dbv-kit-icon-btn-primary-foreground-active: var(--theme-primary); + --dbv-kit-icon-btn-primary-background: transparent; + --dbv-kit-icon-btn-primary-background-hover: var(--theme-secondary); + --dbv-kit-icon-btn-primary-background-active: var(--theme-background); + + --dbv-kit-icon-btn-secondary-background: transparent; + --dbv-kit-icon-btn-secondary-foreground: var(--theme-on-secondary); + --dbv-kit-icon-btn-secondary-foreground-hover: var(--theme-on-secondary); + --dbv-kit-icon-btn-secondary-foreground-active: var(--theme-on-secondary); + --dbv-kit-icon-btn-secondary-background-hover: var(--theme-secondary, #f2f2f2); + --dbv-kit-icon-btn-secondary-background-active: var(--theme-background); + --dbv-kit-icon-btn-secondary-border-color: transparent; + } +} diff --git a/webapp/packages/core-theming/src/styles/main/light.theme.scss b/webapp/packages/core-theming/src/styles/main/light.theme.scss index ea11be6cc4..c2dab02675 100644 --- a/webapp/packages/core-theming/src/styles/main/light.theme.scss +++ b/webapp/packages/core-theming/src/styles/main/light.theme.scss @@ -8,20 +8,22 @@ @import '../theme-light'; -@include theme { - @include colors; - @include color-scheme; - @include css-variables; - @include ripple-core-styles; - @include theme-ripple; - @include layout-grid; - @include form-controls; - @include button-core-styles; - @include radio-core-styles; - @include switch-core-styles; - @include checkbox-core-styles; - @include link; - @include placeholder; - @include delimiter($mdc-theme-secondary); - @include branding-overflow; -} +@layer base { + @include theme { + @include colors; + @include color-scheme; + @include css-variables; + @include ripple-core-styles; + @include theme-ripple; + @include layout-grid; + @include form-controls; + @include button-core-styles; + @include radio-core-styles; + @include switch-core-styles; + @include checkbox-core-styles; + @include link; + @include placeholder; + @include delimiter($mdc-theme-secondary); + @include branding-overflow; + } +} \ No newline at end of file diff --git a/webapp/packages/core-theming/src/styles/main/normalize.pure.css b/webapp/packages/core-theming/src/styles/main/normalize.pure.css index 9181bfb61f..9ba516029c 100644 --- a/webapp/packages/core-theming/src/styles/main/normalize.pure.css +++ b/webapp/packages/core-theming/src/styles/main/normalize.pure.css @@ -14,7 +14,7 @@ * 1. Correct the line height in all browsers. * 2. Prevent adjustments of font size after orientation changes in iOS. */ - +@layer base { html { line-height: 1.15; /* 1 */ @@ -405,3 +405,4 @@ template { fill: currentColor; overflow: hidden; } +} \ No newline at end of file diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx index f767ed5d03..fcef4babda 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx @@ -38,6 +38,9 @@ import { ResultSetDataSource, ResultSetSelectAction, ResultSetViewAction, + DatabaseDataConstraintAction, + getNextOrder, + isResultSetDataModel, } from '@cloudbeaver/plugin-data-viewer'; import { CellRenderer } from './CellRenderer/CellRenderer.js'; @@ -77,6 +80,7 @@ export const DataGridTable = observer(function DataGridT const selectionAction = (model.source as unknown as ResultSetDataSource).getAction(resultIndex, ResultSetSelectAction); const viewAction = (model.source as unknown as ResultSetDataSource).getAction(resultIndex, ResultSetViewAction); + const constraintsAction = (model.source as unknown as ResultSetDataSource).tryGetAction(resultIndex, DatabaseDataConstraintAction); const tableData = useTableData(model as unknown as IDatabaseDataModel, resultIndex, dataGridDivRef); const gridSelectionContext = useGridSelectionContext(tableData, selectionAction); @@ -205,7 +209,7 @@ export const DataGridTable = observer(function DataGridT return () => { tableData.editor.action.removeHandler(syncEditor); }; - }, [tableData.editor, selectionAction]); + }, [tableData.editor, selectionAction, handlers, tableData]); const handleFocusChange = (position: ICellPosition) => { focusedCell.current = position; @@ -249,7 +253,7 @@ export const DataGridTable = observer(function DataGridT getDataGridApi: () => dataGridRef.current, focus: restoreFocus, }), - [model, actions, resultIndex, simple, dataGridRef, gridContainerRef, restoreFocus], + [model, actions, resultIndex, simple, dataGridRef, restoreFocus], ); const columnsCount = useCreateGridReactiveValue( @@ -334,6 +338,50 @@ export const DataGridTable = observer(function DataGridT [], ); + function getColumnSortable(colIdx: number) { + return ( + Boolean(tableData.getColumn(colIdx) && constraintsAction?.supported && isResultSetDataModel(model) && !model.isDisabled(resultIndex)) && + colIdx !== 0 + ); + } + + const columnSortable = useCreateGridReactiveValue( + getColumnSortable, + (onValueChange, colIdx) => reaction(() => getColumnSortable(colIdx), onValueChange), + [tableData, constraintsAction, model], + ); + + function getColumnSortingState(colIdx: number) { + const column = tableData.getColumn(colIdx)?.key; + if (!column || !constraintsAction) { + return null; + } + const resultColumn = tableData.getColumnInfo(column); + return resultColumn ? constraintsAction?.getOrder(resultColumn.position) : null; + } + + const columnSortingState = useCreateGridReactiveValue( + getColumnSortingState, + (onValueChange, colIdx) => reaction(() => getColumnSortingState(colIdx), onValueChange), + [tableData, constraintsAction, model], + ); + + function handleSort(colIdx: number, order: 'asc' | 'desc' | null, isMultiple: boolean) { + const column = tableData.getColumn(colIdx)?.key; + if (!column || !constraintsAction) { + return; + } + const resultColumn = tableData.getColumnInfo(column); + if (!resultColumn) { + return; + } + const currentOrder = constraintsAction!.getOrder(resultColumn.position); + const nextOrder = getNextOrder(currentOrder); + model.request(() => { + constraintsAction!.setOrder(resultColumn.position, nextOrder, isMultiple); + }); + } + function handleCellChange(rowIdx: number, colIdx: number, value: string) { const row = tableData.rows[rowIdx]; const column = tableData.getColumn(colIdx)?.key; @@ -478,13 +526,16 @@ export const DataGridTable = observer(function DataGridT getHeaderResizable={getHeaderResizable} getRowHeight={() => rowHeight} getColumnKey={getColumnKey} - onCellKeyDown={handleCellKeyDown} columnCount={columnsCount} rowCount={rowsCount} + columnSortable={columnSortable} + columnSortingState={columnSortingState} getRowId={rowIdx => (tableData.rows[rowIdx] ? ResultSetDataKeysUtils.serialize(tableData.rows[rowIdx]) : '')} onFocus={handleFocusChange} onScrollToBottom={handleScrollToBottom} + onColumnSort={handleSort} onCellChange={handleCellChange} + onCellKeyDown={handleCellKeyDown} />
diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/OrderButton.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/OrderButton.tsx deleted file mode 100644 index c3d6cce1dd..0000000000 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/OrderButton.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2025 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 { ActionIconButton, useTranslate } from '@cloudbeaver/core-blocks'; -import { DatabaseDataConstraintAction, EOrder, getNextOrder, type IDatabaseDataModel, ResultSetDataSource } from '@cloudbeaver/plugin-data-viewer'; -import { clsx } from '@dbeaver/ui-kit'; - -interface Props { - model: IDatabaseDataModel; - resultIndex: number; - attributePosition: number; - className?: string; -} - -export const OrderButton = observer(function OrderButton({ model, resultIndex, attributePosition, className }) { - const translate = useTranslate(); - const constraints = model.source.getAction(resultIndex, DatabaseDataConstraintAction); - const currentOrder = constraints.getOrder(attributePosition); - const disabled = model.isDisabled(resultIndex) || model.isLoading(); - - let icon = 'order-arrow-unknown'; - if (currentOrder === EOrder.asc) { - icon = 'order-arrow-asc'; - } else if (currentOrder === EOrder.desc) { - icon = 'order-arrow-desc'; - } - - const handleSort = async (e: React.MouseEvent) => { - e.stopPropagation(); - const nextOrder = getNextOrder(currentOrder); - await model.request(() => { - constraints.setOrder(attributePosition, nextOrder, e.ctrlKey || e.metaKey); - }); - }; - - function preventFocus(event: React.MouseEvent) { - event.preventDefault(); - } - - return ( - - ); -}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx index a10366555e..fa5618b06e 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx @@ -9,13 +9,10 @@ import { observer } from 'mobx-react-lite'; import { useContext } from 'react'; import { getComputed, s, StaticImage, useS } from '@cloudbeaver/core-blocks'; -import type { SqlResultColumn } from '@cloudbeaver/core-sdk'; -import { DatabaseDataConstraintAction, isResultSetDataModel, ResultSetDataSource } from '@cloudbeaver/plugin-data-viewer'; import { DataGridContext } from '../DataGridContext.js'; import { DataGridSelectionContext } from '../DataGridSelection/DataGridSelectionContext.js'; import { TableDataContext } from '../TableDataContext.js'; -import { OrderButton } from './OrderButton.js'; import style from './TableColumnHeader.module.css'; import { useTableColumnDnD } from './useTableColumnDnD.js'; @@ -34,17 +31,10 @@ export const TableColumnHeader = observer(function TableColumnHeader({ co const columnInfo = tableDataContext.getColumn(colIdx)!; const dnd = useTableColumnDnD(model, resultIndex, columnInfo.key); - let constraintsAction: DatabaseDataConstraintAction | undefined; - - if (isResultSetDataModel(model)) { - constraintsAction = (model.source as ResultSetDataSource).tryGetAction(resultIndex, DatabaseDataConstraintAction); - } const dataReadonly = getComputed(() => model.isReadonly(resultIndex)); const hasElementIdentifier = getComputed(() => model.hasElementIdentifier(resultIndex)); - const sortingDisabled = getComputed(() => !constraintsAction?.supported || model.isDisabled(resultIndex)); - let resultColumn: SqlResultColumn | undefined; let icon: string | undefined; let columnName: string | undefined; let columnReadOnly = false; @@ -54,7 +44,6 @@ export const TableColumnHeader = observer(function TableColumnHeader({ co const column = tableDataContext.data.getColumn(columnInfo.key); if (column) { - resultColumn = column; columnName = column.label!; icon = column.icon; columnReadOnly ||= tableDataContext.format.isReadOnly({ column: columnInfo.key }); @@ -96,9 +85,6 @@ export const TableColumnHeader = observer(function TableColumnHeader({ co )}
{columnName}
- {!sortingDisabled && resultColumn && isResultSetDataModel(model) && ( - - )} ); }); diff --git a/webapp/packages/product-base/src/baseConfigurationPlugin.ts b/webapp/packages/product-base/src/baseConfigurationPlugin.ts index 97766a29bc..97997a5e06 100644 --- a/webapp/packages/product-base/src/baseConfigurationPlugin.ts +++ b/webapp/packages/product-base/src/baseConfigurationPlugin.ts @@ -84,6 +84,8 @@ export function baseConfigurationPlugin(mode: string, packageJson: any): PluginO modulePreload: false, /* We need to disable css splitting because of the issue with loading CSS for some chunks (dbeaver/pro#5599), can be removed when dbeaver/pro#5204 is done */ cssCodeSplit: false, + /* Imported or referenced assets that are smaller than this threshold will be inlined as base64 URLs to avoid extra http requests. Set to 0 to disable inlining altogether. */ + assetsInlineLimit: 0, rollupOptions: { ...config.build?.rollupOptions, diff --git a/webapp/packages/product-default/src/styles.css b/webapp/packages/product-default/src/styles.css index 910080db92..c16b414fdc 100644 --- a/webapp/packages/product-default/src/styles.css +++ b/webapp/packages/product-default/src/styles.css @@ -1,2 +1,2 @@ -@import 'tailwindcss' prefix(tw); -@custom-variant dark (&:where(.theme-dark, .theme-dark *)); \ No newline at end of file +@import 'tailwindcss' layer(base) prefix(tw); +@custom-variant dark (&:where(.theme-dark, .theme-dark *)); diff --git a/webapp/yarn.lock b/webapp/yarn.lock index b76cca1ffa..9d53b9e37e 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -1427,6 +1427,7 @@ __metadata: "@cloudbeaver/core-utils": "workspace:*" "@cloudbeaver/core-version": "workspace:*" "@cloudbeaver/tsconfig": "workspace:*" + "@dbeaver/ui-kit": "workspace:^" "@types/react": "npm:^19" mobx: "npm:^6" mobx-react-lite: "npm:^4"