Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ 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';

export const CellFormatterFactory = observer<ICellFormatterProps>(function CellFormatterFactory(props) {
const formatterRef = useRef<React.FC<ICellFormatterProps> | null>(null);
Expand All @@ -38,6 +40,17 @@ export const CellFormatterFactory = observer<ICellFormatterProps>(function CellF
if (resultColumn && isBooleanValuePresentationAvailable(holder.value, resultColumn)) {
formatterRef.current = BooleanFormatter;
}

if (tableDataContext.useUserFormatting) {
switch (resultColumn?.dataKind?.toUpperCase()) {
case 'DATETIME':
formatterRef.current = DateTimeFormatter;
break;
case 'NUMERIC':
formatterRef.current = NumberFormatter;
break;
}
}
}
} else {
formatterRef.current = IndexFormatter;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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.
*/

.dateFormatter {
display: flex;
align-items: center;
overflow: hidden;
}

.dateFormatterValue {
overflow: hidden;
text-overflow: ellipsis;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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 { useContext } from 'react';

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

import { CellContext } from '../../CellRenderer/CellContext.js';
import { TableDataContext, DateTimeKind } from '../../TableDataContext.js';
import styles from './DateTimeFormatter.module.css';
import type { ICellFormatterProps } from '../ICellFormatterProps.js';

export const DateTimeFormatter = observer<ICellFormatterProps>(function DateTimeFormatter() {
const tableDataContext = useContext(TableDataContext);
const cellContext = useContext(CellContext);
const style = useS(styles);

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;

const extendedDateKind = tableDataContext.getExtendedDateKind(cellContext.cell.column);

let dateFormatter;
switch (extendedDateKind) {
case DateTimeKind.DateTime:
case DateTimeKind.TimeOnly:
dateFormatter = tableDataContext.useUserFormatting!.dateTime;
break;
case DateTimeKind.DateOnly:
dateFormatter = tableDataContext.useUserFormatting!.dateOnly;
break;
}
const date = new Date(displayValue);
value = dateFormatter.format(date);

return (
<div className={s(style, { dateFormatter: true })}>
<div className={s(style, { dateFormatterValue: true })}>{value}</div>
</div>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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.
*/

.numberFormatter {
display: flex;
align-items: center;
overflow: hidden;
}

.numberFormatterValue {
overflow: hidden;
text-overflow: ellipsis;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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 { useContext } from 'react';

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

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

import styles from './NumberFormatter.module.css';

export const NumberFormatter = observer<ICellFormatterProps>(function NumberFormatter() {
const tableDataContext = useContext(TableDataContext);
const cellContext = useContext(CellContext);
const style = useS(styles);

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 (tableDataContext.useUserFormatting) {
const numberValue = Number(displayValue);

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

return (
<div className={s(style, { numberFormatter: true })}>
<div className={s(style, { numberFormatterValue: true })}>{value}</div>
</div>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,23 @@ import type {
} from '@cloudbeaver/plugin-data-viewer';
import type { GridConditionalFormattingAction } from '@cloudbeaver/plugin-data-viewer-conditional-formatting';

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

export interface IColumnInfo {
key: IGridColumnKey | null;
}

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

export interface ITableData {
formatting: GridConditionalFormattingAction;
format: IDatabaseDataFormatAction<Partial<IGridDataKey>, IResultSetValue, IDatabaseResultSet>;
Expand All @@ -40,6 +53,7 @@ export interface ITableData {
columnKeys: IGridColumnKey[];
rows: IGridRowKey[];
gridDiv: HTMLDivElement | null;
useUserFormatting: IDataGridFormatters | null;
inBounds: (position: IGridDataKey) => boolean;
getRow: (rowIndex: number) => IGridRowKey | undefined;
getColumn: (columnIndex: number) => IColumnInfo | undefined;
Expand All @@ -54,6 +68,7 @@ export interface ITableData {
isIndexColumn: (columnKey: IColumnInfo) => boolean;
isIndexColumnInRange: (columnsRange: Array<IColumnInfo>) => boolean;
isCellReadonly: (key: IGridDataKey) => boolean;
getExtendedDateKind: (columnKey: IGridColumnKey) => DateTimeKind;
}

export function isColumnInfo(column: IColumnInfo | undefined): column is IColumnInfo {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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 { describe, expect, test } from 'vitest';

import { DateTimeKind } from '../TableDataContext.js';
import { detectDateTimeKind } from './detectDateTimeKind.js';

describe('detectDateTimeKind', () => {
test('should detect date-only format (YYYY-MM-DD)', () => {
expect(detectDateTimeKind('2025-12-29')).toBe(DateTimeKind.DateOnly);
expect(detectDateTimeKind('2024-01-01')).toBe(DateTimeKind.DateOnly);
expect(detectDateTimeKind('1999-12-31')).toBe(DateTimeKind.DateOnly);
});

test('should detect time-only format (HH:MM:SS)', () => {
expect(detectDateTimeKind('14:30:00')).toBe(DateTimeKind.TimeOnly);
expect(detectDateTimeKind('00:00:00')).toBe(DateTimeKind.TimeOnly);
expect(detectDateTimeKind('23:59:59')).toBe(DateTimeKind.TimeOnly);
expect(detectDateTimeKind('14:30:00.123')).toBe(DateTimeKind.TimeOnly);
});

test('should detect datetime format', () => {
expect(detectDateTimeKind('2025-12-29 14:30:00')).toBe(DateTimeKind.DateTime);
expect(detectDateTimeKind('2024-01-01 00:00:00')).toBe(DateTimeKind.DateTime);
expect(detectDateTimeKind('2025-12-29T14:30:00')).toBe(DateTimeKind.DateTime);
expect(detectDateTimeKind('2025-12-29T14:30:00.123Z')).toBe(DateTimeKind.DateTime);
});

test('should default to DateTime for unrecognized formats', () => {
expect(detectDateTimeKind('')).toBe(DateTimeKind.DateTime);
expect(detectDateTimeKind('invalid')).toBe(DateTimeKind.DateTime);
expect(detectDateTimeKind('2025/12/29')).toBe(DateTimeKind.DateTime);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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 { DateTimeKind } from '../TableDataContext.js';

export function detectDateTimeKind(displayValue: string): DateTimeKind {
if (/^\d{4}-\d{2}-\d{2}$/.test(displayValue)) {
return DateTimeKind.DateOnly;
}

if (/^\d{2}:\d{2}:\d{2}/.test(displayValue) && !displayValue.includes('-')) {
return DateTimeKind.TimeOnly;
}

return DateTimeKind.DateTime;
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,17 @@ import {
type IGridDataKey,
} from '@cloudbeaver/plugin-data-viewer';

import type { IColumnInfo, ITableData } from './TableDataContext.js';
import { type IColumnInfo, type ITableData, type IDataGridFormatters, DateTimeKind } from './TableDataContext.js';
import { useService } from '@cloudbeaver/core-di';
import { DataGridSettingsService } from '../DataGridSettingsService.js';
import { DataGridSettingsService, NO_FORMAT, OS_FORMAT } from '../DataGridSettingsService.js';
import type { SqlResultColumn } from '@cloudbeaver/core-sdk';
import { GridConditionalFormattingAction } from '@cloudbeaver/plugin-data-viewer-conditional-formatting';
import { detectDateTimeKind } from './helpers/detectDateTimeKind.js';

interface ITableDataPrivate extends ITableData {
dataGridSettingsService: DataGridSettingsService;
gridDIVElement: React.RefObject<HTMLDivElement | null>;
extendedDateKinds: Map<number, DateTimeKind>;
}

export function useTableData(
Expand Down Expand Up @@ -83,6 +85,33 @@ export function useTableData(
// TODO: fix column abstraction
return Boolean(this.data?.columns?.some(column => (column as SqlResultColumn).description));
},
get useUserFormatting(): IDataGridFormatters | null {
const setting = this.dataGridSettingsService.useUserFormatting;

if (setting === NO_FORMAT) {
return null;
}

const locale = setting === OS_FORMAT ? new Intl.DateTimeFormat().resolvedOptions().locale : setting;
return {
locale,
dateTime: new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
}),
dateOnly: new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'numeric',
day: 'numeric',
timeZone: 'UTC',
}),
number: new Intl.NumberFormat(locale),
};
},
getRow(rowIndex) {
return this.rows[rowIndex];
},
Expand Down Expand Up @@ -140,19 +169,42 @@ export function useTableData(

return model.isReadonly(resultIndex) || (this.format.isReadOnly(key) && this.editor?.getElementState(key) !== DatabaseEditChangeType.add);
},
getExtendedDateKind(columnKey: IGridColumnKey): DateTimeKind {
if (this.extendedDateKinds.has(columnKey.index)) {
return this.extendedDateKinds.get(columnKey.index) as DateTimeKind;
}
Copy link
Member

Choose a reason for hiding this comment

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

probably you want to use ResultSetCacheAction

Copy link
Contributor Author

Choose a reason for hiding this comment

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

looks like an over-engineering to me since we don't need cell or row-level caching here, only the cache by columns

Copy link
Member

Choose a reason for hiding this comment

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

we need to add getColumn like we have getRow in it, it's better because it will clear cache when needed and place it in one place


for (const row of this.rows) {
const cellKey = { column: columnKey, row };
const holder = this.getCellHolder(cellKey);

if (!this.format.isNull(holder)) {
const displayValue = this.format.getDisplayString(holder);
const kind = detectDateTimeKind(displayValue);
this.extendedDateKinds.set(columnKey.index, kind);
return kind;
}
}

const defaultKind = DateTimeKind.DateTime;
this.extendedDateKinds.set(columnKey.index, defaultKind);
return defaultKind;
},
}),
{
columns: computed,
rows: computed,
columnKeys: computed,
hasDescription: computed,
useUserFormatting: computed,
formatting: observable.ref,
format: observable.ref,
dataContent: observable.ref,
data: observable.ref,
editor: observable.ref,
view: observable.ref,
gridDIVElement: observable.ref,
extendedDateKinds: observable,
},
{
formatting,
Expand All @@ -163,6 +215,7 @@ export function useTableData(
view,
gridDIVElement,
dataGridSettingsService,
extendedDateKinds: new Map(),
},
);
}
Loading
Loading