diff --git a/webapp/common-typescript/@dbeaver/js-helpers/src/index.ts b/webapp/common-typescript/@dbeaver/js-helpers/src/index.ts index 04bfb692a18..f362b1caa00 100644 --- a/webapp/common-typescript/@dbeaver/js-helpers/src/index.ts +++ b/webapp/common-typescript/@dbeaver/js-helpers/src/index.ts @@ -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. @@ -13,3 +13,4 @@ export * from './isNotNullDefined.js'; export * from './memoizeLast.js'; export * from './mutex.js'; export * from './reorderArray.js'; +export * from './getLocalizedDisplayName.js'; diff --git a/webapp/packages/plugin-data-spreadsheet-new/package.json b/webapp/packages/plugin-data-spreadsheet-new/package.json index ba49589ad29..cd475414968 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/package.json +++ b/webapp/packages/plugin-data-spreadsheet-new/package.json @@ -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", 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 e0888c6dd70..41d572db1b0 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx @@ -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. @@ -34,11 +34,13 @@ import { ResultSetDataSource, getNextOrder, isResultSetDataModel, + IDatabaseDataCacheAction, IDatabaseDataSelectAction, IDatabaseDataViewAction, IDatabaseDataConstraintAction, GridSelectAction, GridViewAction, + ResultSetCacheAction, type IGridEditActionData, type IGridDataKey, } from '@cloudbeaver/plugin-data-viewer'; @@ -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'; @@ -61,15 +65,7 @@ const ROW_HEIGHT = 24; export const HEADER_HEIGHT = 32; export const HEADER_WITH_DESC_HEIGHT = 42; -export const DataGridTable = observer(function DataGridTable({ - model, - actions, - resultIndex, - simple, - className, - dataFormat, - ...rest -}) { +export const DataGridTable = observer(function DataGridTable({ model, actions, resultIndex, simple, className, ...rest }) { const translate = useTranslate(); const gridContainerRef = useRef(null); const dataGridDivRef = useRef(null); @@ -79,8 +75,10 @@ export const DataGridTable = observer(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, resultIndex, dataGridDivRef); + const formatting = useFormatting(tableData, cacheAction); const getHeaderOrder = useCallback(() => (dataGridRef.current?.getColumnsOrdered() ?? []).map(col => col.key), [dataGridRef]); const gridSelectionContext = useGridSelectionContext(tableData, selectionAction, getHeaderOrder); @@ -497,43 +495,45 @@ export const DataGridTable = observer(function DataGridT -
- 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} - /> -
+ +
+ 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} + /> +
+
diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatterFactory.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatterFactory.tsx index b28ce872367..a07d44ffb6f 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatterFactory.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatterFactory.tsx @@ -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. @@ -8,19 +8,56 @@ 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; + resultColumn: ReturnType; +} + +type FormatterSelector = (context: IFormatterContext, cell: IGridDataKey) => React.FC | 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(function CellFormatterFactory(props) { const formatterRef = useRef | null>(null); const tableDataContext = useContext(TableDataContext); + const formattingContext = useFormattingContext(); const cellContext = useContext(CellContext); if (formatterRef.current === null) { @@ -28,15 +65,19 @@ export const CellFormatterFactory = observer(function CellF 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 { diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/DateTimeFormatter.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/DateTimeFormatter.tsx new file mode 100644 index 00000000000..27b106af5bf --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/DateTimeFormatter.tsx @@ -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(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 ; + } + + 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 ( +
+
{value}
+
+ ); +}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/NumberFormatter.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/NumberFormatter.tsx new file mode 100644 index 00000000000..3909c4018ec --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/NumberFormatter.tsx @@ -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(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 ; + } + + let value = displayValue; + + if (formattingContext.formatters) { + const numberValue = Number(displayValue); + + if (!isNaN(numberValue) && displayValue.trim() !== '') { + value = formattingContext.formatters.number.format(numberValue); + } + } + + return ( +
+
{value}
+
+ ); +}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/FormattingContext.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/FormattingContext.ts new file mode 100644 index 00000000000..cc77772588d --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/FormattingContext.ts @@ -0,0 +1,40 @@ +/* + * 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, useContext } 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(null); + +export function useFormattingContext(): IFormattingContext { + const context = useContext(FormattingContext); + + if (!context) { + throw new Error('FormattingContext is required'); + } + + return context; +} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/helpers/detectDateTimeKind.test.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/helpers/detectDateTimeKind.test.ts new file mode 100644 index 00000000000..958a095dd37 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/helpers/detectDateTimeKind.test.ts @@ -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 '../FormattingContext.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); + }); +}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/helpers/detectDateTimeKind.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/helpers/detectDateTimeKind.ts new file mode 100644 index 00000000000..09343e4cf62 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/helpers/detectDateTimeKind.ts @@ -0,0 +1,24 @@ +/* + * 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 { DateTimeKind } from '../FormattingContext.js'; + +const DATE_ONLY_REGEX = /^\d{4}-\d{2}-\d{2}$/; +const TIME_ONLY_REGEX = /^\d{2}:\d{2}:\d{2}/; + +export function detectDateTimeKind(displayValue: string): DateTimeKind { + if (DATE_ONLY_REGEX.test(displayValue)) { + return DateTimeKind.DateOnly; + } + + if (TIME_ONLY_REGEX.test(displayValue) && !displayValue.includes('-')) { + return DateTimeKind.TimeOnly; + } + + return DateTimeKind.DateTime; +} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useFormatting.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useFormatting.ts new file mode 100644 index 00000000000..04b68401ada --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useFormatting.ts @@ -0,0 +1,95 @@ +/* + * 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 { computed, observable } from 'mobx'; + +import { useObservableRef } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import type { IGridColumnKey, ResultSetCacheAction } from '@cloudbeaver/plugin-data-viewer'; + +import { DataGridSettingsService } from '../DataGridSettingsService.js'; +import { detectDateTimeKind } from './helpers/detectDateTimeKind.js'; +import { DateTimeKind, type IDataGridFormatters, type IFormattingContext } from './FormattingContext.js'; +import type { ITableData } from './TableDataContext.js'; + +const EXTENDED_DATE_KIND_CACHE = Symbol('data-grid-extended-date-kind'); + +interface IFormattingContextPrivate extends IFormattingContext { + dataGridSettingsService: DataGridSettingsService; + cache: ResultSetCacheAction; + tableData: ITableData; +} + +export function useFormatting(tableData: ITableData, cache: ResultSetCacheAction): IFormattingContext { + const dataGridSettingsService = useService(DataGridSettingsService); + + return useObservableRef( + () => ({ + get formatters(): IDataGridFormatters | null { + const locale = this.dataGridSettingsService.getFormatLocale(); + + if (locale === null) { + return null; + } + + 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), + }; + }, + getExtendedDateKind(columnKey: IGridColumnKey): DateTimeKind { + const cached = this.cache.getColumn(columnKey, EXTENDED_DATE_KIND_CACHE); + + if (cached !== undefined) { + return cached; + } + + let kind = DateTimeKind.DateTime; + const rows = this.tableData.rows; + + for (const row of rows) { + const cellKey = { column: columnKey, row }; + const holder = this.tableData.getCellHolder(cellKey); + + if (!this.tableData.format.isNull(holder)) { + const displayValue = this.tableData.format.getDisplayString(holder); + kind = detectDateTimeKind(displayValue); + break; + } + } + + this.cache.setColumn(columnKey, EXTENDED_DATE_KIND_CACHE, kind); + + return kind; + }, + }), + { + formatters: computed, + cache: observable.ref, + tableData: observable.ref, + }, + { + dataGridSettingsService, + cache, + tableData, + }, + ); +} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.tsx index 8a102640da8..e85334a9468 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.tsx @@ -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. @@ -28,7 +28,7 @@ import { type IGridDataKey, } from '@cloudbeaver/plugin-data-viewer'; -import type { IColumnInfo, ITableData } from './TableDataContext.js'; +import { type IColumnInfo, type ITableData } from './TableDataContext.js'; import { useService } from '@cloudbeaver/core-di'; import { DataGridSettingsService } from '../DataGridSettingsService.js'; import type { SqlResultColumn } from '@cloudbeaver/core-sdk'; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGridSettingsService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGridSettingsService.ts index a9fa5351ef3..f194aab8495 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGridSettingsService.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGridSettingsService.ts @@ -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. @@ -17,10 +17,27 @@ import { } from '@cloudbeaver/core-settings'; import { schema, schemaExtra } from '@cloudbeaver/core-utils'; import { DATA_EDITOR_SETTINGS_GROUP } from '@cloudbeaver/plugin-data-viewer'; +import { COMMON_LOCALES } from './commonLocales.js'; +import { getLocalizedDisplayName } from '@dbeaver/js-helpers'; + +const NO_FORMAT = 'default'; +const OS_FORMAT = '_OS'; + +function getSupportedLocalesWithRegions() { + return COMMON_LOCALES.filter(localeCode => { + try { + const locale = new Intl.Locale(localeCode); + return locale.language && locale.region; + } catch { + return false; + } + }); +} const defaultSettings = schema.object({ 'plugin.data-spreadsheet.hidden': schemaExtra.stringedBoolean().default(false), 'plugin.data-spreadsheet.showDescriptionInHeader': schemaExtra.stringedBoolean().default(true), + 'plugin.data-spreadsheet.formatLocale': schema.string().default(NO_FORMAT), }); export type DataGridSettingsSchema = typeof defaultSettings; @@ -36,13 +53,22 @@ export class DataGridSettingsService { return this.settings.getValue('plugin.data-spreadsheet.showDescriptionInHeader'); } + get formatLocale(): string { + return this.settings.getValue('plugin.data-spreadsheet.formatLocale'); + } + readonly settings: SettingsProvider; + readonly supportedLocales: string[]; + + private osLocale: string | null; constructor( private readonly settingsProviderService: SettingsProviderService, private readonly settingsManagerService: SettingsManagerService, private readonly settingsResolverService: SettingsResolverService, ) { + this.supportedLocales = getSupportedLocalesWithRegions(); + this.osLocale = null; this.settings = this.settingsProviderService.createSettings(defaultSettings); this.settingsResolverService.addResolver( ROOT_SETTINGS_LAYER, @@ -55,6 +81,28 @@ export class DataGridSettingsService { this.registerSettings(); } + getFormatLocale(): string | null { + const setting = this.formatLocale; + + if (setting === NO_FORMAT) { + return null; + } + + if (setting === OS_FORMAT) { + return this.getOrCreateOSLocale(); + } + + return setting; + } + + private getOrCreateOSLocale(): string { + if (this.osLocale === null) { + this.osLocale = new Intl.DateTimeFormat().resolvedOptions().locale; + } + + return this.osLocale; + } + private registerSettings() { this.settingsManagerService.registerSettings(() => [ { @@ -77,6 +125,23 @@ export class DataGridSettingsService { name: 'plugin_data_spreadsheet_new_settings_description_label', description: 'plugin_data_spreadsheet_new_settings_description_label_description', }, + { + group: DATA_EDITOR_SETTINGS_GROUP, + key: 'plugin.data-spreadsheet.formatLocale', + access: { + scope: ['client'], + }, + type: ESettingsValueType.Select, + options: [ + { value: NO_FORMAT, name: 'plugin_data_spreadsheet_new_settings_use_locale_formatting_none' }, + { value: OS_FORMAT, name: 'plugin_data_spreadsheet_new_settings_use_locale_formatting_os' }, + ...this.supportedLocales + .map((locale: string) => ({ value: locale, name: getLocalizedDisplayName(locale) })) + .sort((a: { value: string; name: string }, b: { value: string; name: string }) => a.name.localeCompare(b.name)), + ], + name: 'plugin_data_spreadsheet_new_settings_use_locale_formatting_title', + description: 'plugin_data_spreadsheet_new_settings_use_locale_formatting_description', + }, ]); } } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/commonLocales.ts b/webapp/packages/plugin-data-spreadsheet-new/src/commonLocales.ts new file mode 100644 index 00000000000..18347663ce0 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/commonLocales.ts @@ -0,0 +1,160 @@ +/* + * 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. + */ +export const COMMON_LOCALES = [ + // English variants + 'en-US', + 'en-GB', + 'en-CA', + 'en-AU', + 'en-NZ', + 'en-IE', + 'en-IN', + 'en-SG', + 'en-ZA', + // Spanish variants + 'es-ES', + 'es-MX', + 'es-AR', + 'es-CO', + 'es-CL', + 'es-VE', + 'es-PE', + 'es-UY', + 'es-BO', + // French variants + 'fr-FR', + 'fr-CA', + 'fr-BE', + 'fr-CH', + 'fr-LU', + // German variants + 'de-DE', + 'de-AT', + 'de-CH', + 'de-LU', + 'de-LI', + // Portuguese variants + 'pt-BR', + 'pt-PT', + // Italian variants + 'it-IT', + 'it-CH', + // Chinese variants + 'zh-CN', + 'zh-TW', + 'zh-HK', + 'zh-SG', + // Japanese + 'ja-JP', + // Korean + 'ko-KR', + // Arabic variants + 'ar-SA', + 'ar-EG', + 'ar-AE', + 'ar-DZ', + 'ar-MA', + 'ar-IQ', + 'ar-JO', + 'ar-LB', + 'ar-KW', + // Russian + 'ru-RU', + 'ru-BY', + 'ru-KZ', + // Dutch variants + 'nl-NL', + 'nl-BE', + // Polish + 'pl-PL', + // Turkish + 'tr-TR', + // Swedish + 'sv-SE', + // Norwegian variants + 'nb-NO', + 'nn-NO', + // Danish + 'da-DK', + // Finnish + 'fi-FI', + // Greek + 'el-GR', + // Czech + 'cs-CZ', + // Hungarian + 'hu-HU', + // Romanian + 'ro-RO', + // Thai + 'th-TH', + // Vietnamese + 'vi-VN', + // Indonesian + 'id-ID', + // Malay + 'ms-MY', + // Hebrew + 'he-IL', + // Hindi + 'hi-IN', + // Bengali + 'bn-BD', + 'bn-IN', + // Ukrainian + 'uk-UA', + // Bulgarian + 'bg-BG', + // Croatian + 'hr-HR', + // Slovak + 'sk-SK', + // Serbian + 'sr-RS', + // Slovenian + 'sl-SI', + // Lithuanian + 'lt-LT', + // Latvian + 'lv-LV', + // Estonian + 'et-EE', + // Icelandic + 'is-IS', + // Catalan + 'ca-ES', + // Basque + 'eu-ES', + // Galician + 'gl-ES', + // Persian + 'fa-IR', + // Urdu + 'ur-PK', + // Tamil + 'ta-IN', + // Telugu + 'te-IN', + // Kannada + 'kn-IN', + // Malayalam + 'ml-IN', + // Marathi + 'mr-IN', + // Gujarati + 'gu-IN', + // Punjabi + 'pa-IN', + // Filipino/Tagalog + 'fil-PH', + // Swahili + 'sw-KE', + 'sw-TZ', + // Afrikaans + 'af-ZA', +]; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/de.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/de.ts index 1a10bfb9a47..2f3000d003c 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/de.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/de.ts @@ -23,4 +23,11 @@ export default [ ['plugin_data_spreadsheet_new_settings_disable_description', 'Deaktivieren Sie die Tabellenpräsentation von Daten für alle Benutzer'], ['plugin_data_spreadsheet_new_settings_description_label', 'Show columns description'], ['plugin_data_spreadsheet_new_settings_description_label_description', 'Description will be shown under the column names in the table header'], + ['plugin_data_spreadsheet_new_settings_use_locale_formatting_title', 'Gebietsschema-Formatierung verwenden'], + ['plugin_data_spreadsheet_new_settings_use_locale_formatting_os', 'OS-Formatierung verwenden'], + ['plugin_data_spreadsheet_new_settings_use_locale_formatting_none', 'Keine'], + [ + 'plugin_data_spreadsheet_new_settings_use_locale_formatting_description', + 'Verwenden Sie die Formatierung für Zahlen und Daten entsprechend dem ausgewählten Gebietsschema. (!) Dies betrifft nur die Datendarstellung; für die Bearbeitung sollten Rohwerte verwendet werden.', + ], ]; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts index 0e82354bb6c..616616960b9 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts @@ -29,4 +29,11 @@ export default [ ['plugin_data_spreadsheet_new_settings_disable_description', 'Disable table presentation of data for all users'], ['plugin_data_spreadsheet_new_settings_description_label', 'Show columns description'], ['plugin_data_spreadsheet_new_settings_description_label_description', 'Description will be shown under the column names in the table header'], + ['plugin_data_spreadsheet_new_settings_use_locale_formatting_title', 'Use locale formatting'], + ['plugin_data_spreadsheet_new_settings_use_locale_formatting_os', 'Use OS formatting'], + ['plugin_data_spreadsheet_new_settings_use_locale_formatting_none', 'None'], + [ + 'plugin_data_spreadsheet_new_settings_use_locale_formatting_description', + 'Use formatting for numbers and dates according to the selected locale. (!)This only affects data representation; raw values should be used for editing.', + ], ]; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/fr.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/fr.ts index e47ec4f1f1d..f822f3ea3b7 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/fr.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/fr.ts @@ -28,4 +28,11 @@ export default [ ['plugin_data_spreadsheet_new_settings_disable', 'Désactiver la présentation de la table'], ['plugin_data_spreadsheet_new_settings_description_label', 'Show columns description'], ['plugin_data_spreadsheet_new_settings_description_label_description', 'Description will be shown under the column names in the table header'], + ['plugin_data_spreadsheet_new_settings_use_locale_formatting_title', 'Utiliser le formatage régional'], + ['plugin_data_spreadsheet_new_settings_use_locale_formatting_os', "Utiliser le formatage du système d'exploitation"], + ['plugin_data_spreadsheet_new_settings_use_locale_formatting_none', 'Aucun'], + [ + 'plugin_data_spreadsheet_new_settings_use_locale_formatting_description', + "Utilisez le formatage pour les nombres et les dates selon les paramètres régionaux sélectionnés. (!) Cela n'affecte que la représentation des données ; les valeurs brutes doivent être utilisées pour l'édition.", + ], ]; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/it.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/it.ts index bc1d6cb6798..24365cbfeaf 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/it.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/it.ts @@ -23,4 +23,11 @@ export default [ ['plugin_data_spreadsheet_new_settings_disable', 'Disable Table presentation'], ['plugin_data_spreadsheet_new_settings_description_label', 'Show columns description'], ['plugin_data_spreadsheet_new_settings_description_label_description', 'Description will be shown under the column names in the table header'], + ['plugin_data_spreadsheet_new_settings_use_locale_formatting_title', 'Usa formattazione locale'], + ['plugin_data_spreadsheet_new_settings_use_locale_formatting_os', 'Usa formattazione del sistema operativo'], + ['plugin_data_spreadsheet_new_settings_use_locale_formatting_none', 'Nessuno'], + [ + 'plugin_data_spreadsheet_new_settings_use_locale_formatting_description', + 'Usa la formattazione per numeri e date in base alla locale selezionata. (!) Questo influisce solo sulla rappresentazione dei dati; i valori grezzi devono essere usati per la modifica.', + ], ]; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts index bd2601d5182..6622b11b0ae 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts @@ -29,4 +29,11 @@ export default [ ['plugin_data_spreadsheet_new_settings_disable_description', 'Отключить табличное представление данных для всех пользователей'], ['plugin_data_spreadsheet_new_settings_description_label', 'Показать описание колонки'], ['plugin_data_spreadsheet_new_settings_description_label_description', 'Описание будет показано под именами колонок в заголовке таблицы'], + ['plugin_data_spreadsheet_new_settings_use_locale_formatting_title', 'Использовать локальное форматирование'], + ['plugin_data_spreadsheet_new_settings_use_locale_formatting_os', 'Использовать системное форматирование'], + ['plugin_data_spreadsheet_new_settings_use_locale_formatting_none', 'Нет'], + [ + 'plugin_data_spreadsheet_new_settings_use_locale_formatting_description', + 'Использовать форматирование для чисел и дат в соответствии с выбранной локалью. (!) Это влияет только на представление данных; для редактирования должны использоваться исходные значения.', + ], ]; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/vi.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/vi.ts index ec58fe7aba2..7a96824c0df 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/vi.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/vi.ts @@ -29,4 +29,11 @@ export default [ ['plugin_data_spreadsheet_new_settings_disable_description', 'Tắt chế độ hiển thị dữ liệu dạng bảng cho tất cả người dùng'], ['plugin_data_spreadsheet_new_settings_description_label', 'Show columns description'], ['plugin_data_spreadsheet_new_settings_description_label_description', 'Description will be shown under the column names in the table header'], + ['plugin_data_spreadsheet_new_settings_use_locale_formatting_title', 'Sử dụng định dạng ngôn ngữ'], + ['plugin_data_spreadsheet_new_settings_use_locale_formatting_os', 'Sử dụng định dạng hệ điều hành'], + ['plugin_data_spreadsheet_new_settings_use_locale_formatting_none', 'Không'], + [ + 'plugin_data_spreadsheet_new_settings_use_locale_formatting_description', + 'Sử dụng định dạng cho số và ngày theo ngôn ngữ đã chọn. (!) Điều này chỉ ảnh hưởng đến việc hiển thị dữ liệu; các giá trị thô nên được sử dụng để chỉnh sửa.', + ], ]; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/zh.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/zh.ts index 2174c28363b..1aa69b20657 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/zh.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/zh.ts @@ -28,4 +28,11 @@ export default [ ['plugin_data_spreadsheet_new_settings_disable', '禁用表显示'], ['plugin_data_spreadsheet_new_settings_description_label', 'Show columns description'], ['plugin_data_spreadsheet_new_settings_description_label_description', 'Description will be shown under the column names in the table header'], + ['plugin_data_spreadsheet_new_settings_use_locale_formatting_title', '使用本地格式化'], + ['plugin_data_spreadsheet_new_settings_use_locale_formatting_os', '使用操作系统格式化'], + ['plugin_data_spreadsheet_new_settings_use_locale_formatting_none', '无'], + [ + 'plugin_data_spreadsheet_new_settings_use_locale_formatting_description', + '根据选定的区域设置对数字和日期进行格式化。(!)这仅影响数据表示;编辑时应使用原始值。', + ], ]; diff --git a/webapp/packages/plugin-data-spreadsheet-new/tsconfig.json b/webapp/packages/plugin-data-spreadsheet-new/tsconfig.json index a7da8123121..4f15facc9b5 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/tsconfig.json +++ b/webapp/packages/plugin-data-spreadsheet-new/tsconfig.json @@ -16,6 +16,9 @@ { "path": "../../common-typescript/@dbeaver/cli" }, + { + "path": "../../common-typescript/@dbeaver/js-helpers" + }, { "path": "../../common-typescript/@dbeaver/result-set-api" }, diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetCacheAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetCacheAction.ts index 94d75084a6f..03e00fde6bb 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetCacheAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetCacheAction.ts @@ -16,7 +16,7 @@ import type { IDatabaseDataCacheAction } from '../IDatabaseDataCacheAction.js'; import { ResultSetDataAction } from './ResultSetDataAction.js'; import { injectable } from '@cloudbeaver/core-di'; import { IDatabaseDataResult } from '../../IDatabaseDataResult.js'; -import type { IGridDataKey, IGridRowKey } from '../Grid/IGridDataKey.js'; +import type { IGridColumnKey, IGridDataKey, IGridRowKey } from '../Grid/IGridDataKey.js'; @injectable(() => [IDatabaseDataSource, IDatabaseDataResult, ResultSetDataAction]) export class ResultSetCacheAction @@ -40,9 +40,11 @@ export class ResultSetCacheAction cache: observable, set: action, setRow: action, + setColumn: action, delete: action, deleteAll: action, deleteRow: action, + deleteColumn: action, }); } @@ -64,6 +66,15 @@ export class ResultSetCacheAction return keyCache.get(scope); } + getColumn(key: IGridColumnKey, scope: symbol): T | undefined { + const keyCache = this.getColumnCache(key); + if (!keyCache) { + return; + } + + return keyCache.get(scope); + } + has(key: IGridDataKey, scope: symbol): boolean { const keyCache = this.getKeyCache(key); @@ -84,6 +95,16 @@ export class ResultSetCacheAction return keyCache.has(scope); } + hasColumn(key: IGridColumnKey, scope: symbol): boolean { + const keyCache = this.getColumnCache(key); + + if (!keyCache) { + return false; + } + + return keyCache.has(scope); + } + set(key: IGridDataKey, scope: symbol, value: T): void { const keyCache = this.getOrCreateKeyCache(key); @@ -96,6 +117,12 @@ export class ResultSetCacheAction keyCache.set(scope, value); } + setColumn(key: IGridColumnKey, scope: symbol, value: T): void { + const keyCache = this.getOrCreateColumnKeyCache(key); + + keyCache.set(scope, value); + } + delete(key: IGridDataKey, scope: symbol): void { const keyCache = this.getKeyCache(key); @@ -118,6 +145,14 @@ export class ResultSetCacheAction } } + deleteColumn(key: IGridColumnKey, scope: symbol): void { + const keyCache = this.getColumnCache(key); + + if (keyCache) { + keyCache.delete(scope); + } + } + override afterResultUpdate(): void { this.cache.clear(); } @@ -130,6 +165,10 @@ export class ResultSetCacheAction return 'row:' + this.data.serializeRowKey(key); } + private serializeColumnKey(key: IGridColumnKey) { + return 'col:' + key.index; + } + private serializeKey(key: IGridDataKey) { return this.data.serialize(key); } @@ -142,6 +181,10 @@ export class ResultSetCacheAction return this.cache.get(this.serializeRowKey(key)); } + private getColumnCache(key: IGridColumnKey) { + return this.cache.get(this.serializeColumnKey(key)); + } + private getOrCreateKeyCache(key: IGridDataKey) { let keyCache = this.getKeyCache(key); @@ -163,4 +206,15 @@ export class ResultSetCacheAction return keyCache; } + + private getOrCreateColumnKeyCache(key: IGridColumnKey) { + let keyCache = this.getColumnCache(key); + + if (!keyCache) { + keyCache = observable(new Map()); + this.cache.set(this.serializeColumnKey(key), keyCache); + } + + return keyCache; + } } diff --git a/webapp/yarn.lock b/webapp/yarn.lock index fce94fc738b..5612fa141fa 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -2903,6 +2903,7 @@ __metadata: "@cloudbeaver/plugin-data-viewer-conditional-formatting": "workspace:*" "@cloudbeaver/tsconfig": "workspace:*" "@dbeaver/cli": "workspace:*" + "@dbeaver/js-helpers": "workspace:*" "@dbeaver/react-tests": "workspace:*" "@dbeaver/result-set-api": "workspace:*" "@dbeaver/ui-kit": "workspace:*"