From 6845bfd2ee361c9e444572e0b87809d131c8d51a Mon Sep 17 00:00:00 2001 From: keppere <2keppere@gmail.com> Date: Wed, 1 Jan 2025 16:40:46 +0700 Subject: [PATCH 1/5] feat: export query result with more options --- .../gui/export/export-result-button.tsx | 436 ++++++++++++++++-- .../table-optimized/OptimizeTableState.tsx | 35 +- .../gui/tabs-result/query-result-tab.tsx | 3 +- src/components/gui/tabs/query-tab.tsx | 8 +- src/components/gui/tabs/table-data-tab.tsx | 7 +- src/components/lib/export-helper.ts | 151 +++++- src/drivers/sqlite/sql-helper.ts | 15 +- 7 files changed, 595 insertions(+), 60 deletions(-) diff --git a/src/components/gui/export/export-result-button.tsx b/src/components/gui/export/export-result-button.tsx index 1e3111ef..26760c69 100644 --- a/src/components/gui/export/export-result-button.tsx +++ b/src/components/gui/export/export-result-button.tsx @@ -1,40 +1,140 @@ import { Button, buttonVariants } from "../../ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "../../ui/popover"; +import OptimizeTableState, { + TableSelectionRange, +} from "../table-optimized/OptimizeTableState"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { getFormatHandlers } from "@/components/lib/export-helper"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "../../ui/select"; -import OptimizeTableState from "../table-optimized/OptimizeTableState"; -import { useCallback, useState } from "react"; -import { getFormatHandlers } from "@/components/lib/export-helper"; +} from "@/components/ui/select"; + +export type ExportTarget = "clipboard" | "file"; +type ExportFormat = "csv" | "delimited" | "json" | "sql" | "xlsx"; +export type ExportSelection = + | "complete" + | "selected_row" + | "selected_col" + | "selected_range"; +export interface ExportOptions { + fieldSeparator?: string; + lineTerminator?: string; + encloser?: string; +} + +interface selectionCount { + rows: number; + cols: number; + ranges: TableSelectionRange[]; +} + +interface ExportSettings { + format: ExportFormat; + target: ExportTarget; + selection: ExportSelection; + options?: ExportOptions; +} export default function ExportResultButton({ data, }: { data: OptimizeTableState; }) { - const [format, setFormat] = useState(null); + const csvDelimeter = useMemo( + () => ({ + fieldSeparator: ",", + lineTerminator: "\\n", + encloser: '"', + }), + [] + ); + const excelDiliemter = { + fieldSeparator: "\\t", + lineTerminator: "\\r\\n", + encloser: '"', + }; + + const saveSetting = (settings: ExportSettings) => { + localStorage.setItem("export_settings", JSON.stringify(settings)); + }; + + const exportSettings = useCallback(() => { + const settings = localStorage.getItem("export_settings"); + if (settings) { + return JSON.parse(settings) as ExportSettings; + } + return { + format: "csv", + target: "clipboard", + selection: "complete", + options: csvDelimeter, + } as ExportSettings; + }, [csvDelimeter]); + + const [format, setFormat] = useState(exportSettings().format); + const [exportTarget, setExportTarget] = useState( + exportSettings().target + ); + const [selectionCount, setSelectionCount] = useState({ + rows: 0, + cols: 0, + ranges: [], + }); + const [exportSelection, setExportSelection] = useState( + () => { + const savedSelection = exportSettings().selection; + return validateExportSelection(savedSelection, selectionCount); + } + ); + const [delimitedOptions, setDelimitedOptions] = useState( + exportSettings().options || { + fieldSeparator: ",", + lineTerminator: "\\n", + encloser: '"', + } + ); + const [exportOptions, setExportOptions] = useState( + () => { + if (format === "csv") { + return csvDelimeter; + } else if (format === "xlsx") { + return excelDiliemter; + } else if (format === "delimited") { + return delimitedOptions; + } else { + return null; + } + } + ); + + const [selectedRangeIndex, setSelectedRangeIndex] = useState( + selectionCount.ranges.length > 0 ? 0 : -1 + ); + const [open, setOpen] = useState(false); const onExportClicked = useCallback(() => { if (!format) return; let content = ""; - const headers = data.getHeaders().map((header) => header.name); - const records = data - .getAllRows() - .map((row) => headers.map((header) => row.raw[header])); - - const tableName = "UnknownTable"; //TODO: replace with actual table name - const formatHandlers = getFormatHandlers(records, headers, tableName); + const formatHandlers = getFormatHandlers( + data, + exportTarget, + exportSelection, + exportOptions, + selectedRangeIndex + ); const handler = formatHandlers[format]; if (handler) { content = handler(); } + setOpen(false); if (!content) return; @@ -42,32 +142,287 @@ export default function ExportResultButton({ const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; - a.download = `export.${format}`; + a.download = `export.${format === "delimited" ? "csv" : format}`; a.click(); URL.revokeObjectURL(url); - }, [format, data]); + }, [ + format, + data, + exportTarget, + exportSelection, + exportOptions, + selectedRangeIndex, + ]); + + useEffect(() => { + const changeCallback = () => { + setSelectionCount({ + rows: data.getFullSelectionRowsIndex().length, + cols: data.getFullSelectionColsIndex().length, + ranges: data.getSelectionRanges(), + } as selectionCount); + if (data.getSelectionRanges().length > 0) { + setSelectedRangeIndex(0); + } + }; + data.addChangeListener(changeCallback); + return () => data.removeChangeListener(changeCallback); + }, [data]); + + useEffect(() => { + setExportSelection( + validateExportSelection(exportSettings().selection, selectionCount) + ); + }, [exportSettings, selectionCount]); + + useEffect(() => { + saveSetting({ + ...exportSettings(), + format, + selection: exportSelection, + target: exportTarget, + }); + if (format === "delimited") { + saveSetting({ + ...exportSettings(), + options: exportOptions ?? csvDelimeter, + }); + if (exportOptions) setDelimitedOptions(exportOptions); + } + }, [ + csvDelimeter, + exportOptions, + exportSelection, + exportSettings, + exportTarget, + format, + ]); + + const SelectedRange = ({ + ranges, + value, + onChange, + }: { + ranges: TableSelectionRange[]; + value?: string; + onChange: (value: string) => void; + }) => { + return ( + + ); + }; return ( - + -
+
setOpen(!open)} + > Export
- +
Export
- +
+ Export target +
+ { + setExportTarget(e as ExportTarget); + }} + > +
+
+ + Copy to clipboard +
+
+ + Export to file +
+
+
+
+
+
+
+ Output format +
+ { + setFormat(e as ExportFormat); + if (e === "csv") { + setExportOptions(csvDelimeter); + } else if (e === "xlsx") { + setExportOptions(excelDiliemter); + } else if (e === "delimited") { + setExportOptions(delimitedOptions); + } else { + setExportOptions(null); + } + }} + > +
+
+ + CSV +
+
+ + DELIMITED TEXT +
+
+ + JSON +
+
+ + SQL +
+
+ + EXCEL +
+
+
+
+
+
+
+ Selection +
+ { + setExportSelection(e as ExportSelection); + }} + > +
+
+ + + Complete ({data.getAllRows().length} rows) + +
+
+ + Rows ({selectionCount.rows} rows) +
+
+ + Columns ({selectionCount.cols} cols) +
+
+ + Ranges +
+ {selectionCount.ranges.length > 0 && + SelectedRange({ + ranges: selectionCount.ranges, + value: + selectedRangeIndex >= 0 + ? selectedRangeIndex.toString() + : "0", + onChange: (value) => { + setSelectedRangeIndex(parseInt(value)); + }, + })} +
+
+
+
+
+
+
+ Options +
+
+ + Field separator: + +
+ { + setExportOptions({ + ...exportOptions, + fieldSeparator: e.target.value, + }); + }} + /> +
+
+
+ + Line terminator: + +
+ { + setExportOptions({ + ...exportOptions, + lineTerminator: e.target.value, + }); + }} + /> +
+
+ +
+ Encloser: +
+ { + setExportOptions({ + ...exportOptions, + encloser: e.target.value, + }); + }} + /> +
+
+
+
+
+
{stat && data && (
-
+
+
+ +
diff --git a/src/components/lib/export-helper.ts b/src/components/lib/export-helper.ts index 06283ab3..c46c79ea 100644 --- a/src/components/lib/export-helper.ts +++ b/src/components/lib/export-helper.ts @@ -1,8 +1,16 @@ import { - escapeCsvValue, + escapeDelimitedValue, escapeIdentity, escapeSqlValue, } from "@/drivers/sqlite/sql-helper"; +import OptimizeTableState from "../gui/table-optimized/OptimizeTableState"; +import { + ExportOptions, + ExportSelection, + ExportTarget, +} from "../gui/export/export-result-button"; +import { toast } from "sonner"; +import { getSingleTableName } from "../gui/tabs/query-tab"; export function selectArrayFromIndexList( data: T[], @@ -14,7 +22,8 @@ export function selectArrayFromIndexList( export function exportRowsToSqlInsert( tableName: string, headers: string[], - records: unknown[][] + records: unknown[][], + exportTarget?: ExportTarget ): string { const result: string[] = []; @@ -29,7 +38,12 @@ export function exportRowsToSqlInsert( result.push(line); } - return result.join("\r\n"); + const content = result.join("\n"); + if (exportTarget === "clipboard") { + copyToClipboard(content); + return ""; + } + return content; } function cellToExcelValue(value: unknown) { @@ -53,8 +67,14 @@ export function exportRowsToExcel(records: unknown[][]) { export function exportToExcel( records: unknown[][], headers: string[], - tablename: string + tablename: string, + exportTarget: ExportTarget ) { + if (exportTarget === "clipboard") { + exportDataAsDelimitedText(headers, records, "\t", "\r\n", '"', "clipboard"); + return ""; + } + const processedData = records.map((row) => row.map((cell) => { return cellToExcelValue(cell); @@ -62,7 +82,6 @@ export function exportToExcel( ); const data = [headers, ...processedData]; - console.log(data); import("xlsx").then((module) => { const XLSX = module; @@ -77,7 +96,8 @@ export function exportToExcel( export function exportRowsToJson( headers: string[], - records: unknown[][] + records: unknown[][], + exportTarget?: ExportTarget ): string { const recordsWithBigIntAsString = records.map((record) => record.map((value) => @@ -95,39 +115,128 @@ export function exportRowsToJson( }, {}) ); - return JSON.stringify(recordsAsObjects, null, 2); + const content = JSON.stringify(recordsAsObjects, null, 2); + + if (exportTarget === "clipboard") { + copyToClipboard(content); + return ""; + } + + return content; } -export function exportRowsToCsv( +export function exportDataAsDelimitedText( headers: string[], - records: unknown[][] + records: unknown[][], + fieldSeparator: string, + lineTerminator: string, + textEncloser: string, + exportTarget: ExportTarget ): string { const result: string[] = []; // Add headers - const escapedHeaders = headers.map(escapeCsvValue); - const headerLine = escapedHeaders.join(","); + const escapedHeaders = headers.map((v) => + escapeDelimitedValue(v, fieldSeparator, lineTerminator, textEncloser) + ); + const headerLine = escapedHeaders.join(fieldSeparator); result.push(headerLine); // Add records for (const record of records) { - const escapedRecord = record.map(escapeCsvValue); - const recordLine = escapedRecord.join(","); + const escapedRecord = record.map((v) => + escapeDelimitedValue(v, fieldSeparator, lineTerminator, textEncloser) + ); + const recordLine = escapedRecord.join(fieldSeparator); result.push(recordLine); } - return result.join("\n"); + const content = result.join(lineTerminator); + + if (exportTarget === "clipboard") { + copyToClipboard(content); + return ""; + } + return content; } export function getFormatHandlers( - records: unknown[][], - headers: string[], - tableName: string + data: OptimizeTableState, + exportTarget: ExportTarget, + exportSelection: ExportSelection, + exportOptions: ExportOptions | null, + selectedRangeIndex: number ): Record string) | undefined> { + const tableName = getSingleTableName(data.getSql()) || "UnknownTable"; + let headers: string[] = []; + let records: unknown[][] = []; + + // Handle export selection + if (exportSelection === "complete") { + headers = data.getHeaders().map((header) => header.name); + records = data + .getAllRows() + .map((row) => headers.map((header) => row.raw[header])); + } else if (exportSelection === "selected_row") { + headers = data.getHeaders().map((header) => header.name); + records = selectArrayFromIndexList( + data.getAllRows(), + data.getSelectedRowIndex() + ).map((row) => headers.map((header) => row.raw[header])); + } else if (exportSelection === "selected_col") { + headers = data + .getHeaders() + .filter((_, index) => data.getFullSelectionColsIndex().includes(index)) + .map((header) => header.name); + records = data + .getAllRows() + .map((row) => headers.map((header) => row.raw[header])); + } else if (exportSelection === "selected_range" && selectedRangeIndex >= 0) { + const selectedRange = data.getSelectionRanges()[selectedRangeIndex]; + headers = data + .getHeaders() + .filter( + (_, index) => index >= selectedRange.x1 && index <= selectedRange.x2 + ) + .map((header) => header.name); + records = data + .getAllRows() + .filter( + (_, index) => index >= selectedRange.y1 && index <= selectedRange.y2 + ) + .map((row) => headers.map((header) => row.raw[header])); + } + return { - csv: () => exportRowsToCsv(headers, records), - json: () => exportRowsToJson(headers, records), - sql: () => exportRowsToSqlInsert(tableName, headers, records), - xlsx: () => exportToExcel(records, headers, tableName), + csv: () => + exportDataAsDelimitedText(headers, records, ",", "\n", '"', exportTarget), + json: () => exportRowsToJson(headers, records, exportTarget), + sql: () => exportRowsToSqlInsert(tableName, headers, records, exportTarget), + xlsx: () => exportToExcel(records, headers, tableName, exportTarget), + delimited: () => + exportDataAsDelimitedText( + headers, + records, + parseUserInput(exportOptions?.fieldSeparator || "") || ",", + parseUserInput(exportOptions?.lineTerminator || "") || "\n", + parseUserInput(exportOptions?.encloser || "") || '"', + exportTarget + ), }; } + +function parseUserInput(input: string): string { + return input + .replace(/^"|"$/g, "") + .replace(/\\n/g, "\n") + .replace(/\\t/g, "\t") + .replace(/\\\\/g, "\\") + .replace(/\\r/g, "\r"); +} + +function copyToClipboard(content: string) { + navigator.clipboard + .writeText(content) + .then(() => toast.success("Copied to clipboard")) + .catch(() => toast.error("Failed to copy to clipboard")); +} diff --git a/src/drivers/sqlite/sql-helper.ts b/src/drivers/sqlite/sql-helper.ts index 19a7a182..0606f2ba 100644 --- a/src/drivers/sqlite/sql-helper.ts +++ b/src/drivers/sqlite/sql-helper.ts @@ -62,19 +62,24 @@ export function convertSqliteType( return TableColumnDataType.TEXT; } -export function escapeCsvValue(value: unknown): string { +export function escapeDelimitedValue( + value: unknown, + fieldSeparator: string, + lineTerminator: string, + encloser: string +): string { if (value === null || value === undefined) { return ""; } const stringValue = value.toString(); const needsEscaping = - stringValue.includes(",") || - stringValue.includes('"') || - stringValue.includes("\n"); + stringValue.includes(fieldSeparator) || + stringValue.includes(lineTerminator) || + stringValue.includes(encloser); if (needsEscaping) { - return `"${stringValue.replace(/"/g, '""')}"`; + return `${encloser}${stringValue.replace(new RegExp(encloser, "g"), encloser + encloser)}${encloser}`; } return stringValue; From 967f228248740631e2ceb821b167e1059e9ddf00 Mon Sep 17 00:00:00 2001 From: keppere <2keppere@gmail.com> Date: Wed, 1 Jan 2025 17:01:11 +0700 Subject: [PATCH 2/5] fix: handle to close export panel on click outside it --- .../gui/export/export-result-button.tsx | 349 +++++++++--------- 1 file changed, 183 insertions(+), 166 deletions(-) diff --git a/src/components/gui/export/export-result-button.tsx b/src/components/gui/export/export-result-button.tsx index 26760c69..979fc550 100644 --- a/src/components/gui/export/export-result-button.tsx +++ b/src/components/gui/export/export-result-button.tsx @@ -3,7 +3,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "../../ui/popover"; import OptimizeTableState, { TableSelectionRange, } from "../table-optimized/OptimizeTableState"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { getFormatHandlers } from "@/components/lib/export-helper"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { @@ -116,6 +116,21 @@ export default function ExportResultButton({ selectionCount.ranges.length > 0 ? 0 : -1 ); const [open, setOpen] = useState(false); + const popoverRef = useRef(null); + + const handleClickOutside = (event: MouseEvent) => { + if ( + popoverRef.current && + !popoverRef.current.contains(event.target as Node) + ) { + setOpen(false); + } + }; + + useEffect(() => { + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); const onExportClicked = useCallback(() => { if (!format) return; @@ -227,210 +242,212 @@ export default function ExportResultButton({ }; return ( - - -
setOpen(!open)} - > - Export -
-
- -
-
Export
-
- Export target -
- { - setExportTarget(e as ExportTarget); - }} - > -
-
- - Copy to clipboard -
-
- - Export to file -
-
-
-
+
+ + +
setOpen(!open)} + > + Export
-
-
- Output format -
+ + +
+
Export
+
+ Export target +
{ - setFormat(e as ExportFormat); - if (e === "csv") { - setExportOptions(csvDelimeter); - } else if (e === "xlsx") { - setExportOptions(excelDiliemter); - } else if (e === "delimited") { - setExportOptions(delimitedOptions); - } else { - setExportOptions(null); - } + setExportTarget(e as ExportTarget); }} > -
-
- - CSV -
-
- - DELIMITED TEXT -
-
- - JSON -
-
- - SQL +
+
+ + Copy to clipboard
- - EXCEL + + Export to file
-
-
- Selection +
+
+ Output format
{ - setExportSelection(e as ExportSelection); + setFormat(e as ExportFormat); + if (e === "csv") { + setExportOptions(csvDelimeter); + } else if (e === "xlsx") { + setExportOptions(excelDiliemter); + } else if (e === "delimited") { + setExportOptions(delimitedOptions); + } else { + setExportOptions(null); + } }} >
- - - Complete ({data.getAllRows().length} rows) - + + CSV
- - Rows ({selectionCount.rows} rows) + + DELIMITED TEXT
- - Columns ({selectionCount.cols} cols) + + JSON
- - Ranges -
- {selectionCount.ranges.length > 0 && - SelectedRange({ - ranges: selectionCount.ranges, - value: - selectedRangeIndex >= 0 - ? selectedRangeIndex.toString() - : "0", - onChange: (value) => { - setSelectedRangeIndex(parseInt(value)); - }, - })} -
+ + SQL +
+
+ + EXCEL
-
- Options -
-
- - Field separator: - -
- { - setExportOptions({ - ...exportOptions, - fieldSeparator: e.target.value, - }); - }} - /> -
+
+
+ Selection +
+ { + setExportSelection(e as ExportSelection); + }} + > +
+
+ + + Complete ({data.getAllRows().length} rows) + +
+
+ + Rows ({selectionCount.rows} rows) +
+
+ + Columns ({selectionCount.cols} cols) +
+
+ + Ranges +
+ {selectionCount.ranges.length > 0 && + SelectedRange({ + ranges: selectionCount.ranges, + value: + selectedRangeIndex >= 0 + ? selectedRangeIndex.toString() + : "0", + onChange: (value) => { + setSelectedRangeIndex(parseInt(value)); + }, + })} +
+
+
+
-
- - Line terminator: - -
- { - setExportOptions({ - ...exportOptions, - lineTerminator: e.target.value, - }); - }} - /> +
+
+ Options +
+
+ + Field separator: + +
+ { + setExportOptions({ + ...exportOptions, + fieldSeparator: e.target.value, + }); + }} + /> +
+
+
+ + Line terminator: + +
+ { + setExportOptions({ + ...exportOptions, + lineTerminator: e.target.value, + }); + }} + /> +
-
-
- Encloser: -
- { - setExportOptions({ - ...exportOptions, - encloser: e.target.value, - }); - }} - /> +
+ Encloser: +
+ { + setExportOptions({ + ...exportOptions, + encloser: e.target.value, + }); + }} + /> +
-
-
- -
- - +
+ +
+ + +
); } From 0c86ee6391cbd2f11a709a749a952b0ce219a0cf Mon Sep 17 00:00:00 2001 From: keppere <2keppere@gmail.com> Date: Wed, 1 Jan 2025 17:17:57 +0700 Subject: [PATCH 3/5] fix: full selection column header background color --- src/components/gui/query-result-table.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/gui/query-result-table.tsx b/src/components/gui/query-result-table.tsx index 96275549..a8a35630 100644 --- a/src/components/gui/query-result-table.tsx +++ b/src/components/gui/query-result-table.tsx @@ -24,7 +24,6 @@ import { DropdownMenuSeparator, } from "../ui/dropdown-menu"; import useTableResultContextMenu from "./table-result/context-menu"; -import { cn } from "@/lib/utils"; interface ResultTableProps { data: OptimizeTableState; @@ -50,12 +49,9 @@ function Header({ if (internalState.getSelectedColIndex().includes(colIndex)) { if (internalState.isFullSelectionCol(colIndex)) { - textClass = cn( - "grow line-clamp-1 font-mono font-bold", - "bg-blue-600 border-red-900 text-white font-bold" - ); + textClass = "grow line-clamp-1 font-mono font-bold text-white font-bold"; thClass = - "flex grow items-center px-2 overflow-hidden bg-blue-600 dark:bg-blue-800"; + "flex grow items-center px-2 overflow-hidden bg-blue-600 dark:bg-blue-900"; } else { textClass = "grow line-clamp-1 font-mono font-bold text-white font-bold"; thClass = From 779c0b55ae206a885df11792bc4ca67e951a9856 Mon Sep 17 00:00:00 2001 From: keppere <2keppere@gmail.com> Date: Wed, 1 Jan 2025 20:20:17 +0700 Subject: [PATCH 4/5] feat: make aggregate function to support date and date time --- .../table-optimized/OptimizeTableState.tsx | 89 ++++++++++++++++--- 1 file changed, 79 insertions(+), 10 deletions(-) diff --git a/src/components/gui/table-optimized/OptimizeTableState.tsx b/src/components/gui/table-optimized/OptimizeTableState.tsx index c2a0bde9..218acfd0 100644 --- a/src/components/gui/table-optimized/OptimizeTableState.tsx +++ b/src/components/gui/table-optimized/OptimizeTableState.tsx @@ -866,6 +866,7 @@ export default class OptimizeTableState { let min = undefined; let max = undefined; let count = 0; + let detectedDataType = undefined; const selectedCell = new Set(); for (const range of this.selectionRanges) { @@ -878,12 +879,57 @@ export default class OptimizeTableState { selectedCell.add(key); const value = this.getValue(y, x); - const parsed = Number(value); - if (!isNaN(parsed)) { - sum = sum !== undefined ? sum + parsed : parsed; - min = min !== undefined ? (min < parsed ? min : parsed) : parsed; - max = max !== undefined ? (max > parsed ? max : parsed) : parsed; + if (value !== null && value !== undefined && value !== "") { + // detect first valid element data type + if (detectedDataType === undefined) { + if (!isNaN(Number(value))) { + detectedDataType = "number"; + } else if ( + typeof value === "string" && + !isNaN(Date.parse(value)) + ) { + detectedDataType = "date"; + } + } + + if (detectedDataType === "number") { + const parsed = Number(value); + if (!isNaN(parsed)) { + sum = sum !== undefined ? sum + parsed : parsed; + min = + min !== undefined + ? (min as number) < parsed + ? min + : parsed + : parsed; + max = + max !== undefined + ? (max as number) > parsed + ? max + : parsed + : parsed; + } + } else if ( + detectedDataType === "date" && + (isValidDate(value as string) || isValidDateTime(value as string)) + ) { + const parsed = Date.parse(value as string); + if (!isNaN(parsed)) { + min = + min !== undefined + ? Date.parse(min as string) < parsed + ? min + : value + : value; + max = + max !== undefined + ? Date.parse(max as string) > parsed + ? max + : value + : value; + } + } } count = count + 1; } @@ -892,12 +938,19 @@ export default class OptimizeTableState { if (sum !== undefined && count > 0) { avg = sum / count; } + if (detectedDataType === "number") { + return { + sum: formatNumber(sum), + avg: formatNumber(avg), + min: formatNumber(min as number), + max: formatNumber(max as number), + count: formatNumber(count), + }; + } return { - sum: formatNumber(sum), - avg: formatNumber(avg), - min: formatNumber(min), - max: formatNumber(max), - count: formatNumber(count), + min, + max, + count, }; } @@ -914,3 +967,19 @@ export default class OptimizeTableState { return this.sql; } } + +function isValidDate(value: string): boolean { + const dateRegex = /^\d{4}-\d{2}-\d{2}$/; + if (!dateRegex.test(value)) return false; + + const parsedDate = new Date(value); + return !isNaN(parsedDate.getTime()); +} + +function isValidDateTime(value: string): boolean { + const dateTimeRegex = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/; + if (!dateTimeRegex.test(value)) return false; + + const parsedDate = new Date(value); + return !isNaN(parsedDate.getTime()); +} From c8bbff6c1ed46f6f12174f7f25da50d5a6a80a3d Mon Sep 17 00:00:00 2001 From: keppere <2keppere@gmail.com> Date: Wed, 1 Jan 2025 20:29:53 +0700 Subject: [PATCH 5/5] fix: type --- .../gui/aggregate-result/aggregate-result-button.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/gui/aggregate-result/aggregate-result-button.tsx b/src/components/gui/aggregate-result/aggregate-result-button.tsx index 72844bb1..4bd6cd47 100644 --- a/src/components/gui/aggregate-result/aggregate-result-button.tsx +++ b/src/components/gui/aggregate-result/aggregate-result-button.tsx @@ -32,7 +32,9 @@ export default function AggregateResultButton({ ); useEffect(() => { const changeCallback = () => { - setResult({ ...data.getSelectionAggregatedResult() }); + setResult({ + ...(data.getSelectionAggregatedResult() as AggregateResult), + }); }; data.addChangeListener(changeCallback);