From e7aafa6015bd868c467319fecd3bb8cb8ef4a0d9 Mon Sep 17 00:00:00 2001 From: Qian Xia Date: Mon, 20 Jul 2026 14:20:52 +0800 Subject: [PATCH 1/3] [#12094] web-v2(UI): Support creating Doris tables with range/list partitions via Gravitino UI --- .../rightContent/CreateTableDialog.js | 183 ++++--- .../entitiesContent/TableDetailsPage.js | 11 +- web-v2/web/src/components/PartitionPanel.js | 475 ++++++++++++++++++ web-v2/web/src/config/index.js | 3 +- 4 files changed, 577 insertions(+), 95 deletions(-) create mode 100644 web-v2/web/src/components/PartitionPanel.js diff --git a/web-v2/web/src/app/catalogs/rightContent/CreateTableDialog.js b/web-v2/web/src/app/catalogs/rightContent/CreateTableDialog.js index 30bc3d72f33..770e6cb1320 100644 --- a/web-v2/web/src/app/catalogs/rightContent/CreateTableDialog.js +++ b/web-v2/web/src/app/catalogs/rightContent/CreateTableDialog.js @@ -47,6 +47,7 @@ import { TreeRefContext } from '../page' import Icons from '@/components/Icons' import { useResetFormOnCloseModal } from '@/lib/hooks/use-reset' import ColumnTypeComponent from '@/components/ColumnTypeComponent' +import PartitionPanel from '@/components/PartitionPanel' import RenderPropertiesFormItem from '@/components/EntityPropertiesFormItem' import { validateMessages, mismatchName } from '@/config' import { @@ -435,10 +436,29 @@ export default function CreateTableDialog({ ...props }) { let idxPartiton = 0 if (table.partitioning?.length) { table.partitioning.forEach(item => { - const fields = item.fieldName || item.fieldNames.map(f => f[0]) + const fields = item.fieldName || item.fieldNames?.map(f => f[0]) form.setFieldValue(['partitions', idxPartiton, 'strategy'], item.strategy) form.setFieldValue(['partitions', idxPartiton, 'fieldName'], fields) form.setFieldValue(['partitions', idxPartiton, 'number'], item.numBuckets || item.width || item.number) + if (item.strategy === 'range' && item.assignments?.length) { + form.setFieldValue( + ['partitions', idxPartiton, 'assignments'], + item.assignments.map(a => ({ + name: a.name, + upper: a.upper?.value ?? '', + lower: a.lower?.value ?? '' + })) + ) + } + if (item.strategy === 'list' && item.assignments?.length) { + form.setFieldValue( + ['partitions', idxPartiton, 'listAssignments'], + item.assignments.map(a => ({ + name: a.name, + listGroups: a.lists?.map(list => list.map(l => l.value ?? '').join(', ')) || [''] + })) + ) + } idxPartiton++ }) } @@ -668,14 +688,80 @@ export default function CreateTableDialog({ ...props }) { if (partitioningInfo) { submitData['partitioning'] = values.partitions?.map(p => { const field = {} + + // Shared column type -> literal dataType mapping for range and list partitions + const columnType = + values.columns?.find(c => c?.name === p.fieldName)?.typeObj?.type?.split('(')[0] || 'string' + + const dataType = + { + integer: 'integer', + long: 'long', + short: 'short', + float: 'float', + double: 'double', + decimal: 'decimal', + date: 'date', + time: 'time', + timestamp: 'timestamp', + timestamp_tz: 'timestamp_tz', + string: 'varchar', + varchar: 'varchar', + char: 'char', + boolean: 'boolean' + }[columnType] || 'varchar' + if (p.strategy === 'list') { + // fieldNames is String[][] (each inner array is one column name); fieldName is String[] field['fieldNames'] = [[p.fieldName]] + if (p.listAssignments?.length) { + // Number of partition columns (fieldNames is String[][], each inner array is one column name) + const numPartitionCols = field['fieldNames']?.length || 1 + field['assignments'] = p.listAssignments.map(a => { + // listGroups: array of comma-separated value strings + // Each group becomes one or more rows in LiteralDTO[][] (one IN tuple per row) + // For single-column partition: 'v1, v2, v3' -> [[{v1}], [{v2}], [{v3}]] + // For multi-column partition: 'v1, v2' -> [[{v1}, {v2}]] + const lists = (a.listGroups || []) + .filter(g => g && g.trim() !== '') + .flatMap(group => { + const vals = group + .split(',') + .map(v => v.trim()) + .filter(v => v !== '') + if (numPartitionCols === 1) { + return vals.map(v => [{ type: 'literal', dataType, value: v }]) + } else { + return [vals.map(v => ({ type: 'literal', dataType, value: v }))] + } + }) + + return { type: 'list', name: a.name, lists, properties: null } + }) + } } else if (p.strategy === 'bucket') { field['numBuckets'] = p.number field['fieldNames'] = [[p.fieldName]] } else if (p.strategy === 'truncate') { field['width'] = p.number field['fieldName'] = [p.fieldName] + } else if (p.strategy === 'range') { + // fieldName is String[] for range (not fieldNames) + field['fieldName'] = [p.fieldName] + if (p.assignments?.length) { + field['assignments'] = p.assignments.map(a => { + const assignment = { type: 'range', name: a.name } + assignment.upper = a.upper + ? { type: 'literal', dataType, value: a.upper } + : { type: 'literal', dataType: 'null', value: null } + assignment.lower = a.lower + ? { type: 'literal', dataType, value: a.lower } + : { type: 'literal', dataType: 'null', value: null } + assignment.properties = null + + return assignment + }) + } } else { field['fieldName'] = [p.fieldName] } @@ -1040,95 +1126,6 @@ export default function CreateTableDialog({ ...props }) { ) } - const renderTablePartitions = (fields, subOpt) => { - return ( -
-
-
Field
-
Strategy
-
Action
-
- {fields.map(subField => ( -
-
-
- - - -
-
- - - - {['truncate', 'bucket'].includes(form.getFieldValue(['partitions', subField.name, 'strategy'])) && ( - - - - )} -
-
- { - if (!!editTable) return - subOpt.remove(subField.name) - }} - /> -
-
-
- ))} -
- -
-
- ) - } - const renderTableSortOrders = (fields, subOpt) => { return (
@@ -1439,9 +1436,9 @@ export default function CreateTableDialog({ ...props }) { {(fields, subOpt) => renderTableColumns(fields, subOpt)} {partitioningInfo && ( - - {(fields, subOpt) => renderTablePartitions(fields, subOpt)} - +
+ +
)} {sortOredsInfo && ( diff --git a/web-v2/web/src/app/catalogs/rightContent/entitiesContent/TableDetailsPage.js b/web-v2/web/src/app/catalogs/rightContent/entitiesContent/TableDetailsPage.js index 90ad4a0d2e7..850b937af9f 100644 --- a/web-v2/web/src/app/catalogs/rightContent/entitiesContent/TableDetailsPage.js +++ b/web-v2/web/src/app/catalogs/rightContent/entitiesContent/TableDetailsPage.js @@ -142,7 +142,7 @@ export default function TableDetailsPage({ ...props }) { const partitioning = store.activatedDetails?.partitioning?.map((i, index) => { let fields = i.fieldName || [] let sub = '' - let last = i.fieldName + let last = i.fieldName || '' switch (i.strategy) { case 'bucket': @@ -157,6 +157,14 @@ export default function TableDetailsPage({ ...props }) { fields = i.fieldNames last = i.fieldNames.map(v => v[0]).join(',') break + case 'range': + if (i.assignments?.length) { + sub = `[${i.assignments.length} partitions]` + } + + // range uses fieldName (String[]), not fieldNames (String[][]) + last = i.fieldName || (i.fieldNames ? i.fieldNames.map(v => v[0]).join(',') : '') + break case 'function': sub = `[${i.funcName}]` fields = i.funcArgs.map(v => v.fieldName) @@ -173,6 +181,7 @@ export default function TableDetailsPage({ ...props }) { width: i.width, funcName: i.funcName, fields, + assignments: i.assignments, text: `${i.strategy}${sub}(${last})` } }) diff --git a/web-v2/web/src/components/PartitionPanel.js b/web-v2/web/src/components/PartitionPanel.js new file mode 100644 index 00000000000..5172702335e --- /dev/null +++ b/web-v2/web/src/components/PartitionPanel.js @@ -0,0 +1,475 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useCallback } from 'react' + +import { DeleteOutlined, PlusOutlined } from '@ant-design/icons' +import { Button, DatePicker, Flex, Form, Input, InputNumber, Select } from 'antd' +import dayjs from 'dayjs' +import Icons from '@/components/Icons' +import { partitionInfoMap, transformsLimitMap } from '@/config' +import { capitalizeFirstLetter } from '@/lib/utils' +import { cn } from '@/lib/utils/tailwind' + +const dateTypes = ['date', 'time', 'timestamp', 'timestamp_tz'] + +/** Helper: get the base column type for a partition row */ +function getColumnBaseType(form, partitionIndex) { + const fieldName = form.getFieldValue(['partitions', partitionIndex, 'fieldName']) + const cols = form.getFieldValue('columns') || [] + const column = cols.find(c => c?.name === fieldName) + + return column?.typeObj?.type?.split('(')[0] +} + +/** Helper: ensure Doris list-partition columns are set to NOT NULL */ +function ensureDorisListNotNull(form, provider, partitionIndex) { + if (provider !== 'jdbc-doris') return + const strategy = form.getFieldValue(['partitions', partitionIndex, 'strategy']) + if (strategy !== 'list') return + const fieldName = form.getFieldValue(['partitions', partitionIndex, 'fieldName']) + if (!fieldName) return + const columns = form.getFieldValue('columns') || [] + const colIdx = columns.findIndex(c => c?.name === fieldName) + if (colIdx >= 0 && !columns[colIdx]?.required) { + form.setFieldValue(['columns', colIdx, 'required'], true) + } +} + +/** Helper: create a stable key for assignment items to avoid index-based keys */ +let assignmentIdCounter = 0 +function newAssignmentId() { + return `assign-${++assignmentIdCounter}` +} + +function BoundInput({ columnType, value, onChange, ...props }) { + if (dateTypes.includes(columnType)) { + return ( + { + // dateString can be string or string[] when multiple formats are configured + const val = Array.isArray(dateString) ? dateString[0] : dateString + onChange?.(val || '') + }} + {...props} + /> + ) + } + + return onChange?.(e.target.value)} {...props} /> +} + +export default function PartitionPanel({ form, editTable, provider }) { + const partitioningInfo = partitionInfoMap[provider] + + return ( + + {(fields, subOpt) => ( +
+
+
Field
+
Strategy
+
Action
+
+ {fields.map(subField => ( + + ))} +
+ +
+
+ )} +
+ ) +} + +function PartitionRow({ form, editTable, subField, subOpt, partitioningInfo, provider }) { + // Only watch strategy to control visibility of number input and range/list assignments. + // Do NOT watch 'columns' or 'fieldName' here — reading them via form.getFieldValue + // inside callbacks and render avoids unnecessary re-renders that reset assignment values. + const currentStrategy = Form.useWatch(['partitions', subField.name, 'strategy'], form) + const idx = subField.name + + const handleFieldChange = useCallback(() => { + // Clear bound values when field changes, since the column type may differ + const assignments = form.getFieldValue(['partitions', idx, 'assignments']) + if (assignments?.length > 0) { + form.setFieldValue( + ['partitions', idx, 'assignments'], + assignments.map(a => ({ ...a, upper: undefined, lower: undefined })) + ) + } + + // Also clear list assignments since column type may differ + const listAssignments = form.getFieldValue(['partitions', idx, 'listAssignments']) + if (listAssignments?.length > 0) { + form.setFieldValue(['partitions', idx, 'listAssignments'], []) + } + ensureDorisListNotNull(form, provider, idx) + }, [form, provider, idx]) + + const handleStrategyChange = useCallback(() => { + const newStrategy = form.getFieldValue(['partitions', idx, 'strategy']) + + // Clear assignments when strategy changes away from range/list + if (newStrategy !== 'range') { + form.setFieldValue(['partitions', idx, 'assignments'], []) + } + if (newStrategy !== 'list') { + form.setFieldValue(['partitions', idx, 'listAssignments'], []) + } + ensureDorisListNotNull(form, provider, idx) + }, [form, provider, idx]) + + const columnOptions = (() => { + const cols = form.getFieldValue('columns') || [] + + return cols + .filter(col => col?.name) + .map(col => ( + + + {col?.name} + {col?.typeObj?.type} + + + )) + })() + + const strategyOptions = (() => { + const fieldName = form.getFieldValue(['partitions', idx, 'fieldName']) + const type = getColumnBaseType(form, idx) + + return partitioningInfo + ?.filter(s => transformsLimitMap[s]?.includes(type) || !transformsLimitMap[s]) + .map(s => ( + + {capitalizeFirstLetter(s)} + + )) + })() + + return ( +
+
+
+ + + +
+
+ + + + {['truncate', 'bucket'].includes(currentStrategy) && ( + + + + )} +
+
+ { + if (!!editTable) return + subOpt.remove(subField.name) + }} + /> +
+
+ {/* Always render assignment panels but hide with CSS when not active. + This prevents Form.List unmount/remount which loses field values. */} +
+ +
+
+ +
+
+ ) +} + +function RangeAssignments({ editTable, partitionIndex, form }) { + // Read assignments directly from form store to avoid nested Form.List path resolution issues. + // Nested Form.List inside another Form.List causes field path conflicts where updating + // one field inadvertently resets siblings (name/upper/lower overwrite each other). + const assignments = Form.useWatch(['partitions', partitionIndex, 'assignments'], form) || [] + + const onFieldChange = (assignIdx, field, value) => { + const current = form.getFieldValue(['partitions', partitionIndex, 'assignments']) || [] + + const updated = current.map((item, idx) => (idx === assignIdx ? { ...item, [field]: value } : item)) + form.setFieldValue(['partitions', partitionIndex, 'assignments'], updated) + } + + const addAssignment = () => { + const current = form.getFieldValue(['partitions', partitionIndex, 'assignments']) || [] + form.setFieldValue( + ['partitions', partitionIndex, 'assignments'], + [...current, { _key: newAssignmentId(), name: '', upper: '', lower: '' }] + ) + } + + const removeAssignment = assignIdx => { + const current = form.getFieldValue(['partitions', partitionIndex, 'assignments']) || [] + form.setFieldValue( + ['partitions', partitionIndex, 'assignments'], + current.filter((_, idx) => idx !== assignIdx) + ) + } + + const columnType = getColumnBaseType(form, partitionIndex) + + return ( +
+
Range Partitions
+
+
+
Name
+
Upper Bound
+
Lower Bound
+
+
+ {assignments.map((assignItem, assignIdx) => ( +
+
+ onFieldChange(assignIdx, 'name', e.target.value)} + /> +
+
+ onFieldChange(assignIdx, 'upper', value ?? '')} + /> +
+
+ onFieldChange(assignIdx, 'lower', value ?? '')} + /> +
+
+ { + if (!!editTable) return + removeAssignment(assignIdx) + }} + /> +
+
+ ))} + +
+
+ ) +} + +function ListAssignments({ editTable, partitionIndex, form }) { + const assignments = Form.useWatch(['partitions', partitionIndex, 'listAssignments'], form) || [] + + const onFieldChange = (assignIdx, field, value) => { + const current = form.getFieldValue(['partitions', partitionIndex, 'listAssignments']) || [] + + const updated = current.map((item, idx) => (idx === assignIdx ? { ...item, [field]: value } : item)) + form.setFieldValue(['partitions', partitionIndex, 'listAssignments'], updated) + } + + const onListGroupChange = (assignIdx, groupIdx, value) => { + const current = form.getFieldValue(['partitions', partitionIndex, 'listAssignments']) || [] + const item = current[assignIdx] + if (!item) return + const newGroups = [...(item.listGroups || [])] + newGroups[groupIdx] = value + + const updated = current.map((it, idx) => (idx === assignIdx ? { ...it, listGroups: newGroups } : it)) + form.setFieldValue(['partitions', partitionIndex, 'listAssignments'], updated) + } + + const addListGroup = assignIdx => { + const current = form.getFieldValue(['partitions', partitionIndex, 'listAssignments']) || [] + const item = current[assignIdx] + if (!item) return + const newGroups = [...(item.listGroups || []), ''] + + const updated = current.map((it, idx) => (idx === assignIdx ? { ...it, listGroups: newGroups } : it)) + form.setFieldValue(['partitions', partitionIndex, 'listAssignments'], updated) + } + + const removeListGroup = (assignIdx, groupIdx) => { + const current = form.getFieldValue(['partitions', partitionIndex, 'listAssignments']) || [] + const item = current[assignIdx] + if (!item) return + const newGroups = (item.listGroups || []).filter((_, i) => i !== groupIdx) + + const updated = current.map((it, idx) => (idx === assignIdx ? { ...it, listGroups: newGroups } : it)) + form.setFieldValue(['partitions', partitionIndex, 'listAssignments'], updated) + } + + const addAssignment = () => { + const current = form.getFieldValue(['partitions', partitionIndex, 'listAssignments']) || [] + form.setFieldValue( + ['partitions', partitionIndex, 'listAssignments'], + [...current, { _key: newAssignmentId(), name: '', listGroups: [''] }] + ) + } + + const removeAssignment = assignIdx => { + const current = form.getFieldValue(['partitions', partitionIndex, 'listAssignments']) || [] + form.setFieldValue( + ['partitions', partitionIndex, 'listAssignments'], + current.filter((_, idx) => idx !== assignIdx) + ) + } + + return ( +
+
List Partitions
+
+ {assignments.map((assignItem, assignIdx) => ( +
+
+
+
Name
+ onFieldChange(assignIdx, 'name', e.target.value)} + /> +
+
+
Value Groups (each group = one IN tuple)
+ {(assignItem?.listGroups || []).map((group, groupIdx) => ( +
+ 1 + ? 'val1, val2 (multi-column values for this group)' + : 'val1, val2, val3' + } + disabled={!!editTable} + value={group ?? ''} + onChange={e => onListGroupChange(assignIdx, groupIdx, e.target.value)} + /> + { + if (!!editTable) return + removeListGroup(assignIdx, groupIdx) + }} + /> +
+ ))} + +
+
+ { + if (!!editTable) return + removeAssignment(assignIdx) + }} + /> +
+
+
+ ))} + +
+
+ ) +} diff --git a/web-v2/web/src/config/index.js b/web-v2/web/src/config/index.js index 15c23285180..8366398942e 100644 --- a/web-v2/web/src/config/index.js +++ b/web-v2/web/src/config/index.js @@ -312,7 +312,8 @@ export const transformsLimitMap = { year: ['date', 'timestamp', 'timestamp_tz'], month: ['date', 'timestamp', 'timestamp_tz'], day: ['date', 'timestamp', 'timestamp_tz'], - hour: ['timestamp', 'timestamp_tz'] + hour: ['timestamp', 'timestamp_tz'], + range: ['integer', 'long', 'decimal', 'date', 'time', 'timestamp', 'timestamp_tz', 'string'] } export const sortOrdersInfoMap = { From 716ccf0ba7404024d10a1d5d26adb3a0c89711a0 Mon Sep 17 00:00:00 2001 From: Qian Xia Date: Mon, 20 Jul 2026 15:03:25 +0800 Subject: [PATCH 2/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../web/src/app/catalogs/rightContent/CreateTableDialog.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web-v2/web/src/app/catalogs/rightContent/CreateTableDialog.js b/web-v2/web/src/app/catalogs/rightContent/CreateTableDialog.js index 770e6cb1320..55eb2d6feb8 100644 --- a/web-v2/web/src/app/catalogs/rightContent/CreateTableDialog.js +++ b/web-v2/web/src/app/catalogs/rightContent/CreateTableDialog.js @@ -436,9 +436,9 @@ export default function CreateTableDialog({ ...props }) { let idxPartiton = 0 if (table.partitioning?.length) { table.partitioning.forEach(item => { - const fields = item.fieldName || item.fieldNames?.map(f => f[0]) - form.setFieldValue(['partitions', idxPartiton, 'strategy'], item.strategy) - form.setFieldValue(['partitions', idxPartiton, 'fieldName'], fields) +const fieldName = item.fieldName?.[0] ?? item.fieldNames?.[0]?.[0] ?? '' +form.setFieldValue(['partitions', idxPartiton, 'strategy'], item.strategy) +form.setFieldValue(['partitions', idxPartiton, 'fieldName'], fieldName) form.setFieldValue(['partitions', idxPartiton, 'number'], item.numBuckets || item.width || item.number) if (item.strategy === 'range' && item.assignments?.length) { form.setFieldValue( From 45e314f93077229626e5d486b70973fc9692a366 Mon Sep 17 00:00:00 2001 From: Qian Xia Date: Mon, 20 Jul 2026 15:21:04 +0800 Subject: [PATCH 3/3] fix by review comment --- .../rightContent/CreateTableDialog.js | 10 +++---- web-v2/web/src/components/PartitionPanel.js | 27 +++++++++++++------ 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/web-v2/web/src/app/catalogs/rightContent/CreateTableDialog.js b/web-v2/web/src/app/catalogs/rightContent/CreateTableDialog.js index 55eb2d6feb8..bf051ad6df4 100644 --- a/web-v2/web/src/app/catalogs/rightContent/CreateTableDialog.js +++ b/web-v2/web/src/app/catalogs/rightContent/CreateTableDialog.js @@ -436,9 +436,9 @@ export default function CreateTableDialog({ ...props }) { let idxPartiton = 0 if (table.partitioning?.length) { table.partitioning.forEach(item => { -const fieldName = item.fieldName?.[0] ?? item.fieldNames?.[0]?.[0] ?? '' -form.setFieldValue(['partitions', idxPartiton, 'strategy'], item.strategy) -form.setFieldValue(['partitions', idxPartiton, 'fieldName'], fieldName) + const fieldName = item.fieldName?.[0] ?? item.fieldNames?.[0]?.[0] ?? '' + form.setFieldValue(['partitions', idxPartiton, 'strategy'], item.strategy) + form.setFieldValue(['partitions', idxPartiton, 'fieldName'], fieldName) form.setFieldValue(['partitions', idxPartiton, 'number'], item.numBuckets || item.width || item.number) if (item.strategy === 'range' && item.assignments?.length) { form.setFieldValue( @@ -753,10 +753,10 @@ form.setFieldValue(['partitions', idxPartiton, 'fieldName'], fieldName) const assignment = { type: 'range', name: a.name } assignment.upper = a.upper ? { type: 'literal', dataType, value: a.upper } - : { type: 'literal', dataType: 'null', value: null } + : { type: 'literal', dataType: 'null', value: 'NULL' } assignment.lower = a.lower ? { type: 'literal', dataType, value: a.lower } - : { type: 'literal', dataType: 'null', value: null } + : { type: 'literal', dataType: 'null', value: 'NULL' } assignment.properties = null return assignment diff --git a/web-v2/web/src/components/PartitionPanel.js b/web-v2/web/src/components/PartitionPanel.js index 5172702335e..770993fdc58 100644 --- a/web-v2/web/src/components/PartitionPanel.js +++ b/web-v2/web/src/components/PartitionPanel.js @@ -20,14 +20,15 @@ import React, { useCallback } from 'react' import { DeleteOutlined, PlusOutlined } from '@ant-design/icons' -import { Button, DatePicker, Flex, Form, Input, InputNumber, Select } from 'antd' +import { Button, DatePicker, Flex, Form, Input, InputNumber, Select, TimePicker } from 'antd' import dayjs from 'dayjs' import Icons from '@/components/Icons' import { partitionInfoMap, transformsLimitMap } from '@/config' import { capitalizeFirstLetter } from '@/lib/utils' import { cn } from '@/lib/utils/tailwind' -const dateTypes = ['date', 'time', 'timestamp', 'timestamp_tz'] +const dateTypes = ['date', 'timestamp', 'timestamp_tz'] +const timeTypes = ['time'] /** Helper: get the base column type for a partition row */ function getColumnBaseType(form, partitionIndex) { @@ -59,6 +60,21 @@ function newAssignmentId() { } function BoundInput({ columnType, value, onChange, ...props }) { + if (timeTypes.includes(columnType)) { + return ( + { + const val = Array.isArray(timeString) ? timeString[0] : timeString + onChange?.(val || '') + }} + {...props} + /> + ) + } + if (dateTypes.includes(columnType)) { return ( { - const fieldName = form.getFieldValue(['partitions', idx, 'fieldName']) const type = getColumnBaseType(form, idx) return partitioningInfo @@ -422,11 +437,7 @@ function ListAssignments({ editTable, partitionIndex, form }) { 1 - ? 'val1, val2 (multi-column values for this group)' - : 'val1, val2, val3' - } + placeholder='val1, val2, val3' disabled={!!editTable} value={group ?? ''} onChange={e => onListGroupChange(assignIdx, groupIdx, e.target.value)}