Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 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 @@ -49,8 +49,10 @@
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 @@ -67,7 +69,7 @@
resultIndex,
simple,
className,
dataFormat,

Check warning on line 72 in webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx

View workflow job for this annotation

GitHub Actions / Frontend / Lint

'dataFormat' is defined but never used
...rest
}) {
const translate = useTranslate();
Expand All @@ -81,6 +83,7 @@
const viewAction = model.source.getAction(resultIndex, IDatabaseDataViewAction, GridViewAction);

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

Expand Down Expand Up @@ -497,43 +500,45 @@
<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,86 @@
/*
* 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 { FormattingContext } 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;
}

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

const formatterSelectors: FormatterSelector[] = [
// Binary
(context, cell) => {
const holder = context.tableDataContext.getCellHolder(cell);
return context.tableDataContext.format.isBinary(holder) ? BlobFormatter : null;
},

// Boolean
(context, cell) => {
const holder = context.tableDataContext.getCellHolder(cell);
const resultColumn = context.tableDataContext.getColumnInfo(cell.column);
return resultColumn && isBooleanValuePresentationAvailable(holder.value, resultColumn) ? BooleanFormatter : null;
},

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

// Numeric
(context, cell) => {
if (!context.hasFormatters) {
return null;
}
const resultColumn = context.tableDataContext.getColumnInfo(cell.column);
return 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 = useContext(FormattingContext);
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 context: IFormatterContext = {
tableDataContext,
hasFormatters: formattingContext.formatters !== null,
};

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,61 @@
/*
* 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, FormattingContext } 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 = useContext(FormattingContext);
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;
switch (extendedDateKind) {
case DateTimeKind.DateTime:
case DateTimeKind.TimeOnly:
dateFormatter = formattingContext.formatters.dateTime;
break;
case DateTimeKind.DateOnly:
dateFormatter = formattingContext.formatters.dateOnly;
break;
}
const date = new Date(displayValue);
value = dateFormatter!.format(date);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add default formatter here? which is gonna just display something as it is without formatting

cause now this line can break the code:
value = dateFormatter!.format(date);

better to have default switch case to prevent that

}

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 { FormattingContext } 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 = useContext(FormattingContext);
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>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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 { createContext } from 'react';

import type { IGridColumnKey } from '@cloudbeaver/plugin-data-viewer';

export enum DateTimeKind {
DateTime = 'DATETIME',
DateOnly = 'DATE',
TimeOnly = 'TIME',
}

export interface IDataGridFormatters {
locale: string;
dateTime: Intl.DateTimeFormat;
dateOnly: Intl.DateTimeFormat;
number: Intl.NumberFormat;
}

export interface IFormattingContext {
formatters: IDataGridFormatters | null;
getExtendedDateKind: (columnKey: IGridColumnKey) => DateTimeKind;
}

export const FormattingContext = createContext<IFormattingContext>(undefined as any);
Loading
Loading