Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
95ff5eb
dbeaver/pro#7855 feat: Add setting to use the user's OS formatting fo…
SychevAndrey Dec 24, 2025
880ff06
dbeaver/pro#7855 refactor: update OS formatting option value in DataG…
SychevAndrey Dec 25, 2025
507f577
dbeaver/pro#7855 feat: implement OS formatting for date and time in D…
SychevAndrey Dec 25, 2025
cf92bab
dbeaver/pro#7855 feat: implement user-specific formatting for numbers…
SychevAndrey Dec 25, 2025
96a044d
dbeaver/pro#7855 feat: add translations
SychevAndrey Dec 25, 2025
94f5493
dbeaver/pro#7855 style: update date formatter
SychevAndrey Dec 25, 2025
48b74d8
dbeaver/pro#7855 feat: add locale-specific formatting options for Dat…
SychevAndrey Dec 25, 2025
fa54ba5
dbeaver/pro#7855 feat: add support for locale-specific formatting wit…
SychevAndrey Dec 26, 2025
efdac7c
dbeaver/pro#7855 refactor: move date type detection to the grid level
SychevAndrey Dec 29, 2025
27240c5
dbeaver/pro#7855 refactor: simplify cell formatter selection logic
SychevAndrey Jan 5, 2026
f391586
Merge branch 'devel' into 7855-cb-add-setting---use-the-users-os-form…
SychevAndrey Jan 5, 2026
9902a6e
Merge branch '7855-cb-add-setting---use-the-users-os-formatting-for-n…
SychevAndrey Jan 5, 2026
543c687
dbeaver/pro#7855 refactor: move formatting to a separate context
SychevAndrey Jan 5, 2026
01b1054
dbeaver/pro#7855 refactor: optimize extended date kind retrieval logic
SychevAndrey Jan 6, 2026
7ce2168
Merge branch 'devel' into 7855-cb-add-setting---use-the-users-os-form…
SychevAndrey Jan 6, 2026
bf3dc5e
dbeaver/pro#7855 fix: tw class names
SychevAndrey Jan 6, 2026
af2257b
Merge branch 'devel' into 7855-cb-add-setting---use-the-users-os-form…
SychevAndrey Jan 8, 2026
c3694e7
Merge branch 'devel' into 7855-cb-add-setting---use-the-users-os-form…
SychevAndrey Jan 8, 2026
3966098
dbeaver/pro#7855 refactor: change caching mechanism and other improve…
SychevAndrey Jan 9, 2026
167de1e
Merge branch 'devel' into 7855-cb-add-setting---use-the-users-os-form…
mr-anton-t Jan 9, 2026
7c82e1a
Merge branch 'devel' into 7855-cb-add-setting---use-the-users-os-form…
mr-anton-t Jan 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion webapp/common-typescript/@dbeaver/js-helpers/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
Expand All @@ -13,3 +13,4 @@ export * from './isNotNullDefined.js';
export * from './memoizeLast.js';
export * from './mutex.js';
export * from './reorderArray.js';
export * from './getLocalizedDisplayName.js';
1 change: 1 addition & 0 deletions webapp/packages/plugin-data-spreadsheet-new/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@cloudbeaver/plugin-data-grid": "workspace:*",
"@cloudbeaver/plugin-data-viewer": "workspace:*",
"@cloudbeaver/plugin-data-viewer-conditional-formatting": "workspace:*",
"@dbeaver/js-helpers": "workspace:*",
"@dbeaver/result-set-api": "workspace:*",
"@dbeaver/ui-kit": "workspace:*",
"mobx": "^6",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -34,11 +34,13 @@ import {
ResultSetDataSource,
getNextOrder,
isResultSetDataModel,
IDatabaseDataCacheAction,
IDatabaseDataSelectAction,
IDatabaseDataViewAction,
IDatabaseDataConstraintAction,
GridSelectAction,
GridViewAction,
ResultSetCacheAction,
type IGridEditActionData,
type IGridDataKey,
} from '@cloudbeaver/plugin-data-viewer';
Expand All @@ -49,8 +51,10 @@ import { DataGridSelectionContext } from './DataGridSelection/DataGridSelectionC
import { useGridSelectionContext } from './DataGridSelection/useGridSelectionContext.js';
import './DataGridTable.css';
import { CellFormatter } from './Formatters/CellFormatter.js';
import { FormattingContext } from './FormattingContext.js';
import { TableDataContext } from './TableDataContext.js';
import { useGridDragging } from './useGridDragging.js';
import { useFormatting } from './useFormatting.js';
import { useGridSelectedCellsCopy } from './useGridSelectedCellsCopy.js';
import { useTableData } from './useTableData.js';
import { TableColumnHeader } from './TableColumnHeader/TableColumnHeader.js';
Expand All @@ -61,15 +65,7 @@ const ROW_HEIGHT = 24;
export const HEADER_HEIGHT = 32;
export const HEADER_WITH_DESC_HEIGHT = 42;

export const DataGridTable = observer<IDataPresentationProps>(function DataGridTable({
model,
actions,
resultIndex,
simple,
className,
dataFormat,
...rest
}) {
export const DataGridTable = observer<IDataPresentationProps>(function DataGridTable({ model, actions, resultIndex, simple, className, ...rest }) {
const translate = useTranslate();
const gridContainerRef = useRef<HTMLDivElement | null>(null);
const dataGridDivRef = useRef<HTMLDivElement | null>(null);
Expand All @@ -79,8 +75,10 @@ export const DataGridTable = observer<IDataPresentationProps>(function DataGridT

const selectionAction = model.source.getAction(resultIndex, IDatabaseDataSelectAction, GridSelectAction);
const viewAction = model.source.getAction(resultIndex, IDatabaseDataViewAction, GridViewAction);
const cacheAction = model.source.getAction(resultIndex, IDatabaseDataCacheAction, ResultSetCacheAction);

const tableData = useTableData(model as unknown as IDatabaseDataModel<ResultSetDataSource>, resultIndex, dataGridDivRef);
const formatting = useFormatting(tableData, cacheAction);
const getHeaderOrder = useCallback(() => (dataGridRef.current?.getColumnsOrdered() ?? []).map(col => col.key), [dataGridRef]);
const gridSelectionContext = useGridSelectionContext(tableData, selectionAction, getHeaderOrder);

Expand Down Expand Up @@ -497,43 +495,45 @@ export const DataGridTable = observer<IDataPresentationProps>(function DataGridT
<DataGridContext.Provider value={gridContext}>
<DataGridSelectionContext.Provider value={gridSelectionContext}>
<TableDataContext.Provider value={tableData}>
<div
ref={setContainersRef}
tabIndex={-1}
{...rest}
className={clsx('data-grid__container', 'theme-typography--caption', className)}
onMouseDown={onMouseDownHandler}
onMouseMove={onMouseMoveHandler}
>
<DataGrid
ref={dataGridRef}
className={clsx('data-grid__grid', className)}
cell={cell}
cellText={cellText}
cellElement={cellElement}
rowElement={rowElement}
getCellEditable={isCellEditable}
headerElement={headerElement}
getHeaderHeight={() => headerHeight}
getHeaderWidth={getHeaderWidth}
getHeaderPinned={getHeaderPinned}
getHeaderResizable={getHeaderResizable}
getRowHeight={() => ROW_HEIGHT}
getColumnKey={getColumnKey}
columnCount={columnsCount}
rowCount={rowsCount}
columnSortable={columnSortable}
columnSortingState={columnSortingState}
getRowId={rowIdx => (tableData.rows[rowIdx] ? GridDataKeysUtils.serialize(tableData.rows[rowIdx]) : '')}
columnSortingMultiple
onFocus={handleFocusChange}
onScrollToBottom={handleScrollToBottom}
onColumnSort={handleSort}
onCellChange={handleCellChange}
onCellKeyDown={handleCellKeyDown}
onHeaderKeyDown={gridSelectedCellCopy.onKeydownHandler}
/>
</div>
<FormattingContext.Provider value={formatting}>
<div
ref={setContainersRef}
tabIndex={-1}
{...rest}
className={clsx('data-grid__container', 'theme-typography--caption', className)}
onMouseDown={onMouseDownHandler}
onMouseMove={onMouseMoveHandler}
>
<DataGrid
ref={dataGridRef}
className={clsx('data-grid__grid', className)}
cell={cell}
cellText={cellText}
cellElement={cellElement}
rowElement={rowElement}
getCellEditable={isCellEditable}
headerElement={headerElement}
getHeaderHeight={() => headerHeight}
getHeaderWidth={getHeaderWidth}
getHeaderPinned={getHeaderPinned}
getHeaderResizable={getHeaderResizable}
getRowHeight={() => ROW_HEIGHT}
getColumnKey={getColumnKey}
columnCount={columnsCount}
rowCount={rowsCount}
columnSortable={columnSortable}
columnSortingState={columnSortingState}
getRowId={rowIdx => (tableData.rows[rowIdx] ? GridDataKeysUtils.serialize(tableData.rows[rowIdx]) : '')}
columnSortingMultiple
onFocus={handleFocusChange}
onScrollToBottom={handleScrollToBottom}
onColumnSort={handleSort}
onCellChange={handleCellChange}
onCellKeyDown={handleCellKeyDown}
onHeaderKeyDown={gridSelectedCellCopy.onKeydownHandler}
/>
</div>
</FormattingContext.Provider>
</TableDataContext.Provider>
</DataGridSelectionContext.Provider>
</DataGridContext.Provider>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,83 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
* Copyright (C) 2020-2026 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 { useContext, useRef } from 'react';

import { isBooleanValuePresentationAvailable } from '@cloudbeaver/plugin-data-viewer';
import { isBooleanValuePresentationAvailable, type IGridDataKey } from '@cloudbeaver/plugin-data-viewer';

import { CellContext } from '../CellRenderer/CellContext.js';
import { TableDataContext } from '../TableDataContext.js';
import { useFormattingContext } from '../FormattingContext.js';
import { TableDataContext, type ITableData } from '../TableDataContext.js';
import { BlobFormatter } from './CellFormatters/BlobFormatter.js';
import { BooleanFormatter } from './CellFormatters/BooleanFormatter.js';
import { TextFormatter } from './CellFormatters/TextFormatter.js';
import type { ICellFormatterProps } from './ICellFormatterProps.js';
import { IndexFormatter } from './IndexFormatter.js';
import { DateTimeFormatter } from './CellFormatters/DateTimeFormatter.js';
import { NumberFormatter } from './CellFormatters/NumberFormatter.js';

interface IFormatterContext {
tableDataContext: ITableData;
hasFormatters: boolean;
holder: ReturnType<ITableData['getCellHolder']>;
resultColumn: ReturnType<ITableData['getColumnInfo']>;
}

type FormatterSelector = (context: IFormatterContext, cell: IGridDataKey) => React.FC<ICellFormatterProps> | null;

const formatterSelectors: FormatterSelector[] = [
// Binary
context => (context.tableDataContext.format.isBinary(context.holder) ? BlobFormatter : null),

// Boolean
context => (context.resultColumn && isBooleanValuePresentationAvailable(context.holder.value, context.resultColumn) ? BooleanFormatter : null),

// DateTime
context => {
if (!context.hasFormatters) {
return null;
}
return context.resultColumn?.dataKind?.toUpperCase() === 'DATETIME' ? DateTimeFormatter : null;
},

// Numeric
context => {
if (!context.hasFormatters) {
return null;
}
return context.resultColumn?.dataKind?.toUpperCase() === 'NUMERIC' ? NumberFormatter : null;
},
];

export const CellFormatterFactory = observer<ICellFormatterProps>(function CellFormatterFactory(props) {
const formatterRef = useRef<React.FC<ICellFormatterProps> | null>(null);
const tableDataContext = useContext(TableDataContext);
const formattingContext = useFormattingContext();
const cellContext = useContext(CellContext);

if (formatterRef.current === null) {
formatterRef.current = TextFormatter;

if (cellContext.cell) {
const holder = tableDataContext.getCellHolder(cellContext.cell);
const isBlob = tableDataContext.format.isBinary(holder);

if (isBlob) {
formatterRef.current = BlobFormatter;
} else {
const resultColumn = tableDataContext.getColumnInfo(cellContext.cell.column);
const resultColumn = tableDataContext.getColumnInfo(cellContext.cell.column);
const context: IFormatterContext = {
tableDataContext,
hasFormatters: formattingContext.formatters !== null,
holder,
resultColumn,
};

if (resultColumn && isBooleanValuePresentationAvailable(holder.value, resultColumn)) {
formatterRef.current = BooleanFormatter;
for (const selector of formatterSelectors) {
const formatter = selector(context, cellContext.cell);
if (formatter) {
formatterRef.current = formatter;
break;
}
}
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2026 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 { useContext } from 'react';

import { getComputed } from '@cloudbeaver/core-blocks';
import { NullFormatter as GridNullFormatter } from '@cloudbeaver/plugin-data-grid';

import { CellContext } from '../../CellRenderer/CellContext.js';
import { DateTimeKind, useFormattingContext } from '../../FormattingContext.js';
import { TableDataContext } from '../../TableDataContext.js';
import type { ICellFormatterProps } from '../ICellFormatterProps.js';

export const DateTimeFormatter = observer<ICellFormatterProps>(function DateTimeFormatter() {
const tableDataContext = useContext(TableDataContext);
const formattingContext = useFormattingContext();
const cellContext = useContext(CellContext);

if (!cellContext.cell) {
return null;
}

const formatter = tableDataContext.format;
const valueHolder = getComputed(() => formatter.get(cellContext.cell!));
const nullValue = getComputed(() => formatter.isNull(valueHolder));
const displayValue = getComputed(() => formatter.getDisplayString(valueHolder));

if (nullValue) {
return <GridNullFormatter />;
}

let value = displayValue;

if (formattingContext.formatters) {
const extendedDateKind = formattingContext.getExtendedDateKind(cellContext.cell.column);

let dateFormatter: Intl.DateTimeFormat | null = null;
switch (extendedDateKind) {
case DateTimeKind.DateTime:
case DateTimeKind.TimeOnly:
dateFormatter = formattingContext.formatters.dateTime;
break;
case DateTimeKind.DateOnly:
dateFormatter = formattingContext.formatters.dateOnly;
break;
}
if (dateFormatter) {
const date = new Date(displayValue);
value = dateFormatter.format(date);
}
}

return (
<div className="tw:flex tw:items-center tw:overflow-hidden">
<div className="tw:overflow-hidden tw:text-ellipsis">{value}</div>
</div>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2026 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 { useContext } from 'react';

import { getComputed } from '@cloudbeaver/core-blocks';
import { NullFormatter as GridNullFormatter } from '@cloudbeaver/plugin-data-grid';

import { CellContext } from '../../CellRenderer/CellContext.js';
import { useFormattingContext } from '../../FormattingContext.js';
import { TableDataContext } from '../../TableDataContext.js';
import type { ICellFormatterProps } from '../ICellFormatterProps.js';

export const NumberFormatter = observer<ICellFormatterProps>(function NumberFormatter() {
const tableDataContext = useContext(TableDataContext);
const formattingContext = useFormattingContext();
const cellContext = useContext(CellContext);

if (!cellContext.cell) {
return null;
}

const formatter = tableDataContext.format;
const valueHolder = getComputed(() => formatter.get(cellContext.cell!));
const nullValue = getComputed(() => formatter.isNull(valueHolder));
const displayValue = getComputed(() => formatter.getDisplayString(valueHolder));

if (nullValue) {
return <GridNullFormatter />;
}

let value = displayValue;

if (formattingContext.formatters) {
const numberValue = Number(displayValue);

if (!isNaN(numberValue) && displayValue.trim() !== '') {
value = formattingContext.formatters.number.format(numberValue);
}
}

return (
<div className="tw:flex tw:items-center tw:overflow-hidden">
<div className="tw:overflow-hidden tw:text-ellipsis">{value}</div>
</div>
);
});
Loading
Loading