@bsol-oss/react-datatable5 13.0.1-beta.0 → 13.0.1-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +71 -21
- package/dist/index.js +2119 -1250
- package/dist/index.mjs +2122 -1254
- package/dist/types/components/DatePicker/Calendar.d.ts +2 -0
- package/dist/types/components/DatePicker/DatePicker.d.ts +27 -0
- package/dist/types/components/DatePicker/DateTimePicker.d.ts +20 -4
- package/dist/types/components/DatePicker/UniversalPicker.d.ts +2 -3
- package/dist/types/components/DatePicker/index.d.ts +1 -2
- package/dist/types/components/Form/SchemaFormContext.d.ts +0 -1
- package/dist/types/components/Form/components/core/FormRoot.d.ts +1 -2
- package/dist/types/components/Form/components/fields/FilePicker.d.ts +1 -1
- package/dist/types/components/Form/components/fields/TagPicker.d.ts +1 -1
- package/dist/types/components/Form/components/fields/useIdPickerData.d.ts +8 -2
- package/dist/types/components/Form/components/types/CustomJSONSchema7.d.ts +43 -0
- package/dist/types/components/Form/components/viewers/EnumViewer.d.ts +1 -1
- package/dist/types/components/Form/components/viewers/TagViewer.d.ts +1 -1
- package/dist/types/components/Form/utils/getTableData.d.ts +1 -2
- package/dist/types/components/Form/utils/useFormI18n.d.ts +14 -30
- package/dist/types/components/TimePicker/TimePicker.d.ts +39 -10
- package/dist/types/index.d.ts +0 -1
- package/package.json +7 -4
- package/dist/types/components/DatePicker/DatePickerInput.d.ts +0 -18
- package/dist/types/components/DatePicker/IsoTimePicker.d.ts +0 -24
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
|
-
import { Button as Button$1, AbsoluteCenter, Spinner, Span, IconButton, Portal, Dialog, Flex, Text, useDisclosure, DialogBackdrop, RadioGroup as RadioGroup$1, Grid, Box, Slider as Slider$1, HStack, For, CheckboxCard as CheckboxCard$1, Input, Menu, createRecipeContext, createContext as createContext$1, Pagination as Pagination$1, usePaginationContext, Tooltip as Tooltip$1, Group, InputElement, Icon, EmptyState as EmptyState$2, VStack, List, Table as Table$1, Checkbox as Checkbox$1, Card, MenuRoot as MenuRoot$1, MenuTrigger as MenuTrigger$1, Clipboard, Badge, Link, Tag as Tag$1, Image, Alert, Field as Field$1, Popover, useFilter, useListCollection, Combobox, Tabs, Skeleton, NumberInput,
|
|
2
|
+
import { Button as Button$1, AbsoluteCenter, Spinner, Span, IconButton, Portal, Dialog, Flex, Text, useDisclosure, DialogBackdrop, RadioGroup as RadioGroup$1, Grid, Box, Slider as Slider$1, HStack, For, CheckboxCard as CheckboxCard$1, Input, Menu, createRecipeContext, createContext as createContext$1, Pagination as Pagination$1, usePaginationContext, Tooltip as Tooltip$1, Group, InputElement, Icon, EmptyState as EmptyState$2, VStack, List, Table as Table$1, Checkbox as Checkbox$1, Card, MenuRoot as MenuRoot$1, MenuTrigger as MenuTrigger$1, Clipboard, Badge, Link, Tag as Tag$1, Image, Alert, Field as Field$1, Popover, useFilter, useListCollection, Combobox, Tabs, useCombobox, Show, Skeleton, NumberInput, RadioCard, CheckboxGroup, InputGroup as InputGroup$1, Select, Center, Heading, Stack } from '@chakra-ui/react';
|
|
3
3
|
import { AiOutlineColumnWidth } from 'react-icons/ai';
|
|
4
4
|
import * as React from 'react';
|
|
5
5
|
import { createContext, useContext, useState, useMemo, useCallback, useEffect, useRef, forwardRef } from 'react';
|
|
6
6
|
import { LuX, LuCheck, LuChevronRight, LuCopy, LuExternalLink, LuSearch, LuImage, LuFile } from 'react-icons/lu';
|
|
7
7
|
import { MdOutlineSort, MdFilterAlt, MdSearch, MdOutlineChecklist, MdClear, MdOutlineViewColumn, MdFilterListAlt, MdPushPin, MdCancel, MdDateRange } from 'react-icons/md';
|
|
8
|
-
import { FaUpDown, FaGripLinesVertical
|
|
9
|
-
import { BiDownArrow, BiUpArrow, BiError } from 'react-icons/bi';
|
|
8
|
+
import { FaUpDown, FaGripLinesVertical } from 'react-icons/fa6';
|
|
9
|
+
import { BiDownArrow, BiUpArrow, BiX, BiError } from 'react-icons/bi';
|
|
10
10
|
import { CgClose, CgTrash } from 'react-icons/cg';
|
|
11
11
|
import { HiMiniEllipsisHorizontal, HiChevronLeft, HiChevronRight } from 'react-icons/hi2';
|
|
12
12
|
import { IoMdEye, IoMdCheckbox, IoMdClock } from 'react-icons/io';
|
|
@@ -28,10 +28,10 @@ import { FormProvider, useFormContext, useForm as useForm$1 } from 'react-hook-f
|
|
|
28
28
|
import Ajv from 'ajv';
|
|
29
29
|
import addFormats from 'ajv-formats';
|
|
30
30
|
import dayjs from 'dayjs';
|
|
31
|
-
import
|
|
31
|
+
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
|
32
32
|
import timezone from 'dayjs/plugin/timezone';
|
|
33
|
+
import utc from 'dayjs/plugin/utc';
|
|
33
34
|
import { TiDeleteOutline } from 'react-icons/ti';
|
|
34
|
-
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
|
35
35
|
import { rankItem } from '@tanstack/match-sorter-utils';
|
|
36
36
|
|
|
37
37
|
const DataTableContext = createContext({
|
|
@@ -3694,7 +3694,7 @@ const TextWithCopy = ({ text, globalFilter, highlightedText, }) => {
|
|
|
3694
3694
|
const displayText = highlightedText !== undefined
|
|
3695
3695
|
? highlightedText
|
|
3696
3696
|
: highlightText$1(textValue, globalFilter);
|
|
3697
|
-
return (jsxs(HStack, { gap: 2, alignItems: "center", children: [jsx(Text, { as: "span", children: displayText }), jsx(Clipboard.Root, { value: textValue, children: jsx(Clipboard.Trigger, { asChild: true, children: jsx(IconButton, { size: "
|
|
3697
|
+
return (jsxs(HStack, { gap: 2, alignItems: "center", children: [jsx(Text, { as: "span", children: displayText }), jsx(Clipboard.Root, { value: textValue, children: jsx(Clipboard.Trigger, { asChild: true, children: jsx(IconButton, { size: "2xs", variant: "ghost", "aria-label": "Copy", fontSize: "1em", children: jsx(Clipboard.Indicator, { copied: jsx(LuCheck, {}), children: jsx(LuCopy, {}) }) }) }) })] }));
|
|
3698
3698
|
};
|
|
3699
3699
|
|
|
3700
3700
|
// Helper function to highlight matching text
|
|
@@ -4016,7 +4016,6 @@ const getColumns = ({ schema, include = [], ignore = [], width = [], meta = {},
|
|
|
4016
4016
|
//@ts-expect-error TODO: find appropriate type
|
|
4017
4017
|
const SchemaFormContext = createContext({
|
|
4018
4018
|
schema: {},
|
|
4019
|
-
serverUrl: '',
|
|
4020
4019
|
requestUrl: '',
|
|
4021
4020
|
order: [],
|
|
4022
4021
|
ignore: [],
|
|
@@ -4127,6 +4126,22 @@ const convertAjvErrorsToFieldErrors = (errors, schema) => {
|
|
|
4127
4126
|
// Get the schema node for this field to check for custom error messages
|
|
4128
4127
|
const fieldSchema = getSchemaNodeForField(schema, fieldName);
|
|
4129
4128
|
const customMessage = fieldSchema?.errorMessages?.[error.keyword];
|
|
4129
|
+
// Debug log when error message is missing
|
|
4130
|
+
if (!customMessage) {
|
|
4131
|
+
console.debug(`[Form Validation] Missing error message for field '${fieldName}' with keyword '${error.keyword}'. Add errorMessages.${error.keyword} to schema for field '${fieldName}'`, {
|
|
4132
|
+
fieldName,
|
|
4133
|
+
keyword: error.keyword,
|
|
4134
|
+
instancePath: error.instancePath,
|
|
4135
|
+
schemaPath: error.schemaPath,
|
|
4136
|
+
params: error.params,
|
|
4137
|
+
fieldSchema: fieldSchema
|
|
4138
|
+
? {
|
|
4139
|
+
type: fieldSchema.type,
|
|
4140
|
+
errorMessages: fieldSchema.errorMessages,
|
|
4141
|
+
}
|
|
4142
|
+
: undefined,
|
|
4143
|
+
});
|
|
4144
|
+
}
|
|
4130
4145
|
// Provide helpful fallback message if no custom message is provided
|
|
4131
4146
|
const fallbackMessage = customMessage ||
|
|
4132
4147
|
`Missing error message for ${error.keyword}. Add errorMessages.${error.keyword} to schema for field '${fieldName}'`;
|
|
@@ -4256,7 +4271,7 @@ const idPickerSanityCheck = (column, foreign_key) => {
|
|
|
4256
4271
|
throw new Error(`The key column does not exist in properties of column ${column} when using id-picker.`);
|
|
4257
4272
|
}
|
|
4258
4273
|
};
|
|
4259
|
-
const FormRoot = ({ schema, idMap, setIdMap, form,
|
|
4274
|
+
const FormRoot = ({ schema, idMap, setIdMap, form, translate, children, order = [], ignore = [], include = [], onSubmit = undefined, rowNumber = undefined, requestOptions = {}, getUpdatedData = () => { }, customErrorRenderer, customSuccessRenderer, displayConfig = {
|
|
4260
4275
|
showSubmitButton: true,
|
|
4261
4276
|
showResetButton: true,
|
|
4262
4277
|
showTitle: true,
|
|
@@ -4297,9 +4312,11 @@ const FormRoot = ({ schema, idMap, setIdMap, form, serverUrl, translate, childre
|
|
|
4297
4312
|
}
|
|
4298
4313
|
};
|
|
4299
4314
|
const defaultSubmitPromise = (data) => {
|
|
4315
|
+
if (!requestOptions.url) {
|
|
4316
|
+
throw new Error('requestOptions.url is required when onSubmit is not provided');
|
|
4317
|
+
}
|
|
4300
4318
|
const options = {
|
|
4301
4319
|
method: 'POST',
|
|
4302
|
-
url: `${serverUrl}`,
|
|
4303
4320
|
data: clearEmptyString(data),
|
|
4304
4321
|
...requestOptions,
|
|
4305
4322
|
};
|
|
@@ -4317,7 +4334,6 @@ const FormRoot = ({ schema, idMap, setIdMap, form, serverUrl, translate, childre
|
|
|
4317
4334
|
};
|
|
4318
4335
|
return (jsx(SchemaFormContext.Provider, { value: {
|
|
4319
4336
|
schema,
|
|
4320
|
-
serverUrl,
|
|
4321
4337
|
order,
|
|
4322
4338
|
ignore,
|
|
4323
4339
|
include,
|
|
@@ -4357,39 +4373,31 @@ const FormRoot = ({ schema, idMap, setIdMap, form, serverUrl, translate, childre
|
|
|
4357
4373
|
}, children: jsx(FormProvider, { ...form, children: children }) }));
|
|
4358
4374
|
};
|
|
4359
4375
|
|
|
4360
|
-
function removeIndex(str) {
|
|
4361
|
-
return str.replace(/\.\d+\./g, ".");
|
|
4362
|
-
}
|
|
4363
|
-
|
|
4364
4376
|
/**
|
|
4365
|
-
* Custom hook for form field labels
|
|
4366
|
-
* Automatically handles colLabel construction
|
|
4367
|
-
* Uses schema.title
|
|
4377
|
+
* Custom hook for form field labels.
|
|
4378
|
+
* Automatically handles colLabel construction.
|
|
4379
|
+
* Uses schema.title for labels and schema.errorMessages for error messages.
|
|
4368
4380
|
*
|
|
4369
4381
|
* @param column - The column name
|
|
4370
4382
|
* @param prefix - The prefix for the field (usually empty string or parent path)
|
|
4371
|
-
* @param schema -
|
|
4383
|
+
* @param schema - Required schema object with title and errorMessages properties
|
|
4372
4384
|
* @returns Object with label helper functions
|
|
4373
4385
|
*
|
|
4374
4386
|
* @example
|
|
4375
4387
|
* ```tsx
|
|
4376
4388
|
* const formI18n = useFormI18n(column, prefix, schema);
|
|
4377
4389
|
*
|
|
4378
|
-
* // Get field label (
|
|
4390
|
+
* // Get field label (from schema.title)
|
|
4379
4391
|
* <Field label={formI18n.label()} />
|
|
4380
4392
|
*
|
|
4381
|
-
* // Get required error message
|
|
4393
|
+
* // Get required error message (from schema.errorMessages?.required)
|
|
4382
4394
|
* <Text>{formI18n.required()}</Text>
|
|
4383
4395
|
*
|
|
4384
|
-
* // Get custom text
|
|
4385
|
-
* <Text>{formI18n.t('add_more')}</Text>
|
|
4386
|
-
*
|
|
4387
4396
|
* // Access the raw colLabel
|
|
4388
4397
|
* const colLabel = formI18n.colLabel;
|
|
4389
4398
|
* ```
|
|
4390
4399
|
*/
|
|
4391
4400
|
const useFormI18n = (column, prefix = '', schema) => {
|
|
4392
|
-
const { translate } = useSchemaContext();
|
|
4393
4401
|
const colLabel = `${prefix}${column}`;
|
|
4394
4402
|
return {
|
|
4395
4403
|
/**
|
|
@@ -4397,36 +4405,55 @@ const useFormI18n = (column, prefix = '', schema) => {
|
|
|
4397
4405
|
*/
|
|
4398
4406
|
colLabel,
|
|
4399
4407
|
/**
|
|
4400
|
-
* Get the field label from schema title
|
|
4401
|
-
*
|
|
4408
|
+
* Get the field label from schema title property.
|
|
4409
|
+
* Logs a debug message if title is missing.
|
|
4402
4410
|
*/
|
|
4403
|
-
label: (
|
|
4404
|
-
if (schema
|
|
4411
|
+
label: () => {
|
|
4412
|
+
if (schema.title) {
|
|
4405
4413
|
return schema.title;
|
|
4406
4414
|
}
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
+
// Debug log when field title is missing
|
|
4416
|
+
console.debug(`[Form Field Label] Missing title for field '${colLabel}'. Add title property to schema for field '${colLabel}'.`, {
|
|
4417
|
+
fieldName: column,
|
|
4418
|
+
colLabel,
|
|
4419
|
+
prefix,
|
|
4420
|
+
schema: {
|
|
4421
|
+
type: schema.type,
|
|
4422
|
+
errorMessages: schema.errorMessages
|
|
4423
|
+
? Object.keys(schema.errorMessages)
|
|
4424
|
+
: undefined,
|
|
4425
|
+
},
|
|
4426
|
+
});
|
|
4427
|
+
// Return column name as fallback
|
|
4428
|
+
return column;
|
|
4415
4429
|
},
|
|
4416
4430
|
/**
|
|
4417
|
-
* Get
|
|
4418
|
-
*
|
|
4419
|
-
*
|
|
4420
|
-
* @param key - The key suffix (e.g., 'add_more', 'total', etc.)
|
|
4421
|
-
* @param options - Optional options (e.g., defaultValue, interpolation variables)
|
|
4431
|
+
* Get the required error message from schema.errorMessages?.required.
|
|
4432
|
+
* Returns a helpful fallback message if not provided.
|
|
4422
4433
|
*/
|
|
4423
|
-
|
|
4424
|
-
|
|
4434
|
+
required: () => {
|
|
4435
|
+
const errorMessage = schema.errorMessages?.required;
|
|
4436
|
+
if (errorMessage) {
|
|
4437
|
+
return errorMessage;
|
|
4438
|
+
}
|
|
4439
|
+
// Debug log when error message is missing
|
|
4440
|
+
console.debug(`[Form Field Required] Missing error message for required field '${colLabel}'. Add errorMessages.required to schema for field '${colLabel}'.`, {
|
|
4441
|
+
fieldName: column,
|
|
4442
|
+
colLabel,
|
|
4443
|
+
prefix,
|
|
4444
|
+
schema: {
|
|
4445
|
+
type: schema.type,
|
|
4446
|
+
title: schema.title,
|
|
4447
|
+
required: schema.required,
|
|
4448
|
+
hasErrorMessages: !!schema.errorMessages,
|
|
4449
|
+
errorMessageKeys: schema.errorMessages
|
|
4450
|
+
? Object.keys(schema.errorMessages)
|
|
4451
|
+
: undefined,
|
|
4452
|
+
},
|
|
4453
|
+
});
|
|
4454
|
+
// Return helpful fallback message
|
|
4455
|
+
return `Missing error message for required. Add errorMessages.required to schema for field '${colLabel}'`;
|
|
4425
4456
|
},
|
|
4426
|
-
/**
|
|
4427
|
-
* Access to the original translate object for edge cases
|
|
4428
|
-
*/
|
|
4429
|
-
translate,
|
|
4430
4457
|
};
|
|
4431
4458
|
};
|
|
4432
4459
|
|
|
@@ -4501,52 +4528,56 @@ const Calendar = ({ calendars, getBackProps, getForwardProps, getDateProps, firs
|
|
|
4501
4528
|
const { labels } = useContext(DatePickerContext);
|
|
4502
4529
|
const { monthNamesShort, weekdayNamesShort, backButtonLabel, forwardButtonLabel, } = labels;
|
|
4503
4530
|
if (calendars.length) {
|
|
4504
|
-
return (jsxs(Grid, { children: [jsxs(Grid, { templateColumns: 'repeat(
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4531
|
+
return (jsx(Grid, { children: jsx(Grid, { templateColumns: 'repeat(2, auto)', justifyContent: 'center', children: calendars.map((calendar) => (jsxs(Grid, { gap: 2, children: [jsxs(Grid, { templateColumns: 'repeat(6, auto)', justifyContent: 'center', alignItems: 'center', gap: 2, children: [jsx(Button$1, { variant: 'ghost', size: 'sm', colorPalette: 'gray', ...getBackProps({ calendars }), children: '<' }), jsx(Text, { textAlign: 'center', children: monthNamesShort[calendar.month] }), jsx(Button$1, { variant: 'ghost', size: 'sm', colorPalette: 'gray', ...getForwardProps({ calendars }), children: '>' }), jsx(Button$1, { variant: 'ghost', size: 'sm', colorPalette: 'gray', ...getBackProps({
|
|
4532
|
+
calendars,
|
|
4533
|
+
offset: 12,
|
|
4534
|
+
}), children: '<' }), jsx(Text, { textAlign: 'center', children: calendar.year }), jsx(Button$1, { variant: 'ghost', size: 'sm', colorPalette: 'gray', ...getForwardProps({
|
|
4535
|
+
calendars,
|
|
4536
|
+
offset: 12,
|
|
4537
|
+
}), children: '>' })] }), jsxs(Grid, { templateColumns: 'repeat(7, auto)', justifyContent: 'center', children: [[0, 1, 2, 3, 4, 5, 6].map((weekdayNum) => {
|
|
4538
|
+
const weekday = (weekdayNum + firstDayOfWeek) % 7;
|
|
4539
|
+
return (jsx(Text, { textAlign: 'center', children: weekdayNamesShort[weekday] }, `${calendar.month}${calendar.year}${weekday}`));
|
|
4540
|
+
}), calendar.weeks.map((week, weekIndex) => week.map((dateObj, index) => {
|
|
4541
|
+
const key = `${calendar.month}${calendar.year}${weekIndex}${index}`;
|
|
4542
|
+
if (!dateObj) {
|
|
4543
|
+
return jsx(Grid, {}, key);
|
|
4544
|
+
}
|
|
4545
|
+
const { date, selected, selectable, today, isCurrentMonth, } = dateObj;
|
|
4546
|
+
const getDateColor = ({ today, selected, selectable, }) => {
|
|
4547
|
+
if (!selectable) {
|
|
4548
|
+
return 'gray';
|
|
4517
4549
|
}
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
};
|
|
4543
|
-
const color = getDateColor({ today, selected, selectable });
|
|
4544
|
-
const variant = getVariant({ today, selected, selectable });
|
|
4545
|
-
return (jsx(Button$1, { variant: variant, colorPalette: color, opacity: isCurrentMonth ? 1 : 0.4, ...getDateProps({ dateObj }), children: selectable ? date.getDate() : 'X' }, key));
|
|
4546
|
-
}))] })] }, `${calendar.month}${calendar.year}`))) })] }));
|
|
4550
|
+
if (selected) {
|
|
4551
|
+
return 'blue';
|
|
4552
|
+
}
|
|
4553
|
+
if (today) {
|
|
4554
|
+
return 'green';
|
|
4555
|
+
}
|
|
4556
|
+
return '';
|
|
4557
|
+
};
|
|
4558
|
+
const getVariant = ({ today, selected, selectable, }) => {
|
|
4559
|
+
if (!selectable) {
|
|
4560
|
+
return 'surface';
|
|
4561
|
+
}
|
|
4562
|
+
if (selected) {
|
|
4563
|
+
return 'surface';
|
|
4564
|
+
}
|
|
4565
|
+
if (today) {
|
|
4566
|
+
return 'outline';
|
|
4567
|
+
}
|
|
4568
|
+
return 'ghost';
|
|
4569
|
+
};
|
|
4570
|
+
const color = getDateColor({ today, selected, selectable });
|
|
4571
|
+
const variant = getVariant({ today, selected, selectable });
|
|
4572
|
+
return (jsx(Button$1, { variant: variant, colorPalette: color, size: 'xs', opacity: isCurrentMonth ? 1 : 0.4, ...getDateProps({ dateObj }), children: selectable ? date.getDate() : 'X' }, key));
|
|
4573
|
+
}))] })] }, `${calendar.month}${calendar.year}`))) }) }));
|
|
4547
4574
|
}
|
|
4548
4575
|
return null;
|
|
4549
4576
|
};
|
|
4577
|
+
|
|
4578
|
+
dayjs.extend(utc);
|
|
4579
|
+
dayjs.extend(timezone);
|
|
4580
|
+
dayjs.extend(customParseFormat);
|
|
4550
4581
|
const DatePickerContext = createContext({
|
|
4551
4582
|
labels: {
|
|
4552
4583
|
monthNamesShort: [
|
|
@@ -4566,6 +4597,9 @@ const DatePickerContext = createContext({
|
|
|
4566
4597
|
weekdayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
|
4567
4598
|
backButtonLabel: 'Back',
|
|
4568
4599
|
forwardButtonLabel: 'Next',
|
|
4600
|
+
todayLabel: 'Today',
|
|
4601
|
+
yesterdayLabel: 'Yesterday',
|
|
4602
|
+
tomorrowLabel: 'Tomorrow',
|
|
4569
4603
|
},
|
|
4570
4604
|
});
|
|
4571
4605
|
const DatePicker$1 = ({ labels = {
|
|
@@ -4586,6 +4620,9 @@ const DatePicker$1 = ({ labels = {
|
|
|
4586
4620
|
weekdayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
|
4587
4621
|
backButtonLabel: 'Back',
|
|
4588
4622
|
forwardButtonLabel: 'Next',
|
|
4623
|
+
todayLabel: 'Today',
|
|
4624
|
+
yesterdayLabel: 'Yesterday',
|
|
4625
|
+
tomorrowLabel: 'Tomorrow',
|
|
4589
4626
|
}, onDateSelected, selected, firstDayOfWeek, showOutsideDays, date, minDate, maxDate, monthsToDisplay, render, }) => {
|
|
4590
4627
|
const calendarData = useCalendar({
|
|
4591
4628
|
onDateSelected,
|
|
@@ -4600,9 +4637,171 @@ const DatePicker$1 = ({ labels = {
|
|
|
4600
4637
|
return (jsx(DatePickerContext.Provider, { value: { labels }, children: render ? (render(calendarData)) : (jsx(Calendar, { ...calendarData,
|
|
4601
4638
|
firstDayOfWeek })) }));
|
|
4602
4639
|
};
|
|
4640
|
+
function DatePickerInput({ value, onChange, placeholder = 'Select a date', dateFormat = 'YYYY-MM-DD', displayFormat = 'YYYY-MM-DD', labels = {
|
|
4641
|
+
monthNamesShort: [
|
|
4642
|
+
'Jan',
|
|
4643
|
+
'Feb',
|
|
4644
|
+
'Mar',
|
|
4645
|
+
'Apr',
|
|
4646
|
+
'May',
|
|
4647
|
+
'Jun',
|
|
4648
|
+
'Jul',
|
|
4649
|
+
'Aug',
|
|
4650
|
+
'Sep',
|
|
4651
|
+
'Oct',
|
|
4652
|
+
'Nov',
|
|
4653
|
+
'Dec',
|
|
4654
|
+
],
|
|
4655
|
+
weekdayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
|
4656
|
+
backButtonLabel: 'Back',
|
|
4657
|
+
forwardButtonLabel: 'Next',
|
|
4658
|
+
todayLabel: 'Today',
|
|
4659
|
+
yesterdayLabel: 'Yesterday',
|
|
4660
|
+
tomorrowLabel: 'Tomorrow',
|
|
4661
|
+
}, timezone = 'Asia/Hong_Kong', minDate, maxDate, firstDayOfWeek, showOutsideDays, monthsToDisplay = 1, insideDialog = false, readOnly = false, showHelperButtons = true, }) {
|
|
4662
|
+
const [open, setOpen] = useState(false);
|
|
4663
|
+
const [inputValue, setInputValue] = useState('');
|
|
4664
|
+
// Sync inputValue with value prop changes
|
|
4665
|
+
useEffect(() => {
|
|
4666
|
+
if (value) {
|
|
4667
|
+
const formatted = typeof value === 'string'
|
|
4668
|
+
? dayjs(value).tz(timezone).isValid()
|
|
4669
|
+
? dayjs(value).tz(timezone).format(displayFormat)
|
|
4670
|
+
: ''
|
|
4671
|
+
: dayjs(value).tz(timezone).format(displayFormat);
|
|
4672
|
+
setInputValue(formatted);
|
|
4673
|
+
}
|
|
4674
|
+
else {
|
|
4675
|
+
setInputValue('');
|
|
4676
|
+
}
|
|
4677
|
+
}, [value, timezone, displayFormat]);
|
|
4678
|
+
// Convert value to Date object for DatePicker
|
|
4679
|
+
const selectedDate = value
|
|
4680
|
+
? typeof value === 'string'
|
|
4681
|
+
? dayjs(value).tz(timezone).isValid()
|
|
4682
|
+
? dayjs(value).tz(timezone).toDate()
|
|
4683
|
+
: new Date()
|
|
4684
|
+
: value
|
|
4685
|
+
: new Date();
|
|
4686
|
+
// Shared function to parse and validate input value
|
|
4687
|
+
const parseAndValidateInput = (inputVal) => {
|
|
4688
|
+
// If empty, clear the value
|
|
4689
|
+
if (!inputVal.trim()) {
|
|
4690
|
+
onChange?.(undefined);
|
|
4691
|
+
setInputValue('');
|
|
4692
|
+
return;
|
|
4693
|
+
}
|
|
4694
|
+
// Try parsing with displayFormat first
|
|
4695
|
+
let parsedDate = dayjs(inputVal, displayFormat, true);
|
|
4696
|
+
// If that fails, try common date formats
|
|
4697
|
+
if (!parsedDate.isValid()) {
|
|
4698
|
+
parsedDate = dayjs(inputVal);
|
|
4699
|
+
}
|
|
4700
|
+
// If still invalid, try parsing with dateFormat
|
|
4701
|
+
if (!parsedDate.isValid()) {
|
|
4702
|
+
parsedDate = dayjs(inputVal, dateFormat, true);
|
|
4703
|
+
}
|
|
4704
|
+
// If valid, check constraints and update
|
|
4705
|
+
if (parsedDate.isValid()) {
|
|
4706
|
+
const dateObj = parsedDate.tz(timezone).toDate();
|
|
4707
|
+
// Check min/max constraints
|
|
4708
|
+
if (minDate && dateObj < minDate) {
|
|
4709
|
+
// Invalid: before minDate, reset to prop value
|
|
4710
|
+
resetToPropValue();
|
|
4711
|
+
return;
|
|
4712
|
+
}
|
|
4713
|
+
if (maxDate && dateObj > maxDate) {
|
|
4714
|
+
// Invalid: after maxDate, reset to prop value
|
|
4715
|
+
resetToPropValue();
|
|
4716
|
+
return;
|
|
4717
|
+
}
|
|
4718
|
+
// Valid date - format and update
|
|
4719
|
+
const formattedDate = parsedDate.tz(timezone).format(dateFormat);
|
|
4720
|
+
const formattedDisplay = parsedDate.tz(timezone).format(displayFormat);
|
|
4721
|
+
onChange?.(formattedDate);
|
|
4722
|
+
setInputValue(formattedDisplay);
|
|
4723
|
+
}
|
|
4724
|
+
else {
|
|
4725
|
+
// Invalid date - reset to prop value
|
|
4726
|
+
resetToPropValue();
|
|
4727
|
+
}
|
|
4728
|
+
};
|
|
4729
|
+
// Helper function to reset input to prop value
|
|
4730
|
+
const resetToPropValue = () => {
|
|
4731
|
+
if (value) {
|
|
4732
|
+
const formatted = typeof value === 'string'
|
|
4733
|
+
? dayjs(value).tz(timezone).isValid()
|
|
4734
|
+
? dayjs(value).tz(timezone).format(displayFormat)
|
|
4735
|
+
: ''
|
|
4736
|
+
: dayjs(value).tz(timezone).format(displayFormat);
|
|
4737
|
+
setInputValue(formatted);
|
|
4738
|
+
}
|
|
4739
|
+
else {
|
|
4740
|
+
setInputValue('');
|
|
4741
|
+
}
|
|
4742
|
+
};
|
|
4743
|
+
const handleInputChange = (e) => {
|
|
4744
|
+
// Only update the input value, don't parse yet
|
|
4745
|
+
setInputValue(e.target.value);
|
|
4746
|
+
};
|
|
4747
|
+
const handleInputBlur = () => {
|
|
4748
|
+
// Parse and validate when input loses focus
|
|
4749
|
+
parseAndValidateInput(inputValue);
|
|
4750
|
+
};
|
|
4751
|
+
const handleKeyDown = (e) => {
|
|
4752
|
+
// Parse and validate when Enter is pressed
|
|
4753
|
+
if (e.key === 'Enter') {
|
|
4754
|
+
e.preventDefault();
|
|
4755
|
+
parseAndValidateInput(inputValue);
|
|
4756
|
+
}
|
|
4757
|
+
};
|
|
4758
|
+
const handleDateSelected = ({ date }) => {
|
|
4759
|
+
console.debug('[DatePickerInput] handleDateSelected called:', {
|
|
4760
|
+
date: date.toISOString(),
|
|
4761
|
+
timezone,
|
|
4762
|
+
dateFormat,
|
|
4763
|
+
formattedDate: dayjs(date).tz(timezone).format(dateFormat),
|
|
4764
|
+
});
|
|
4765
|
+
const formattedDate = dayjs(date).tz(timezone).format(dateFormat);
|
|
4766
|
+
console.debug('[DatePickerInput] Calling onChange with formatted date:', formattedDate);
|
|
4767
|
+
onChange?.(formattedDate);
|
|
4768
|
+
setOpen(false);
|
|
4769
|
+
};
|
|
4770
|
+
// Helper function to get dates in the correct timezone
|
|
4771
|
+
const getToday = () => dayjs().tz(timezone).startOf('day').toDate();
|
|
4772
|
+
const getYesterday = () => dayjs().tz(timezone).subtract(1, 'day').startOf('day').toDate();
|
|
4773
|
+
const getTomorrow = () => dayjs().tz(timezone).add(1, 'day').startOf('day').toDate();
|
|
4774
|
+
// Check if a date is within min/max constraints
|
|
4775
|
+
const isDateValid = (date) => {
|
|
4776
|
+
if (minDate) {
|
|
4777
|
+
const minDateStart = dayjs(minDate).tz(timezone).startOf('day').toDate();
|
|
4778
|
+
const dateStart = dayjs(date).tz(timezone).startOf('day').toDate();
|
|
4779
|
+
if (dateStart < minDateStart)
|
|
4780
|
+
return false;
|
|
4781
|
+
}
|
|
4782
|
+
if (maxDate) {
|
|
4783
|
+
const maxDateStart = dayjs(maxDate).tz(timezone).startOf('day').toDate();
|
|
4784
|
+
const dateStart = dayjs(date).tz(timezone).startOf('day').toDate();
|
|
4785
|
+
if (dateStart > maxDateStart)
|
|
4786
|
+
return false;
|
|
4787
|
+
}
|
|
4788
|
+
return true;
|
|
4789
|
+
};
|
|
4790
|
+
const handleHelperButtonClick = (date) => {
|
|
4791
|
+
if (isDateValid(date)) {
|
|
4792
|
+
handleDateSelected({ date });
|
|
4793
|
+
}
|
|
4794
|
+
};
|
|
4795
|
+
const today = getToday();
|
|
4796
|
+
const yesterday = getYesterday();
|
|
4797
|
+
const tomorrow = getTomorrow();
|
|
4798
|
+
const datePickerContent = (jsxs(Grid, { gap: 2, children: [showHelperButtons && (jsxs(Grid, { templateColumns: "repeat(3, 1fr)", gap: 2, children: [jsx(Button$1, { size: "sm", variant: "outline", onClick: () => handleHelperButtonClick(yesterday), disabled: !isDateValid(yesterday), children: labels.yesterdayLabel ?? 'Yesterday' }), jsx(Button$1, { size: "sm", variant: "outline", onClick: () => handleHelperButtonClick(today), disabled: !isDateValid(today), children: labels.todayLabel ?? 'Today' }), jsx(Button$1, { size: "sm", variant: "outline", onClick: () => handleHelperButtonClick(tomorrow), disabled: !isDateValid(tomorrow), children: labels.tomorrowLabel ?? 'Tomorrow' })] })), jsx(DatePicker$1, { selected: selectedDate, onDateSelected: handleDateSelected, labels: labels, minDate: minDate, maxDate: maxDate, firstDayOfWeek: firstDayOfWeek, showOutsideDays: showOutsideDays, monthsToDisplay: monthsToDisplay })] }));
|
|
4799
|
+
return (jsxs(Popover.Root, { open: open, onOpenChange: (e) => setOpen(e.open), closeOnInteractOutside: true, autoFocus: false, children: [jsx(InputGroup, { endElement: jsx(Popover.Trigger, { asChild: true, children: jsx(IconButton, { variant: "ghost", size: "2xs", "aria-label": "Open calendar", onClick: () => setOpen(true), children: jsx(Icon, { children: jsx(MdDateRange, {}) }) }) }), children: jsx(Input, { value: inputValue, onChange: handleInputChange, onBlur: handleInputBlur, onKeyDown: handleKeyDown, placeholder: placeholder, readOnly: readOnly }) }), insideDialog ? (jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", minH: "25rem", children: jsx(Popover.Body, { children: datePickerContent }) }) })) : (jsx(Portal, { children: jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", minH: "25rem", children: jsx(Popover.Body, { children: datePickerContent }) }) }) }))] }));
|
|
4800
|
+
}
|
|
4603
4801
|
|
|
4604
4802
|
dayjs.extend(utc);
|
|
4605
4803
|
dayjs.extend(timezone);
|
|
4804
|
+
dayjs.extend(customParseFormat);
|
|
4606
4805
|
const DatePicker = ({ column, schema, prefix }) => {
|
|
4607
4806
|
const { watch, formState: { errors }, setValue, } = useFormContext();
|
|
4608
4807
|
const { timezone, dateTimePickerLabels, insideDialog } = useSchemaContext();
|
|
@@ -4611,15 +4810,29 @@ const DatePicker = ({ column, schema, prefix }) => {
|
|
|
4611
4810
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
4612
4811
|
const colLabel = formI18n.colLabel;
|
|
4613
4812
|
const [open, setOpen] = useState(false);
|
|
4813
|
+
const [inputValue, setInputValue] = useState('');
|
|
4614
4814
|
const selectedDate = watch(colLabel);
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4815
|
+
// Update input value when form value changes
|
|
4816
|
+
useEffect(() => {
|
|
4817
|
+
if (selectedDate) {
|
|
4818
|
+
const parsedDate = dayjs(selectedDate).tz(timezone);
|
|
4819
|
+
if (parsedDate.isValid()) {
|
|
4820
|
+
const formatted = parsedDate.format(displayDateFormat);
|
|
4821
|
+
setInputValue(formatted);
|
|
4822
|
+
}
|
|
4823
|
+
else {
|
|
4824
|
+
setInputValue('');
|
|
4825
|
+
}
|
|
4826
|
+
}
|
|
4827
|
+
else {
|
|
4828
|
+
setInputValue('');
|
|
4829
|
+
}
|
|
4830
|
+
}, [selectedDate, displayDateFormat, timezone]);
|
|
4831
|
+
// Format and validate existing value
|
|
4618
4832
|
useEffect(() => {
|
|
4619
4833
|
try {
|
|
4620
4834
|
if (selectedDate) {
|
|
4621
4835
|
// Parse the selectedDate as UTC or in a specific timezone to avoid +8 hour shift
|
|
4622
|
-
// For example, parse as UTC:
|
|
4623
4836
|
const parsedDate = dayjs(selectedDate).tz(timezone);
|
|
4624
4837
|
if (!parsedDate.isValid())
|
|
4625
4838
|
return;
|
|
@@ -4637,7 +4850,7 @@ const DatePicker = ({ column, schema, prefix }) => {
|
|
|
4637
4850
|
catch (e) {
|
|
4638
4851
|
console.error(e);
|
|
4639
4852
|
}
|
|
4640
|
-
}, [selectedDate, dateFormat, colLabel, setValue]);
|
|
4853
|
+
}, [selectedDate, dateFormat, colLabel, setValue, timezone]);
|
|
4641
4854
|
const datePickerLabels = {
|
|
4642
4855
|
monthNamesShort: dateTimePickerLabels?.monthNamesShort ?? [
|
|
4643
4856
|
'January',
|
|
@@ -4665,14 +4878,92 @@ const DatePicker = ({ column, schema, prefix }) => {
|
|
|
4665
4878
|
backButtonLabel: dateTimePickerLabels?.backButtonLabel ?? 'Back',
|
|
4666
4879
|
forwardButtonLabel: dateTimePickerLabels?.forwardButtonLabel ?? 'Forward',
|
|
4667
4880
|
};
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4881
|
+
// Convert value to Date object for DatePicker
|
|
4882
|
+
const selectedDateObj = selectedDate
|
|
4883
|
+
? dayjs(selectedDate).tz(timezone).isValid()
|
|
4884
|
+
? dayjs(selectedDate).tz(timezone).toDate()
|
|
4885
|
+
: new Date()
|
|
4886
|
+
: new Date();
|
|
4887
|
+
// Shared function to parse and validate input value
|
|
4888
|
+
const parseAndValidateInput = (inputVal) => {
|
|
4889
|
+
// If empty, clear the value
|
|
4890
|
+
if (!inputVal.trim()) {
|
|
4891
|
+
setValue(colLabel, undefined, {
|
|
4892
|
+
shouldValidate: true,
|
|
4893
|
+
shouldDirty: true,
|
|
4894
|
+
});
|
|
4895
|
+
setInputValue('');
|
|
4896
|
+
return;
|
|
4897
|
+
}
|
|
4898
|
+
// Try parsing with displayDateFormat first
|
|
4899
|
+
let parsedDate = dayjs(inputVal, displayDateFormat, true);
|
|
4900
|
+
// If that fails, try common date formats
|
|
4901
|
+
if (!parsedDate.isValid()) {
|
|
4902
|
+
parsedDate = dayjs(inputVal);
|
|
4903
|
+
}
|
|
4904
|
+
// If still invalid, try parsing with dateFormat
|
|
4905
|
+
if (!parsedDate.isValid()) {
|
|
4906
|
+
parsedDate = dayjs(inputVal, dateFormat, true);
|
|
4907
|
+
}
|
|
4908
|
+
// If valid, format and update
|
|
4909
|
+
if (parsedDate.isValid()) {
|
|
4910
|
+
const formattedDate = parsedDate.tz(timezone).format(dateFormat);
|
|
4911
|
+
const formattedDisplay = parsedDate
|
|
4912
|
+
.tz(timezone)
|
|
4913
|
+
.format(displayDateFormat);
|
|
4914
|
+
setValue(colLabel, formattedDate, {
|
|
4915
|
+
shouldValidate: true,
|
|
4916
|
+
shouldDirty: true,
|
|
4917
|
+
});
|
|
4918
|
+
setInputValue(formattedDisplay);
|
|
4919
|
+
}
|
|
4920
|
+
else {
|
|
4921
|
+
// Invalid date - reset to prop value
|
|
4922
|
+
resetToPropValue();
|
|
4923
|
+
}
|
|
4924
|
+
};
|
|
4925
|
+
// Helper function to reset input to prop value
|
|
4926
|
+
const resetToPropValue = () => {
|
|
4927
|
+
if (selectedDate) {
|
|
4928
|
+
const parsedDate = dayjs(selectedDate).tz(timezone);
|
|
4929
|
+
if (parsedDate.isValid()) {
|
|
4930
|
+
const formatted = parsedDate.format(displayDateFormat);
|
|
4931
|
+
setInputValue(formatted);
|
|
4932
|
+
}
|
|
4933
|
+
else {
|
|
4934
|
+
setInputValue('');
|
|
4935
|
+
}
|
|
4936
|
+
}
|
|
4937
|
+
else {
|
|
4938
|
+
setInputValue('');
|
|
4939
|
+
}
|
|
4940
|
+
};
|
|
4941
|
+
const handleInputChange = (e) => {
|
|
4942
|
+
// Only update the input value, don't parse yet
|
|
4943
|
+
setInputValue(e.target.value);
|
|
4944
|
+
};
|
|
4945
|
+
const handleInputBlur = () => {
|
|
4946
|
+
// Parse and validate when input loses focus
|
|
4947
|
+
parseAndValidateInput(inputValue);
|
|
4948
|
+
};
|
|
4949
|
+
const handleKeyDown = (e) => {
|
|
4950
|
+
// Parse and validate when Enter is pressed
|
|
4951
|
+
if (e.key === 'Enter') {
|
|
4952
|
+
e.preventDefault();
|
|
4953
|
+
parseAndValidateInput(inputValue);
|
|
4954
|
+
}
|
|
4955
|
+
};
|
|
4956
|
+
const handleDateSelected = ({ date }) => {
|
|
4957
|
+
const formattedDate = dayjs(date).tz(timezone).format(dateFormat);
|
|
4958
|
+
setValue(colLabel, formattedDate, {
|
|
4959
|
+
shouldValidate: true,
|
|
4960
|
+
shouldDirty: true,
|
|
4961
|
+
});
|
|
4962
|
+
setOpen(false);
|
|
4963
|
+
};
|
|
4964
|
+
const datePickerContent = (jsx(DatePicker$1, { selected: selectedDateObj, onDateSelected: handleDateSelected, labels: datePickerLabels }));
|
|
4672
4965
|
return (jsx(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
4673
|
-
gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: jsxs(Popover.Root, { open: open, onOpenChange: (e) => setOpen(e.open), closeOnInteractOutside: true, children: [jsx(Popover.Trigger, { asChild: true, children:
|
|
4674
|
-
setOpen(true);
|
|
4675
|
-
}, justifyContent: 'start', children: [jsx(MdDateRange, {}), selectedDate !== undefined ? `${displayDate}` : ''] }) }), insideDialog ? (jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", minH: "25rem", children: jsx(Popover.Body, { children: datePickerContent }) }) })) : (jsx(Portal, { children: jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", minH: "25rem", children: jsx(Popover.Body, { children: datePickerContent }) }) }) }))] }) }));
|
|
4966
|
+
gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: jsxs(Popover.Root, { open: open, onOpenChange: (e) => setOpen(e.open), closeOnInteractOutside: true, autoFocus: false, children: [jsx(InputGroup, { endElement: jsx(Popover.Trigger, { asChild: true, children: jsx(IconButton, { variant: "ghost", size: "2xs", "aria-label": "Open calendar", onClick: () => setOpen(true), children: jsx(Icon, { children: jsx(MdDateRange, {}) }) }) }), children: jsx(Input, { value: inputValue, onChange: handleInputChange, onBlur: handleInputBlur, onKeyDown: handleKeyDown, placeholder: formI18n.label(), size: "sm" }) }), insideDialog ? (jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", minH: "25rem", children: jsx(Popover.Body, { children: datePickerContent }) }) })) : (jsx(Portal, { children: jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", minH: "25rem", children: jsx(Popover.Body, { children: datePickerContent }) }) }) }))] }) }));
|
|
4676
4967
|
};
|
|
4677
4968
|
|
|
4678
4969
|
dayjs.extend(utc);
|
|
@@ -4680,7 +4971,7 @@ dayjs.extend(timezone);
|
|
|
4680
4971
|
const DateRangePicker = ({ column, schema, prefix, }) => {
|
|
4681
4972
|
const { watch, formState: { errors }, setValue, } = useFormContext();
|
|
4682
4973
|
const { timezone, insideDialog } = useSchemaContext();
|
|
4683
|
-
const formI18n = useFormI18n(column, prefix);
|
|
4974
|
+
const formI18n = useFormI18n(column, prefix, schema);
|
|
4684
4975
|
const { required, gridColumn = 'span 12', gridRow = 'span 1', displayDateFormat = 'YYYY-MM-DD', dateFormat = 'YYYY-MM-DD', } = schema;
|
|
4685
4976
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
4686
4977
|
const colLabel = formI18n.colLabel;
|
|
@@ -4786,27 +5077,82 @@ const EnumPicker = ({ column, isMultiple = false, schema, prefix, showTotalAndLi
|
|
|
4786
5077
|
const watchEnum = watch(colLabel);
|
|
4787
5078
|
const watchEnums = (watch(colLabel) ?? []);
|
|
4788
5079
|
const dataList = schema.enum ?? [];
|
|
5080
|
+
// Helper function to render enum value (returns ReactNode)
|
|
5081
|
+
// If renderDisplay is provided, use it; otherwise show the enum string value directly
|
|
5082
|
+
const renderEnumValue = (value) => {
|
|
5083
|
+
if (renderDisplay) {
|
|
5084
|
+
return renderDisplay(value);
|
|
5085
|
+
}
|
|
5086
|
+
// If no renderDisplay provided, show the enum string value directly
|
|
5087
|
+
return value;
|
|
5088
|
+
};
|
|
5089
|
+
// Helper function to get string representation for input display
|
|
5090
|
+
// Converts ReactNode to string for combobox input display
|
|
5091
|
+
const getDisplayString = (value) => {
|
|
5092
|
+
if (renderDisplay) {
|
|
5093
|
+
const rendered = renderDisplay(value);
|
|
5094
|
+
// If renderDisplay returns a string, use it directly
|
|
5095
|
+
if (typeof rendered === 'string') {
|
|
5096
|
+
return rendered;
|
|
5097
|
+
}
|
|
5098
|
+
// If it's a React element, try to extract text content
|
|
5099
|
+
// For now, fallback to the raw value if we can't extract text
|
|
5100
|
+
// In most cases, renderDisplay should return a string or simple element
|
|
5101
|
+
if (typeof rendered === 'object' &&
|
|
5102
|
+
rendered !== null &&
|
|
5103
|
+
'props' in rendered) {
|
|
5104
|
+
const props = rendered.props;
|
|
5105
|
+
// Try to extract text from React element props
|
|
5106
|
+
if (props?.children) {
|
|
5107
|
+
const children = props.children;
|
|
5108
|
+
if (typeof children === 'string') {
|
|
5109
|
+
return children;
|
|
5110
|
+
}
|
|
5111
|
+
}
|
|
5112
|
+
}
|
|
5113
|
+
// Fallback: use raw value if we can't extract string
|
|
5114
|
+
return value;
|
|
5115
|
+
}
|
|
5116
|
+
return value;
|
|
5117
|
+
};
|
|
5118
|
+
// Debug log when renderDisplay is missing
|
|
5119
|
+
if (!renderDisplay) {
|
|
5120
|
+
console.debug(`[EnumPicker] Missing renderDisplay for field '${colLabel}'. Add renderDisplay function to schema for field '${colLabel}' to provide custom UI rendering. Currently showing enum string values directly.`, {
|
|
5121
|
+
fieldName: column,
|
|
5122
|
+
colLabel,
|
|
5123
|
+
prefix,
|
|
5124
|
+
enumValues: dataList,
|
|
5125
|
+
});
|
|
5126
|
+
}
|
|
4789
5127
|
// Current value for combobox (array format)
|
|
4790
5128
|
const currentValue = isMultiple
|
|
4791
5129
|
? watchEnums.filter((val) => val != null && val !== '')
|
|
4792
5130
|
: watchEnum
|
|
4793
5131
|
? [watchEnum]
|
|
4794
5132
|
: [];
|
|
4795
|
-
//
|
|
5133
|
+
// Track input focus state for single selection
|
|
5134
|
+
const [isInputFocused, setIsInputFocused] = useState(false);
|
|
5135
|
+
// Get the selected value for single selection display
|
|
5136
|
+
const selectedSingleValue = !isMultiple && watchEnum ? watchEnum : null;
|
|
5137
|
+
const selectedSingleRendered = selectedSingleValue
|
|
5138
|
+
? renderEnumValue(selectedSingleValue)
|
|
5139
|
+
: null;
|
|
5140
|
+
const isSelectedSingleValueString = typeof selectedSingleRendered === 'string';
|
|
4796
5141
|
const comboboxItems = useMemo(() => {
|
|
4797
5142
|
return dataList.map((item) => ({
|
|
4798
|
-
label:
|
|
4799
|
-
? String(renderDisplay(item))
|
|
4800
|
-
: formI18n.t(item),
|
|
5143
|
+
label: item, // Internal: used for search/filtering only
|
|
4801
5144
|
value: item,
|
|
5145
|
+
raw: item, // Passed to renderEnumValue for UI rendering
|
|
5146
|
+
displayLabel: getDisplayString(item), // Used for input display when selected
|
|
4802
5147
|
}));
|
|
4803
|
-
}, [dataList, renderDisplay
|
|
5148
|
+
}, [dataList, renderDisplay]);
|
|
4804
5149
|
// Use filter hook for combobox
|
|
4805
5150
|
const { contains } = useFilter({ sensitivity: 'base' });
|
|
4806
5151
|
// Create collection for combobox
|
|
5152
|
+
// itemToString uses displayLabel to show rendered display in input when selected
|
|
4807
5153
|
const { collection, filter } = useListCollection({
|
|
4808
5154
|
initialItems: comboboxItems,
|
|
4809
|
-
itemToString: (item) => item.
|
|
5155
|
+
itemToString: (item) => item.displayLabel, // Use displayLabel for selected value display
|
|
4810
5156
|
itemToValue: (item) => item.value,
|
|
4811
5157
|
filter: contains,
|
|
4812
5158
|
});
|
|
@@ -4830,9 +5176,7 @@ const EnumPicker = ({ column, isMultiple = false, schema, prefix, showTotalAndLi
|
|
|
4830
5176
|
setValue(colLabel, details.value);
|
|
4831
5177
|
}
|
|
4832
5178
|
}, children: jsx(HStack, { gap: "6", children: dataList.map((item) => {
|
|
4833
|
-
return (jsxs(RadioGroup$1.Item, { value: item, children: [jsx(RadioGroup$1.ItemHiddenInput, {}), jsx(RadioGroup$1.ItemIndicator, {}), jsx(RadioGroup$1.ItemText, { children:
|
|
4834
|
-
? renderDisplay(item)
|
|
4835
|
-
: formI18n.t(item) })] }, `${colLabel}-${item}`));
|
|
5179
|
+
return (jsxs(RadioGroup$1.Item, { value: item, children: [jsx(RadioGroup$1.ItemHiddenInput, {}), jsx(RadioGroup$1.ItemIndicator, {}), jsx(RadioGroup$1.ItemText, { children: renderEnumValue(item) })] }, `${colLabel}-${item}`));
|
|
4836
5180
|
}) }) }) }));
|
|
4837
5181
|
}
|
|
4838
5182
|
return (jsxs(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
@@ -4843,16 +5187,31 @@ const EnumPicker = ({ column, isMultiple = false, schema, prefix, showTotalAndLi
|
|
|
4843
5187
|
return (jsx(Tag, { size: "lg", closable: true, onClick: () => {
|
|
4844
5188
|
const newValue = currentValue.filter((val) => val !== enumValue);
|
|
4845
5189
|
setValue(colLabel, newValue);
|
|
4846
|
-
}, children:
|
|
4847
|
-
? renderDisplay(enumValue)
|
|
4848
|
-
: formI18n.t(enumValue) }, enumValue));
|
|
5190
|
+
}, children: renderEnumValue(enumValue) }, enumValue));
|
|
4849
5191
|
}) })), jsxs(Combobox.Root, { collection: collection, value: currentValue, onValueChange: handleValueChange, onInputValueChange: handleInputValueChange, multiple: isMultiple, closeOnSelect: !isMultiple, openOnClick: true, invalid: !!errors[colLabel], width: "100%", positioning: insideDialog
|
|
4850
5192
|
? { strategy: 'fixed', hideWhenDetached: true }
|
|
4851
|
-
: undefined, children: [jsxs(Combobox.Control, {
|
|
5193
|
+
: undefined, children: [jsxs(Combobox.Control, { position: "relative", children: [!isMultiple &&
|
|
5194
|
+
selectedSingleValue &&
|
|
5195
|
+
!isInputFocused &&
|
|
5196
|
+
!isSelectedSingleValueString &&
|
|
5197
|
+
selectedSingleRendered && (jsx(Box, { position: "absolute", left: 3, top: "50%", transform: "translateY(-50%)", pointerEvents: "none", zIndex: 1, maxWidth: "calc(100% - 60px)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", children: selectedSingleRendered })), jsx(Combobox.Input, { placeholder: !isMultiple && selectedSingleValue && !isInputFocused
|
|
5198
|
+
? undefined
|
|
5199
|
+
: enumPickerLabels?.typeToSearch ?? 'Type to search', onFocus: () => setIsInputFocused(true), onBlur: () => setIsInputFocused(false), style: {
|
|
5200
|
+
color: !isMultiple &&
|
|
5201
|
+
selectedSingleValue &&
|
|
5202
|
+
!isInputFocused &&
|
|
5203
|
+
!isSelectedSingleValueString
|
|
5204
|
+
? 'transparent'
|
|
5205
|
+
: undefined,
|
|
5206
|
+
caretColor: !isMultiple &&
|
|
5207
|
+
selectedSingleValue &&
|
|
5208
|
+
!isInputFocused &&
|
|
5209
|
+
!isSelectedSingleValueString
|
|
5210
|
+
? 'transparent'
|
|
5211
|
+
: undefined,
|
|
5212
|
+
} }), jsxs(Combobox.IndicatorGroup, { children: [!isMultiple && currentValue.length > 0 && (jsx(Combobox.ClearTrigger, { onClick: () => {
|
|
4852
5213
|
setValue(colLabel, '');
|
|
4853
|
-
} })), jsx(Combobox.Trigger, {})] })] }), insideDialog ? (jsx(Combobox.Positioner, { children: jsxs(Combobox.Content, { children: [showTotalAndLimit && (jsx(Text, { p: 2, fontSize: "sm", color: "fg.muted", children: `${enumPickerLabels?.total ??
|
|
4854
|
-
formI18n.t('empty_search_result') })) : (jsx(Fragment, { children: collection.items.map((item, index) => (jsxs(Combobox.Item, { item: item, children: [jsx(Combobox.ItemText, { children: item.label }), jsx(Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) }))] }) })) : (jsx(Portal, { children: jsx(Combobox.Positioner, { children: jsxs(Combobox.Content, { children: [showTotalAndLimit && (jsx(Text, { p: 2, fontSize: "sm", color: "fg.muted", children: `${enumPickerLabels?.total ?? formI18n.t('total')}: ${collection.items.length}` })), collection.items.length === 0 ? (jsx(Combobox.Empty, { children: enumPickerLabels?.emptySearchResult ??
|
|
4855
|
-
formI18n.t('empty_search_result') })) : (jsx(Fragment, { children: collection.items.map((item, index) => (jsxs(Combobox.Item, { item: item, children: [jsx(Combobox.ItemText, { children: item.label }), jsx(Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) }))] }) }) }))] })] }));
|
|
5214
|
+
} })), jsx(Combobox.Trigger, {})] })] }), insideDialog ? (jsx(Combobox.Positioner, { children: jsxs(Combobox.Content, { children: [showTotalAndLimit && (jsx(Text, { p: 2, fontSize: "sm", color: "fg.muted", children: `${enumPickerLabels?.total ?? 'Total'}: ${collection.items.length}` })), collection.items.length === 0 ? (jsx(Combobox.Empty, { children: enumPickerLabels?.emptySearchResult ?? 'No results found' })) : (jsx(Fragment, { children: collection.items.map((item, index) => (jsxs(Combobox.Item, { item: item, children: [jsx(Combobox.ItemText, { children: renderEnumValue(item.raw) }), jsx(Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) }))] }) })) : (jsx(Portal, { children: jsx(Combobox.Positioner, { children: jsxs(Combobox.Content, { children: [showTotalAndLimit && (jsx(Text, { p: 2, fontSize: "sm", color: "fg.muted", children: `${enumPickerLabels?.total ?? 'Total'}: ${collection.items.length}` })), collection.items.length === 0 ? (jsx(Combobox.Empty, { children: enumPickerLabels?.emptySearchResult ?? 'No results found' })) : (jsx(Fragment, { children: collection.items.map((item, index) => (jsxs(Combobox.Item, { item: item, children: [jsx(Combobox.ItemText, { children: renderEnumValue(item.raw) }), jsx(Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) }))] }) }) }))] })] }));
|
|
4856
5215
|
};
|
|
4857
5216
|
|
|
4858
5217
|
function isEnteringWindow(_ref) {
|
|
@@ -5325,7 +5684,7 @@ const MediaLibraryBrowser = ({ onFetchFiles, filterImageOnly = false, labels, en
|
|
|
5325
5684
|
}) })) }))] }));
|
|
5326
5685
|
};
|
|
5327
5686
|
|
|
5328
|
-
function MediaBrowserDialog({ open, onClose, onSelect, title, filterImageOnly = false, onFetchFiles, onUploadFile, enableUpload = false, labels,
|
|
5687
|
+
function MediaBrowserDialog({ open, onClose, onSelect, title, filterImageOnly = false, onFetchFiles, onUploadFile, enableUpload = false, labels, }) {
|
|
5329
5688
|
const [selectedFile, setSelectedFile] = useState(undefined);
|
|
5330
5689
|
const [activeTab, setActiveTab] = useState('browse');
|
|
5331
5690
|
const [uploadingFiles, setUploadingFiles] = useState(new Set());
|
|
@@ -5409,7 +5768,7 @@ function MediaBrowserDialog({ open, onClose, onSelect, title, filterImageOnly =
|
|
|
5409
5768
|
const FilePicker = ({ column, schema, prefix }) => {
|
|
5410
5769
|
const { setValue, formState: { errors }, watch, } = useFormContext();
|
|
5411
5770
|
const { filePickerLabels } = useSchemaContext();
|
|
5412
|
-
const formI18n = useFormI18n(column, prefix);
|
|
5771
|
+
const formI18n = useFormI18n(column, prefix, schema);
|
|
5413
5772
|
const { required, gridColumn = 'span 12', gridRow = 'span 1', type, } = schema;
|
|
5414
5773
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
5415
5774
|
const isSingleSelect = type === 'string';
|
|
@@ -5467,7 +5826,7 @@ const FilePicker = ({ column, schema, prefix }) => {
|
|
|
5467
5826
|
const newFiles = files.filter(({ name }) => !currentFiles.some((cur) => cur.name === name));
|
|
5468
5827
|
setValue(colLabel, [...currentFiles, ...newFiles]);
|
|
5469
5828
|
}
|
|
5470
|
-
}, placeholder: filePickerLabels?.fileDropzone ??
|
|
5829
|
+
}, placeholder: filePickerLabels?.fileDropzone ?? 'Drop files here' }) }), jsx(Flex, { flexFlow: 'column', gap: 1, children: currentFiles.map((file, index) => {
|
|
5471
5830
|
const fileIdentifier = getFileIdentifier(file, index);
|
|
5472
5831
|
const fileName = getFileName(file);
|
|
5473
5832
|
const fileSize = getFileSize(file);
|
|
@@ -5485,7 +5844,7 @@ const FilePicker = ({ column, schema, prefix }) => {
|
|
|
5485
5844
|
const FormMediaLibraryBrowser = ({ column, schema, prefix, }) => {
|
|
5486
5845
|
const { setValue, formState: { errors }, watch, } = useFormContext();
|
|
5487
5846
|
const { filePickerLabels } = useSchemaContext();
|
|
5488
|
-
const formI18n = useFormI18n(column, prefix);
|
|
5847
|
+
const formI18n = useFormI18n(column, prefix, schema);
|
|
5489
5848
|
const { required, gridColumn = 'span 12', gridRow = 'span 1', filePicker, type, } = schema;
|
|
5490
5849
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
5491
5850
|
const isSingleSelect = type === 'string';
|
|
@@ -5578,11 +5937,7 @@ const FormMediaLibraryBrowser = ({ column, schema, prefix, }) => {
|
|
|
5578
5937
|
}
|
|
5579
5938
|
};
|
|
5580
5939
|
return (jsxs(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
5581
|
-
gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: [jsx(VStack, { align: "stretch", gap: 2, children: jsx(Button$1, { variant: "outline", onClick: () => setDialogOpen(true), borderColor: "border.default", bg: "bg.panel", _hover: { bg: 'bg.muted' }, children: filePickerLabels?.browseLibrary ??
|
|
5582
|
-
formI18n.t('browse_library') ??
|
|
5583
|
-
'Browse from Library' }) }), jsx(MediaBrowserDialog, { open: dialogOpen, onClose: () => setDialogOpen(false), onSelect: handleMediaLibrarySelect, title: filePickerLabels?.dialogTitle ??
|
|
5584
|
-
filePickerLabels?.dialogTitle ??
|
|
5585
|
-
'Select File', filterImageOnly: filterImageOnly, onFetchFiles: onFetchFiles, onUploadFile: onUploadFile, enableUpload: enableUpload, labels: filePickerLabels, colLabel: colLabel }), jsx(Flex, { flexFlow: 'column', gap: 1, children: currentFileIds.map((fileId, index) => {
|
|
5940
|
+
gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: [jsx(VStack, { align: "stretch", gap: 2, children: jsx(Button$1, { variant: "outline", onClick: () => setDialogOpen(true), borderColor: "border.default", bg: "bg.panel", _hover: { bg: 'bg.muted' }, children: filePickerLabels?.browseLibrary ?? 'Browse from Library' }) }), jsx(MediaBrowserDialog, { open: dialogOpen, onClose: () => setDialogOpen(false), onSelect: handleMediaLibrarySelect, title: filePickerLabels?.dialogTitle ?? formI18n.label() ?? 'Select File', filterImageOnly: filterImageOnly, onFetchFiles: onFetchFiles, onUploadFile: onUploadFile, enableUpload: enableUpload, labels: filePickerLabels, colLabel: colLabel }), jsx(Flex, { flexFlow: 'column', gap: 1, children: currentFileIds.map((fileId, index) => {
|
|
5586
5941
|
const file = fileMap.get(fileId);
|
|
5587
5942
|
const isImage = file
|
|
5588
5943
|
? /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(file.name)
|
|
@@ -5598,43 +5953,51 @@ const FormMediaLibraryBrowser = ({ column, schema, prefix, }) => {
|
|
|
5598
5953
|
}) })] }));
|
|
5599
5954
|
};
|
|
5600
5955
|
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
}
|
|
5605
|
-
if (in_table === undefined || in_table.length == 0) {
|
|
5606
|
-
throw new Error("The in_table is missing");
|
|
5607
|
-
}
|
|
5608
|
-
const options = {
|
|
5609
|
-
method: "GET",
|
|
5610
|
-
url: `${serverUrl}/api/g/${in_table}`,
|
|
5611
|
-
params: {
|
|
5612
|
-
searching,
|
|
5613
|
-
where,
|
|
5614
|
-
limit,
|
|
5615
|
-
offset
|
|
5616
|
-
},
|
|
5617
|
-
};
|
|
5618
|
-
try {
|
|
5619
|
-
const { data } = await axios.request(options);
|
|
5620
|
-
console.log(data);
|
|
5621
|
-
return data;
|
|
5622
|
-
}
|
|
5623
|
-
catch (error) {
|
|
5624
|
-
console.error(error);
|
|
5625
|
-
throw error;
|
|
5626
|
-
}
|
|
5627
|
-
};
|
|
5628
|
-
|
|
5629
|
-
// Default renderDisplay function that stringifies JSON
|
|
5956
|
+
// Default renderDisplay function that intelligently displays items
|
|
5957
|
+
// If item is an object, tries to find common display fields (name, title, label, etc.)
|
|
5958
|
+
// Otherwise falls back to JSON.stringify
|
|
5630
5959
|
const defaultRenderDisplay = (item) => {
|
|
5960
|
+
// Check if item is an object (not null, not array, not primitive)
|
|
5961
|
+
if (item !== null &&
|
|
5962
|
+
typeof item === 'object' &&
|
|
5963
|
+
!Array.isArray(item) &&
|
|
5964
|
+
!(item instanceof Date)) {
|
|
5965
|
+
const obj = item;
|
|
5966
|
+
// Try common display fields in order of preference
|
|
5967
|
+
const displayFields = [
|
|
5968
|
+
'name',
|
|
5969
|
+
'title',
|
|
5970
|
+
'label',
|
|
5971
|
+
'displayName',
|
|
5972
|
+
'display_name',
|
|
5973
|
+
'text',
|
|
5974
|
+
'value',
|
|
5975
|
+
];
|
|
5976
|
+
for (const field of displayFields) {
|
|
5977
|
+
if (obj[field] !== undefined && obj[field] !== null) {
|
|
5978
|
+
const value = obj[field];
|
|
5979
|
+
// Return the value if it's a string or number, otherwise stringify it
|
|
5980
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
5981
|
+
return String(value);
|
|
5982
|
+
}
|
|
5983
|
+
}
|
|
5984
|
+
}
|
|
5985
|
+
// If no display field found, fall back to JSON.stringify
|
|
5986
|
+
return JSON.stringify(item);
|
|
5987
|
+
}
|
|
5988
|
+
// For non-objects (primitives, arrays, dates), use JSON.stringify
|
|
5631
5989
|
return JSON.stringify(item);
|
|
5632
5990
|
};
|
|
5633
5991
|
|
|
5634
5992
|
const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
|
|
5635
5993
|
const { watch, getValues, formState: { errors }, setValue, } = useFormContext();
|
|
5636
|
-
const {
|
|
5637
|
-
const { renderDisplay, foreign_key } = schema;
|
|
5994
|
+
const { idMap, setIdMap, idPickerLabels, insideDialog } = useSchemaContext();
|
|
5995
|
+
const { renderDisplay, itemToValue: schemaItemToValue, loadInitialValues, foreign_key, variant, } = schema;
|
|
5996
|
+
// loadInitialValues should be provided in schema for id-picker fields
|
|
5997
|
+
// It's used to load the record of the id so the display is human-readable
|
|
5998
|
+
if (variant === 'id-picker' && !loadInitialValues) {
|
|
5999
|
+
console.warn(`loadInitialValues is recommended in schema for IdPicker field '${column}'. Please provide loadInitialValues function in the schema to load records for human-readable display.`);
|
|
6000
|
+
}
|
|
5638
6001
|
const { table, column: column_ref, customQueryFn, } = foreign_key;
|
|
5639
6002
|
const [searchText, setSearchText] = useState('');
|
|
5640
6003
|
const [debouncedSearchText, setDebouncedSearchText] = useState('');
|
|
@@ -5679,94 +6042,58 @@ const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
|
|
|
5679
6042
|
const missingIdsKey = useMemo(() => {
|
|
5680
6043
|
return JSON.stringify([...missingIds].sort());
|
|
5681
6044
|
}, [missingIds]);
|
|
6045
|
+
// Include idMap state in query key to force refetch when idMap is reset (e.g., on remount from another page)
|
|
6046
|
+
// This ensures the query runs even if React Query has cached data for the same missing IDs
|
|
6047
|
+
const idMapStateKey = useMemo(() => {
|
|
6048
|
+
// Create a key based on whether the required IDs are in idMap
|
|
6049
|
+
const hasRequiredIds = currentValue.every((id) => idMap[id]);
|
|
6050
|
+
return hasRequiredIds ? 'complete' : 'incomplete';
|
|
6051
|
+
}, [currentValue, idMap]);
|
|
5682
6052
|
// Query to fetch initial values that are missing from idMap
|
|
5683
6053
|
// This query runs automatically when missingIds.length > 0 and updates idMap
|
|
5684
6054
|
const initialValuesQuery = useQuery({
|
|
5685
|
-
queryKey: [`idpicker-initial`, column, missingIdsKey],
|
|
6055
|
+
queryKey: [`idpicker-initial`, column, missingIdsKey, idMapStateKey],
|
|
5686
6056
|
queryFn: async () => {
|
|
5687
6057
|
if (missingIds.length === 0) {
|
|
5688
6058
|
return { data: [], count: 0 };
|
|
5689
6059
|
}
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
offset: 0,
|
|
5695
|
-
where: [
|
|
5696
|
-
{
|
|
5697
|
-
id: column_ref,
|
|
5698
|
-
value: missingIds.length === 1 ? missingIds[0] : missingIds,
|
|
5699
|
-
},
|
|
5700
|
-
],
|
|
5701
|
-
});
|
|
5702
|
-
setIdMap((state) => {
|
|
5703
|
-
return { ...state, ...idMap };
|
|
5704
|
-
});
|
|
5705
|
-
return data;
|
|
6060
|
+
// Use schema's loadInitialValues (required for id-picker)
|
|
6061
|
+
if (!loadInitialValues) {
|
|
6062
|
+
console.warn(`loadInitialValues is required in schema for IdPicker field '${column}'. Returning empty idMap.`);
|
|
6063
|
+
return { data: [], count: 0 };
|
|
5706
6064
|
}
|
|
5707
|
-
const
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
limit: missingIds.length,
|
|
5712
|
-
offset: 0,
|
|
5713
|
-
where: [
|
|
5714
|
-
{
|
|
5715
|
-
id: column_ref,
|
|
5716
|
-
value: missingIds.length === 1 ? missingIds[0] : missingIds,
|
|
5717
|
-
},
|
|
5718
|
-
],
|
|
6065
|
+
const result = await loadInitialValues({
|
|
6066
|
+
ids: missingIds,
|
|
6067
|
+
foreign_key: foreign_key,
|
|
6068
|
+
setIdMap,
|
|
5719
6069
|
});
|
|
5720
|
-
|
|
5721
|
-
return [
|
|
5722
|
-
item[column_ref],
|
|
5723
|
-
{
|
|
5724
|
-
...item,
|
|
5725
|
-
},
|
|
5726
|
-
];
|
|
5727
|
-
}));
|
|
5728
|
-
setIdMap((state) => {
|
|
5729
|
-
return { ...state, ...newMap };
|
|
5730
|
-
});
|
|
5731
|
-
return data;
|
|
6070
|
+
return result.data;
|
|
5732
6071
|
},
|
|
5733
6072
|
enabled: missingIds.length > 0, // Only fetch if there are missing IDs
|
|
5734
|
-
staleTime:
|
|
6073
|
+
staleTime: 0, // Always consider data stale to refetch on remount
|
|
6074
|
+
refetchOnMount: true, // Always refetch when component remounts (e.g., from another page)
|
|
6075
|
+
refetchOnWindowFocus: false, // Don't refetch on window focus
|
|
5735
6076
|
});
|
|
5736
6077
|
const { isLoading: isLoadingInitialValues, isFetching: isFetchingInitialValues, } = initialValuesQuery;
|
|
5737
6078
|
// Query for search results (async loading)
|
|
5738
6079
|
const query = useQuery({
|
|
5739
6080
|
queryKey: [`idpicker`, { column, searchText: debouncedSearchText, limit }],
|
|
5740
6081
|
queryFn: async () => {
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
limit: limit,
|
|
5745
|
-
offset: 0,
|
|
5746
|
-
});
|
|
5747
|
-
setIdMap((state) => {
|
|
5748
|
-
return { ...state, ...idMap };
|
|
5749
|
-
});
|
|
5750
|
-
return data;
|
|
6082
|
+
// customQueryFn is required when serverUrl is not available
|
|
6083
|
+
if (!customQueryFn) {
|
|
6084
|
+
throw new Error(`customQueryFn is required in foreign_key for table ${table}. serverUrl has been removed.`);
|
|
5751
6085
|
}
|
|
5752
|
-
const data = await
|
|
5753
|
-
serverUrl,
|
|
6086
|
+
const { data, idMap } = await customQueryFn({
|
|
5754
6087
|
searching: debouncedSearchText ?? '',
|
|
5755
|
-
in_table: table,
|
|
5756
6088
|
limit: limit,
|
|
5757
6089
|
offset: 0,
|
|
5758
6090
|
});
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
{
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
];
|
|
5766
|
-
}));
|
|
5767
|
-
setIdMap((state) => {
|
|
5768
|
-
return { ...state, ...newMap };
|
|
5769
|
-
});
|
|
6091
|
+
// Update idMap with returned values
|
|
6092
|
+
if (idMap && Object.keys(idMap).length > 0) {
|
|
6093
|
+
setIdMap((state) => {
|
|
6094
|
+
return { ...state, ...idMap };
|
|
6095
|
+
});
|
|
6096
|
+
}
|
|
5770
6097
|
return data;
|
|
5771
6098
|
},
|
|
5772
6099
|
enabled: true, // Always enabled for combobox
|
|
@@ -5798,17 +6125,51 @@ const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
|
|
|
5798
6125
|
// Depend on idMapKey which only changes when items we care about change
|
|
5799
6126
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5800
6127
|
}, [currentValueKey, idMapKey]);
|
|
6128
|
+
// Default itemToValue function: extract value from item using column_ref
|
|
6129
|
+
const defaultItemToValue = (item) => String(item[column_ref]);
|
|
6130
|
+
// Use schema's itemToValue if provided, otherwise use default
|
|
6131
|
+
const itemToValueFn = schemaItemToValue
|
|
6132
|
+
? (item) => schemaItemToValue(item)
|
|
6133
|
+
: defaultItemToValue;
|
|
6134
|
+
// itemToString function: convert item to readable string using renderDisplay
|
|
6135
|
+
// This ensures items can always be displayed as readable strings in the combobox
|
|
6136
|
+
const renderFn = renderDisplay || defaultRenderDisplay;
|
|
6137
|
+
const itemToStringFn = (item) => {
|
|
6138
|
+
const rendered = renderFn(item);
|
|
6139
|
+
// If already a string or number, return it
|
|
6140
|
+
if (typeof rendered === 'string')
|
|
6141
|
+
return rendered;
|
|
6142
|
+
if (typeof rendered === 'number')
|
|
6143
|
+
return String(rendered);
|
|
6144
|
+
// For ReactNode, fall back to defaultRenderDisplay which converts to string
|
|
6145
|
+
return String(defaultRenderDisplay(item));
|
|
6146
|
+
};
|
|
5801
6147
|
// Transform data for combobox collection
|
|
5802
6148
|
// label is used for filtering/searching (must be a string)
|
|
6149
|
+
// displayLabel is used for input display when selected (string representation of rendered display)
|
|
5803
6150
|
// raw item is stored for custom rendering
|
|
5804
6151
|
// Also include items from idMap that match currentValue (for initial values display)
|
|
5805
6152
|
const comboboxItems = useMemo(() => {
|
|
5806
6153
|
const renderFn = renderDisplay || defaultRenderDisplay;
|
|
6154
|
+
// Helper to convert rendered display to string for displayLabel
|
|
6155
|
+
// For ReactNodes (non-string/number), we can't safely stringify due to circular refs
|
|
6156
|
+
// So we use the label (which is already a string) as fallback
|
|
6157
|
+
const getDisplayString = (rendered, fallbackLabel) => {
|
|
6158
|
+
if (typeof rendered === 'string')
|
|
6159
|
+
return rendered;
|
|
6160
|
+
if (typeof rendered === 'number')
|
|
6161
|
+
return String(rendered);
|
|
6162
|
+
// For ReactNode, use the fallback label (which is already a string representation)
|
|
6163
|
+
// The actual ReactNode will be rendered in the overlay, not in the input
|
|
6164
|
+
return fallbackLabel;
|
|
6165
|
+
};
|
|
5807
6166
|
const itemsFromDataList = dataList.map((item) => {
|
|
5808
6167
|
const rendered = renderFn(item);
|
|
6168
|
+
const label = typeof rendered === 'string' ? rendered : JSON.stringify(item); // Use string for filtering
|
|
5809
6169
|
return {
|
|
5810
|
-
label
|
|
5811
|
-
|
|
6170
|
+
label, // Use string for filtering
|
|
6171
|
+
displayLabel: getDisplayString(rendered, label), // String representation for input display
|
|
6172
|
+
value: itemToValueFn(item),
|
|
5812
6173
|
raw: item,
|
|
5813
6174
|
};
|
|
5814
6175
|
});
|
|
@@ -5817,25 +6178,28 @@ const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
|
|
|
5817
6178
|
const itemsFromIdMap = idMapItems
|
|
5818
6179
|
.map((item) => {
|
|
5819
6180
|
// Check if this item is already in itemsFromDataList
|
|
5820
|
-
const alreadyIncluded = itemsFromDataList.some((i) => i.value ===
|
|
6181
|
+
const alreadyIncluded = itemsFromDataList.some((i) => i.value === itemToValueFn(item));
|
|
5821
6182
|
if (alreadyIncluded)
|
|
5822
6183
|
return null;
|
|
5823
6184
|
const rendered = renderFn(item);
|
|
6185
|
+
const label = typeof rendered === 'string' ? rendered : JSON.stringify(item);
|
|
5824
6186
|
return {
|
|
5825
|
-
label
|
|
5826
|
-
|
|
6187
|
+
label,
|
|
6188
|
+
displayLabel: getDisplayString(rendered, label), // String representation for input display
|
|
6189
|
+
value: itemToValueFn(item),
|
|
5827
6190
|
raw: item,
|
|
5828
6191
|
};
|
|
5829
6192
|
})
|
|
5830
6193
|
.filter((item) => item !== null);
|
|
5831
6194
|
return [...itemsFromIdMap, ...itemsFromDataList];
|
|
5832
|
-
}, [dataList, column_ref, renderDisplay, idMapItems]);
|
|
6195
|
+
}, [dataList, column_ref, renderDisplay, idMapItems, itemToValueFn]);
|
|
5833
6196
|
// Use filter hook for combobox
|
|
5834
6197
|
const { contains } = useFilter({ sensitivity: 'base' });
|
|
5835
6198
|
// Create collection for combobox
|
|
6199
|
+
// itemToString uses displayLabel to show rendered display in input when selected
|
|
5836
6200
|
const { collection, filter, set } = useListCollection({
|
|
5837
6201
|
initialItems: comboboxItems,
|
|
5838
|
-
itemToString: (item) => item.
|
|
6202
|
+
itemToString: (item) => item.displayLabel, // Use displayLabel for selected value display
|
|
5839
6203
|
itemToValue: (item) => item.value,
|
|
5840
6204
|
filter: contains,
|
|
5841
6205
|
});
|
|
@@ -5890,6 +6254,10 @@ const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
|
|
|
5890
6254
|
idPickerLabels,
|
|
5891
6255
|
insideDialog: insideDialog ?? false,
|
|
5892
6256
|
renderDisplay,
|
|
6257
|
+
itemToValue: itemToValueFn,
|
|
6258
|
+
itemToString: itemToStringFn,
|
|
6259
|
+
loadInitialValues: loadInitialValues ??
|
|
6260
|
+
(async () => ({ data: { data: [], count: 0 }, idMap: {} })), // Fallback if not provided
|
|
5893
6261
|
column_ref,
|
|
5894
6262
|
errors,
|
|
5895
6263
|
setValue,
|
|
@@ -5898,60 +6266,69 @@ const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
|
|
|
5898
6266
|
|
|
5899
6267
|
const IdPickerSingle = ({ column, schema, prefix, }) => {
|
|
5900
6268
|
const formI18n = useFormI18n(column, prefix, schema);
|
|
5901
|
-
const { required, gridColumn = 'span 12', gridRow = 'span 1'
|
|
6269
|
+
const { required, gridColumn = 'span 12', gridRow = 'span 1' } = schema;
|
|
5902
6270
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
5903
|
-
const { colLabel, currentValue, searchText, setSearchText, isLoading, isFetching, isPending, isError, isSearching,
|
|
6271
|
+
const { colLabel, currentValue, searchText, setSearchText, isLoading, isFetching, isPending, isError, isSearching, collection, filter, idMap, idPickerLabels, insideDialog, renderDisplay: renderDisplayFn, itemToValue, itemToString, errors, setValue, } = useIdPickerData({
|
|
5904
6272
|
column,
|
|
5905
6273
|
schema,
|
|
5906
6274
|
prefix,
|
|
5907
6275
|
isMultiple: false,
|
|
5908
6276
|
});
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
const
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
6277
|
+
// Get the selected value for single selection display
|
|
6278
|
+
const selectedId = currentValue.length > 0 ? currentValue[0] : null;
|
|
6279
|
+
const selectedItem = selectedId
|
|
6280
|
+
? idMap[selectedId]
|
|
6281
|
+
: undefined;
|
|
6282
|
+
// Use itemToValue to get the combobox value from the selected item, or use the ID directly
|
|
6283
|
+
const comboboxValue = selectedItem
|
|
6284
|
+
? itemToString(selectedItem)
|
|
6285
|
+
: selectedId || '';
|
|
6286
|
+
// itemToString is available from the hook and can be used to get a readable string
|
|
6287
|
+
// representation of any item. The collection's itemToString is automatically used
|
|
6288
|
+
// by the combobox to display selected values.
|
|
6289
|
+
// Use useCombobox hook to control input value
|
|
6290
|
+
const combobox = useCombobox({
|
|
6291
|
+
collection,
|
|
6292
|
+
value: [comboboxValue],
|
|
6293
|
+
onInputValueChange(e) {
|
|
6294
|
+
setSearchText(e.inputValue);
|
|
6295
|
+
filter(e.inputValue);
|
|
6296
|
+
},
|
|
6297
|
+
onValueChange(e) {
|
|
6298
|
+
setValue(colLabel, e.value[0] || '');
|
|
6299
|
+
// Clear the input value after selection
|
|
6300
|
+
setSearchText('');
|
|
6301
|
+
},
|
|
6302
|
+
multiple: false,
|
|
6303
|
+
closeOnSelect: true,
|
|
6304
|
+
openOnClick: true,
|
|
6305
|
+
invalid: !!errors[colLabel],
|
|
6306
|
+
});
|
|
6307
|
+
// Use renderDisplay from hook (which comes from schema) or fallback to default
|
|
6308
|
+
const renderDisplayFunction = renderDisplayFn || defaultRenderDisplay;
|
|
6309
|
+
// Get the selected value for single selection display (already computed above)
|
|
6310
|
+
const selectedRendered = selectedItem
|
|
6311
|
+
? renderDisplayFunction(selectedItem)
|
|
6312
|
+
: null;
|
|
6313
|
+
return (jsx(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
6314
|
+
gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: jsxs(Combobox.RootProvider, { value: combobox, width: "100%", children: [jsx(Show, { when: selectedId && selectedRendered, children: jsxs(HStack, { justifyContent: 'space-between', children: [jsx(Box, { children: selectedRendered }), currentValue.length > 0 && (jsx(Button$1, { variant: "ghost", size: "sm", onClick: () => {
|
|
6315
|
+
setValue(colLabel, '');
|
|
6316
|
+
}, children: jsx(Icon, { children: jsx(BiX, {}) }) }))] }) }), jsx(Show, { when: !selectedId || !selectedRendered, children: jsxs(Combobox.Control, { position: "relative", children: [jsx(Combobox.Input, { placeholder: idPickerLabels?.typeToSearch ?? 'Type to search' }), jsxs(Combobox.IndicatorGroup, { children: [(isFetching || isLoading || isPending) && jsx(Spinner, { size: "xs" }), isError && (jsx(Icon, { color: "fg.error", children: jsx(BiError, {}) })), jsx(Combobox.Trigger, {})] })] }) }), insideDialog ? (jsx(Combobox.Positioner, { children: jsx(Combobox.Content, { children: isError ? (jsx(Text, { p: 2, color: "fg.error", fontSize: "sm", children: idPickerLabels?.emptySearchResult ?? 'Loading failed' })) : isFetching || isLoading || isPending || isSearching ? (
|
|
6317
|
+
// Show skeleton items to prevent UI shift
|
|
6318
|
+
jsx(Fragment, { children: Array.from({ length: 5 }).map((_, index) => (jsx(Flex, { p: 2, align: "center", gap: 2, children: jsx(Skeleton, { height: "20px", flex: "1" }) }, `skeleton-${index}`))) })) : collection.items.length === 0 ? (jsx(Combobox.Empty, { children: searchText
|
|
6319
|
+
? idPickerLabels?.emptySearchResult ?? 'No results found'
|
|
6320
|
+
: idPickerLabels?.initialResults ??
|
|
6321
|
+
'Start typing to search' })) : (jsx(Fragment, { children: collection.items.map((item, index) => (jsxs(Combobox.Item, { item: item, children: [renderDisplayFunction(item.raw), jsx(Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) })) }) })) : (jsx(Portal, { children: jsx(Combobox.Positioner, { children: jsx(Combobox.Content, { children: isError ? (jsx(Text, { p: 2, color: "fg.error", fontSize: "sm", children: idPickerLabels?.emptySearchResult ?? 'Loading failed' })) : isFetching || isLoading || isPending || isSearching ? (
|
|
5936
6322
|
// Show skeleton items to prevent UI shift
|
|
5937
6323
|
jsx(Fragment, { children: Array.from({ length: 5 }).map((_, index) => (jsx(Flex, { p: 2, align: "center", gap: 2, children: jsx(Skeleton, { height: "20px", flex: "1" }) }, `skeleton-${index}`))) })) : collection.items.length === 0 ? (jsx(Combobox.Empty, { children: searchText
|
|
5938
6324
|
? idPickerLabels?.emptySearchResult ?? 'No results found'
|
|
5939
6325
|
: idPickerLabels?.initialResults ??
|
|
5940
|
-
'Start typing to search' })) : (jsx(Fragment, { children: collection.items.map((item, index) => (
|
|
5941
|
-
? renderDisplayFunction(item.raw)
|
|
5942
|
-
: item.label }), jsx(Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) })) }) })) : (jsx(Portal, { children: jsx(Combobox.Positioner, { children: jsx(Combobox.Content, { children: isError ? (jsx(Text, { p: 2, color: "fg.error", fontSize: "sm", children: idPickerLabels?.emptySearchResult ?? 'Loading failed' })) : isFetching || isLoading || isPending || isSearching ? (
|
|
5943
|
-
// Show skeleton items to prevent UI shift
|
|
5944
|
-
jsx(Fragment, { children: Array.from({ length: 5 }).map((_, index) => (jsx(Flex, { p: 2, align: "center", gap: 2, children: jsx(Skeleton, { height: "20px", flex: "1" }) }, `skeleton-${index}`))) })) : collection.items.length === 0 ? (jsx(Combobox.Empty, { children: searchText
|
|
5945
|
-
? idPickerLabels?.emptySearchResult ?? 'No results found'
|
|
5946
|
-
: idPickerLabels?.initialResults ??
|
|
5947
|
-
'Start typing to search' })) : (jsx(Fragment, { children: collection.items.map((item, index) => (jsxs(Combobox.Item, { item: item, children: [jsx(Combobox.ItemText, { children: !!renderDisplayFunction === true
|
|
5948
|
-
? renderDisplayFunction(item.raw)
|
|
5949
|
-
: item.label }), jsx(Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) })) }) }) }))] })] }));
|
|
6326
|
+
'Start typing to search' })) : (jsx(Fragment, { children: collection.items.map((item, index) => (jsx(Combobox.Item, { item: item, children: renderDisplayFunction(item.raw) }, item.value ?? `item-${index}`))) })) }) }) }))] }) }));
|
|
5950
6327
|
};
|
|
5951
6328
|
|
|
5952
6329
|
const IdPickerMultiple = ({ column, schema, prefix, }) => {
|
|
5953
6330
|
const formI18n = useFormI18n(column, prefix, schema);
|
|
5954
|
-
const { required, gridColumn = 'span 12', gridRow = 'span 1'
|
|
6331
|
+
const { required, gridColumn = 'span 12', gridRow = 'span 1' } = schema;
|
|
5955
6332
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
5956
6333
|
const { colLabel, currentValue, searchText, setSearchText, isLoading, isFetching, isPending, isError, isSearching, isLoadingInitialValues, isFetchingInitialValues, missingIds, collection, idMap, idPickerLabels, insideDialog, renderDisplay: renderDisplayFn, errors, setValue, } = useIdPickerData({
|
|
5957
6334
|
column,
|
|
@@ -5965,7 +6342,8 @@ const IdPickerMultiple = ({ column, schema, prefix, }) => {
|
|
|
5965
6342
|
const handleValueChange = (details) => {
|
|
5966
6343
|
setValue(colLabel, details.value);
|
|
5967
6344
|
};
|
|
5968
|
-
|
|
6345
|
+
// Use renderDisplay from hook (which comes from schema) or fallback to default
|
|
6346
|
+
const renderDisplayFunction = renderDisplayFn || defaultRenderDisplay;
|
|
5969
6347
|
return (jsxs(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
5970
6348
|
gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: [currentValue.length > 0 && (jsx(Flex, { flexFlow: 'wrap', gap: 1, mb: 2, children: currentValue.map((id) => {
|
|
5971
6349
|
const item = idMap[id];
|
|
@@ -5990,16 +6368,12 @@ const IdPickerMultiple = ({ column, schema, prefix, }) => {
|
|
|
5990
6368
|
jsx(Fragment, { children: Array.from({ length: 5 }).map((_, index) => (jsx(Flex, { p: 2, align: "center", gap: 2, children: jsx(Skeleton, { height: "20px", flex: "1" }) }, `skeleton-${index}`))) })) : collection.items.length === 0 ? (jsx(Combobox.Empty, { children: searchText
|
|
5991
6369
|
? idPickerLabels?.emptySearchResult ?? 'No results found'
|
|
5992
6370
|
: idPickerLabels?.initialResults ??
|
|
5993
|
-
'Start typing to search' })) : (jsx(Fragment, { children: collection.items.map((item, index) => (jsxs(Combobox.Item, { item: item, children: [jsx(Combobox.
|
|
5994
|
-
? renderDisplayFunction(item.raw)
|
|
5995
|
-
: item.label }), jsx(Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) })) }) })) : (jsx(Portal, { children: jsx(Combobox.Positioner, { children: jsx(Combobox.Content, { children: isError ? (jsx(Text, { p: 2, color: "fg.error", fontSize: "sm", children: idPickerLabels?.emptySearchResult ?? 'Loading failed' })) : isFetching || isLoading || isPending || isSearching ? (
|
|
6371
|
+
'Start typing to search' })) : (jsx(Fragment, { children: collection.items.map((item, index) => (jsxs(Combobox.Item, { item: item, children: [renderDisplayFunction(item.raw), jsx(Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) })) }) })) : (jsx(Portal, { children: jsx(Combobox.Positioner, { children: jsx(Combobox.Content, { children: isError ? (jsx(Text, { p: 2, color: "fg.error", fontSize: "sm", children: idPickerLabels?.emptySearchResult ?? 'Loading failed' })) : isFetching || isLoading || isPending || isSearching ? (
|
|
5996
6372
|
// Show skeleton items to prevent UI shift
|
|
5997
6373
|
jsx(Fragment, { children: Array.from({ length: 5 }).map((_, index) => (jsx(Flex, { p: 2, align: "center", gap: 2, children: jsx(Skeleton, { height: "20px", flex: "1" }) }, `skeleton-${index}`))) })) : collection.items.length === 0 ? (jsx(Combobox.Empty, { children: searchText
|
|
5998
6374
|
? idPickerLabels?.emptySearchResult ?? 'No results found'
|
|
5999
6375
|
: idPickerLabels?.initialResults ??
|
|
6000
|
-
'Start typing to search' })) : (jsx(Fragment, { children: collection.items.map((item, index) => (jsxs(Combobox.Item, { item: item, children: [jsx(Combobox.
|
|
6001
|
-
? renderDisplayFunction(item.raw)
|
|
6002
|
-
: item.label }), jsx(Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) })) }) }) }))] })] }));
|
|
6376
|
+
'Start typing to search' })) : (jsx(Fragment, { children: collection.items.map((item, index) => (jsxs(Combobox.Item, { item: item, children: [renderDisplayFunction(item.raw), jsx(Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) })) }) }) }))] })] }));
|
|
6003
6377
|
};
|
|
6004
6378
|
|
|
6005
6379
|
const NumberInputRoot = React.forwardRef(function NumberInput$1(props, ref) {
|
|
@@ -6183,31 +6557,35 @@ RadioCard.ItemIndicator;
|
|
|
6183
6557
|
|
|
6184
6558
|
const TagPicker = ({ column, schema, prefix }) => {
|
|
6185
6559
|
const { watch, formState: { errors }, setValue, } = useFormContext();
|
|
6186
|
-
const { serverUrl } = useSchemaContext();
|
|
6187
6560
|
if (schema.properties == undefined) {
|
|
6188
|
-
throw new Error(
|
|
6561
|
+
throw new Error('schema properties undefined when using DatePicker');
|
|
6189
6562
|
}
|
|
6190
|
-
const { gridColumn, gridRow, in_table, object_id_column } = schema;
|
|
6563
|
+
const { gridColumn, gridRow, in_table, object_id_column, tagPicker } = schema;
|
|
6191
6564
|
if (in_table === undefined) {
|
|
6192
|
-
throw new Error(
|
|
6565
|
+
throw new Error('in_table is undefined when using TagPicker');
|
|
6193
6566
|
}
|
|
6194
6567
|
if (object_id_column === undefined) {
|
|
6195
|
-
throw new Error(
|
|
6568
|
+
throw new Error('object_id_column is undefined when using TagPicker');
|
|
6569
|
+
}
|
|
6570
|
+
if (!tagPicker?.queryFn) {
|
|
6571
|
+
throw new Error('tagPicker.queryFn is required in schema. serverUrl has been removed.');
|
|
6196
6572
|
}
|
|
6197
6573
|
const query = useQuery({
|
|
6198
6574
|
queryKey: [`tagpicker`, in_table],
|
|
6199
6575
|
queryFn: async () => {
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
in_table: "tables_tags_view",
|
|
6576
|
+
const result = await tagPicker.queryFn({
|
|
6577
|
+
in_table: 'tables_tags_view',
|
|
6203
6578
|
where: [
|
|
6204
6579
|
{
|
|
6205
|
-
id:
|
|
6580
|
+
id: 'table_name',
|
|
6206
6581
|
value: [in_table],
|
|
6207
6582
|
},
|
|
6208
6583
|
],
|
|
6209
6584
|
limit: 100,
|
|
6585
|
+
offset: 0,
|
|
6586
|
+
searching: '',
|
|
6210
6587
|
});
|
|
6588
|
+
return result.data || { data: [] };
|
|
6211
6589
|
},
|
|
6212
6590
|
staleTime: 10000,
|
|
6213
6591
|
});
|
|
@@ -6215,17 +6593,19 @@ const TagPicker = ({ column, schema, prefix }) => {
|
|
|
6215
6593
|
const existingTagsQuery = useQuery({
|
|
6216
6594
|
queryKey: [`existing`, { in_table, object_id_column }, object_id],
|
|
6217
6595
|
queryFn: async () => {
|
|
6218
|
-
|
|
6219
|
-
serverUrl,
|
|
6596
|
+
const result = await tagPicker.queryFn({
|
|
6220
6597
|
in_table: in_table,
|
|
6221
6598
|
where: [
|
|
6222
6599
|
{
|
|
6223
6600
|
id: object_id_column,
|
|
6224
|
-
value: object_id[0],
|
|
6601
|
+
value: [object_id[0]],
|
|
6225
6602
|
},
|
|
6226
6603
|
],
|
|
6227
6604
|
limit: 100,
|
|
6605
|
+
offset: 0,
|
|
6606
|
+
searching: '',
|
|
6228
6607
|
});
|
|
6608
|
+
return result.data || { data: [] };
|
|
6229
6609
|
},
|
|
6230
6610
|
enabled: object_id != undefined,
|
|
6231
6611
|
staleTime: 10000,
|
|
@@ -6236,9 +6616,9 @@ const TagPicker = ({ column, schema, prefix }) => {
|
|
|
6236
6616
|
if (!!object_id === false) {
|
|
6237
6617
|
return jsx(Fragment, {});
|
|
6238
6618
|
}
|
|
6239
|
-
return (jsxs(Flex, { flexFlow:
|
|
6619
|
+
return (jsxs(Flex, { flexFlow: 'column', gap: 4, gridColumn,
|
|
6240
6620
|
gridRow, children: [isFetching && jsx(Fragment, { children: "isFetching" }), isLoading && jsx(Fragment, { children: "isLoading" }), isPending && jsx(Fragment, { children: "isPending" }), isError && jsx(Fragment, { children: "isError" }), dataList.map(({ parent_tag_name, all_tags, is_mutually_exclusive }) => {
|
|
6241
|
-
return (jsxs(Flex, { flexFlow:
|
|
6621
|
+
return (jsxs(Flex, { flexFlow: 'column', gap: 2, children: [jsx(Text, { children: parent_tag_name }), is_mutually_exclusive && (jsx(RadioCardRoot, { defaultValue: "next", variant: 'surface', onValueChange: (tagIds) => {
|
|
6242
6622
|
const existedTags = Object.values(all_tags)
|
|
6243
6623
|
.filter(({ id }) => {
|
|
6244
6624
|
return existingTagList.some(({ tag_id }) => tag_id === id);
|
|
@@ -6250,20 +6630,20 @@ const TagPicker = ({ column, schema, prefix }) => {
|
|
|
6250
6630
|
tagIds.value,
|
|
6251
6631
|
]);
|
|
6252
6632
|
setValue(`${column}.${parent_tag_name}.old`, existedTags);
|
|
6253
|
-
}, children: jsx(Flex, { flexFlow:
|
|
6633
|
+
}, children: jsx(Flex, { flexFlow: 'wrap', gap: 2, children: Object.entries(all_tags).map(([tagName, { id }]) => {
|
|
6254
6634
|
if (existingTagList.some(({ tag_id }) => tag_id === id)) {
|
|
6255
|
-
return (jsx(RadioCardItem, { label: tagName, value: id, flex:
|
|
6635
|
+
return (jsx(RadioCardItem, { label: tagName, value: id, flex: '0 0 0%', disabled: true }, `${tagName}-${id}`));
|
|
6256
6636
|
}
|
|
6257
|
-
return (jsx(RadioCardItem, { label: tagName, value: id, flex:
|
|
6637
|
+
return (jsx(RadioCardItem, { label: tagName, value: id, flex: '0 0 0%', colorPalette: 'blue' }, `${tagName}-${id}`));
|
|
6258
6638
|
}) }) })), !is_mutually_exclusive && (jsx(CheckboxGroup, { onValueChange: (tagIds) => {
|
|
6259
6639
|
setValue(`${column}.${parent_tag_name}.current`, tagIds);
|
|
6260
|
-
}, children: jsx(Flex, { flexFlow:
|
|
6640
|
+
}, children: jsx(Flex, { flexFlow: 'wrap', gap: 2, children: Object.entries(all_tags).map(([tagName, { id }]) => {
|
|
6261
6641
|
if (existingTagList.some(({ tag_id }) => tag_id === id)) {
|
|
6262
|
-
return (jsx(CheckboxCard, { label: tagName, value: id, flex:
|
|
6642
|
+
return (jsx(CheckboxCard, { label: tagName, value: id, flex: '0 0 0%', disabled: true, colorPalette: 'blue' }, `${tagName}-${id}`));
|
|
6263
6643
|
}
|
|
6264
|
-
return (jsx(CheckboxCard, { label: tagName, value: id, flex:
|
|
6644
|
+
return (jsx(CheckboxCard, { label: tagName, value: id, flex: '0 0 0%' }, `${tagName}-${id}`));
|
|
6265
6645
|
}) }) }))] }, `tag-${parent_tag_name}`));
|
|
6266
|
-
}), errors[`${column}`] && (jsx(Text, { color:
|
|
6646
|
+
}), errors[`${column}`] && (jsx(Text, { color: 'red.400', children: (errors[`${column}`]?.message ?? 'No error message') }))] }));
|
|
6267
6647
|
};
|
|
6268
6648
|
|
|
6269
6649
|
const Textarea = forwardRef(({ value, defaultValue, placeholder, onChange, onFocus, onBlur, disabled = false, readOnly = false, className, rows = 4, maxLength, autoFocus = false, invalid = false, required = false, label, helperText, errorText, ...props }, ref) => {
|
|
@@ -6370,11 +6750,193 @@ const TextAreaInput = ({ column, schema, prefix, }) => {
|
|
|
6370
6750
|
|
|
6371
6751
|
dayjs.extend(utc);
|
|
6372
6752
|
dayjs.extend(timezone);
|
|
6373
|
-
const TimePicker$1 = (
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6753
|
+
const TimePicker$1 = (props) => {
|
|
6754
|
+
const { format = '12h', value: controlledValue, onChange: controlledOnChange, hour: uncontrolledHour, setHour: uncontrolledSetHour, minute: uncontrolledMinute, setMinute: uncontrolledSetMinute, startTime, selectedDate, timezone = 'Asia/Hong_Kong', portalled = true, labels = {
|
|
6755
|
+
placeholder: format === '24h' ? 'HH:mm:ss' : 'hh:mm AM/PM',
|
|
6756
|
+
emptyMessage: 'No time found',
|
|
6757
|
+
}, onTimeChange, } = props;
|
|
6758
|
+
const is24Hour = format === '24h';
|
|
6759
|
+
const uncontrolledMeridiem = is24Hour ? undefined : props.meridiem;
|
|
6760
|
+
const uncontrolledSetMeridiem = is24Hour ? undefined : props.setMeridiem;
|
|
6761
|
+
const uncontrolledSecond = is24Hour ? props.second : undefined;
|
|
6762
|
+
const uncontrolledSetSecond = is24Hour ? props.setSecond : undefined;
|
|
6763
|
+
// Determine if we're in controlled mode
|
|
6764
|
+
const isControlled = controlledValue !== undefined;
|
|
6765
|
+
// Parse time string to extract hour, minute, second, meridiem
|
|
6766
|
+
const parseTimeString = (timeStr) => {
|
|
6767
|
+
if (!timeStr || !timeStr.trim()) {
|
|
6768
|
+
return { hour: null, minute: null, second: null, meridiem: null };
|
|
6769
|
+
}
|
|
6770
|
+
// Remove timezone suffix if present (e.g., "14:30:00Z" -> "14:30:00")
|
|
6771
|
+
const timeWithoutTz = timeStr.replace(/[Z+-]\d{2}:?\d{2}$/, '').trim();
|
|
6772
|
+
// Try parsing 24-hour format: "HH:mm:ss" or "HH:mm"
|
|
6773
|
+
const time24Pattern = /^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?$/;
|
|
6774
|
+
const match24 = timeWithoutTz.match(time24Pattern);
|
|
6775
|
+
if (match24) {
|
|
6776
|
+
const hour24 = parseInt(match24[1], 10);
|
|
6777
|
+
const minute = parseInt(match24[2], 10);
|
|
6778
|
+
const second = match24[3] ? parseInt(match24[3], 10) : 0;
|
|
6779
|
+
if (hour24 >= 0 &&
|
|
6780
|
+
hour24 <= 23 &&
|
|
6781
|
+
minute >= 0 &&
|
|
6782
|
+
minute <= 59 &&
|
|
6783
|
+
second >= 0 &&
|
|
6784
|
+
second <= 59) {
|
|
6785
|
+
if (is24Hour) {
|
|
6786
|
+
return { hour: hour24, minute, second, meridiem: null };
|
|
6787
|
+
}
|
|
6788
|
+
else {
|
|
6789
|
+
// Convert to 12-hour format
|
|
6790
|
+
let hour12 = hour24;
|
|
6791
|
+
let meridiem;
|
|
6792
|
+
if (hour24 === 0) {
|
|
6793
|
+
hour12 = 12;
|
|
6794
|
+
meridiem = 'am';
|
|
6795
|
+
}
|
|
6796
|
+
else if (hour24 === 12) {
|
|
6797
|
+
hour12 = 12;
|
|
6798
|
+
meridiem = 'pm';
|
|
6799
|
+
}
|
|
6800
|
+
else if (hour24 > 12) {
|
|
6801
|
+
hour12 = hour24 - 12;
|
|
6802
|
+
meridiem = 'pm';
|
|
6803
|
+
}
|
|
6804
|
+
else {
|
|
6805
|
+
hour12 = hour24;
|
|
6806
|
+
meridiem = 'am';
|
|
6807
|
+
}
|
|
6808
|
+
return { hour: hour12, minute, second: null, meridiem };
|
|
6809
|
+
}
|
|
6810
|
+
}
|
|
6811
|
+
}
|
|
6812
|
+
// Try parsing 12-hour format: "hh:mm AM/PM" or "hh:mm:ss AM/PM"
|
|
6813
|
+
const time12Pattern = /^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?\s*(am|pm|AM|PM)$/i;
|
|
6814
|
+
const match12 = timeWithoutTz.match(time12Pattern);
|
|
6815
|
+
if (match12 && !is24Hour) {
|
|
6816
|
+
const hour12 = parseInt(match12[1], 10);
|
|
6817
|
+
const minute = parseInt(match12[2], 10);
|
|
6818
|
+
const second = match12[3] ? parseInt(match12[3], 10) : null;
|
|
6819
|
+
const meridiem = match12[4].toLowerCase();
|
|
6820
|
+
if (hour12 >= 1 &&
|
|
6821
|
+
hour12 <= 12 &&
|
|
6822
|
+
minute >= 0 &&
|
|
6823
|
+
minute <= 59 &&
|
|
6824
|
+
(second === null || (second >= 0 && second <= 59))) {
|
|
6825
|
+
return { hour: hour12, minute, second, meridiem };
|
|
6826
|
+
}
|
|
6827
|
+
}
|
|
6828
|
+
return { hour: null, minute: null, second: null, meridiem: null };
|
|
6829
|
+
};
|
|
6830
|
+
// Format time values to time string
|
|
6831
|
+
const formatTimeString = (hour, minute, second, meridiem) => {
|
|
6832
|
+
if (hour === null || minute === null) {
|
|
6833
|
+
return undefined;
|
|
6834
|
+
}
|
|
6835
|
+
if (is24Hour) {
|
|
6836
|
+
const h = hour.toString().padStart(2, '0');
|
|
6837
|
+
const m = minute.toString().padStart(2, '0');
|
|
6838
|
+
const s = (second ?? 0).toString().padStart(2, '0');
|
|
6839
|
+
return `${h}:${m}:${s}`;
|
|
6840
|
+
}
|
|
6841
|
+
else {
|
|
6842
|
+
if (meridiem === null) {
|
|
6843
|
+
return undefined;
|
|
6844
|
+
}
|
|
6845
|
+
const h = hour.toString();
|
|
6846
|
+
const m = minute.toString().padStart(2, '0');
|
|
6847
|
+
return `${h}:${m} ${meridiem.toUpperCase()}`;
|
|
6848
|
+
}
|
|
6849
|
+
};
|
|
6850
|
+
// Internal state for controlled mode
|
|
6851
|
+
const [internalHour, setInternalHour] = useState(null);
|
|
6852
|
+
const [internalMinute, setInternalMinute] = useState(null);
|
|
6853
|
+
const [internalSecond, setInternalSecond] = useState(null);
|
|
6854
|
+
const [internalMeridiem, setInternalMeridiem] = useState(null);
|
|
6855
|
+
// Use controlled or uncontrolled values
|
|
6856
|
+
const hour = isControlled ? internalHour : uncontrolledHour ?? null;
|
|
6857
|
+
const minute = isControlled ? internalMinute : uncontrolledMinute ?? null;
|
|
6858
|
+
const second = isControlled ? internalSecond : uncontrolledSecond ?? null;
|
|
6859
|
+
const meridiem = isControlled
|
|
6860
|
+
? internalMeridiem
|
|
6861
|
+
: uncontrolledMeridiem ?? null;
|
|
6862
|
+
// Setters that work for both modes
|
|
6863
|
+
const setHour = isControlled
|
|
6864
|
+
? setInternalHour
|
|
6865
|
+
: uncontrolledSetHour || (() => { });
|
|
6866
|
+
const setMinute = isControlled
|
|
6867
|
+
? setInternalMinute
|
|
6868
|
+
: uncontrolledSetMinute || (() => { });
|
|
6869
|
+
const setSecond = isControlled
|
|
6870
|
+
? setInternalSecond
|
|
6871
|
+
: uncontrolledSetSecond || (() => { });
|
|
6872
|
+
const setMeridiem = isControlled
|
|
6873
|
+
? setInternalMeridiem
|
|
6874
|
+
: uncontrolledSetMeridiem || (() => { });
|
|
6875
|
+
// Sync internal state with controlled value prop
|
|
6876
|
+
const prevValueRef = useRef(controlledValue);
|
|
6877
|
+
useEffect(() => {
|
|
6878
|
+
if (!isControlled)
|
|
6879
|
+
return;
|
|
6880
|
+
if (prevValueRef.current === controlledValue) {
|
|
6881
|
+
return;
|
|
6882
|
+
}
|
|
6883
|
+
prevValueRef.current = controlledValue;
|
|
6884
|
+
const parsed = parseTimeString(controlledValue);
|
|
6885
|
+
setInternalHour(parsed.hour);
|
|
6886
|
+
setInternalMinute(parsed.minute);
|
|
6887
|
+
if (is24Hour) {
|
|
6888
|
+
setInternalSecond(parsed.second);
|
|
6889
|
+
}
|
|
6890
|
+
else {
|
|
6891
|
+
setInternalMeridiem(parsed.meridiem);
|
|
6892
|
+
}
|
|
6893
|
+
}, [controlledValue, isControlled, is24Hour]);
|
|
6894
|
+
// Wrapper onChange that calls both controlled and uncontrolled onChange
|
|
6895
|
+
const handleTimeChange = (newHour, newMinute, newSecond, newMeridiem) => {
|
|
6896
|
+
if (isControlled) {
|
|
6897
|
+
const timeString = formatTimeString(newHour, newMinute, newSecond, newMeridiem);
|
|
6898
|
+
controlledOnChange?.(timeString);
|
|
6899
|
+
}
|
|
6900
|
+
else {
|
|
6901
|
+
// Call legacy onTimeChange if provided
|
|
6902
|
+
if (onTimeChange) {
|
|
6903
|
+
if (is24Hour) {
|
|
6904
|
+
const timeChange24h = onTimeChange;
|
|
6905
|
+
timeChange24h({
|
|
6906
|
+
hour: newHour,
|
|
6907
|
+
minute: newMinute,
|
|
6908
|
+
second: newSecond,
|
|
6909
|
+
});
|
|
6910
|
+
}
|
|
6911
|
+
else {
|
|
6912
|
+
const timeChange12h = onTimeChange;
|
|
6913
|
+
timeChange12h({
|
|
6914
|
+
hour: newHour,
|
|
6915
|
+
minute: newMinute,
|
|
6916
|
+
meridiem: newMeridiem,
|
|
6917
|
+
});
|
|
6918
|
+
}
|
|
6919
|
+
}
|
|
6920
|
+
}
|
|
6921
|
+
};
|
|
6922
|
+
const [inputValue, setInputValue] = useState('');
|
|
6923
|
+
// Sync inputValue with current time
|
|
6924
|
+
useEffect(() => {
|
|
6925
|
+
if (is24Hour && second !== undefined) {
|
|
6926
|
+
if (hour !== null && minute !== null && second !== null) {
|
|
6927
|
+
const formatted = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}:${second.toString().padStart(2, '0')}`;
|
|
6928
|
+
setInputValue(formatted);
|
|
6929
|
+
}
|
|
6930
|
+
else {
|
|
6931
|
+
setInputValue('');
|
|
6932
|
+
}
|
|
6933
|
+
}
|
|
6934
|
+
else {
|
|
6935
|
+
// 12-hour format - input is managed by combobox
|
|
6936
|
+
setInputValue('');
|
|
6937
|
+
}
|
|
6938
|
+
}, [hour, minute, second, is24Hour]);
|
|
6939
|
+
// Generate time options based on format
|
|
6378
6940
|
const timeOptions = useMemo(() => {
|
|
6379
6941
|
const options = [];
|
|
6380
6942
|
// Get start time for comparison if provided
|
|
@@ -6385,32 +6947,25 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
|
|
|
6385
6947
|
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6386
6948
|
if (startDateObj.isValid() && selectedDateObj.isValid()) {
|
|
6387
6949
|
startDateTime = startDateObj;
|
|
6388
|
-
// Only filter if dates are the same
|
|
6389
6950
|
shouldFilterByDate =
|
|
6390
6951
|
startDateObj.format('YYYY-MM-DD') ===
|
|
6391
6952
|
selectedDateObj.format('YYYY-MM-DD');
|
|
6392
6953
|
}
|
|
6393
6954
|
}
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
for (let
|
|
6397
|
-
for (
|
|
6398
|
-
//
|
|
6399
|
-
let hour24 = h;
|
|
6400
|
-
if (mer === 'am' && h === 12)
|
|
6401
|
-
hour24 = 0;
|
|
6402
|
-
else if (mer === 'pm' && h < 12)
|
|
6403
|
-
hour24 = h + 12;
|
|
6404
|
-
// Filter out times that would result in negative duration (only when dates are the same)
|
|
6955
|
+
if (is24Hour) {
|
|
6956
|
+
// Generate 24-hour format options (0-23 for hours)
|
|
6957
|
+
for (let h = 0; h < 24; h++) {
|
|
6958
|
+
for (let m = 0; m < 60; m += 15) {
|
|
6959
|
+
// Filter out times that would result in negative duration
|
|
6405
6960
|
if (startDateTime && selectedDate && shouldFilterByDate) {
|
|
6406
6961
|
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6407
6962
|
const optionDateTime = selectedDateObj
|
|
6408
|
-
.hour(
|
|
6963
|
+
.hour(h)
|
|
6409
6964
|
.minute(m)
|
|
6410
6965
|
.second(0)
|
|
6411
6966
|
.millisecond(0);
|
|
6412
6967
|
if (optionDateTime.isBefore(startDateTime)) {
|
|
6413
|
-
continue;
|
|
6968
|
+
continue;
|
|
6414
6969
|
}
|
|
6415
6970
|
}
|
|
6416
6971
|
// Calculate duration if startTime is provided
|
|
@@ -6418,7 +6973,7 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
|
|
|
6418
6973
|
if (startDateTime && selectedDate) {
|
|
6419
6974
|
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6420
6975
|
const optionDateTime = selectedDateObj
|
|
6421
|
-
.hour(
|
|
6976
|
+
.hour(h)
|
|
6422
6977
|
.minute(m)
|
|
6423
6978
|
.second(0)
|
|
6424
6979
|
.millisecond(0);
|
|
@@ -6443,58 +6998,204 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
|
|
|
6443
6998
|
}
|
|
6444
6999
|
}
|
|
6445
7000
|
}
|
|
6446
|
-
const
|
|
6447
|
-
const minuteDisplay = m.toString().padStart(2, '0');
|
|
6448
|
-
const timeDisplay = `${hourDisplay}:${minuteDisplay} ${mer.toUpperCase()}`;
|
|
7001
|
+
const timeDisplay = `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:00`;
|
|
6449
7002
|
options.push({
|
|
6450
7003
|
label: timeDisplay,
|
|
6451
|
-
value: `${h}:${m}
|
|
7004
|
+
value: `${h}:${m}:0`,
|
|
6452
7005
|
hour: h,
|
|
6453
7006
|
minute: m,
|
|
6454
|
-
|
|
6455
|
-
searchText: timeDisplay,
|
|
7007
|
+
second: 0,
|
|
7008
|
+
searchText: timeDisplay,
|
|
6456
7009
|
durationText,
|
|
6457
7010
|
});
|
|
6458
7011
|
}
|
|
6459
7012
|
}
|
|
6460
7013
|
}
|
|
7014
|
+
else {
|
|
7015
|
+
// Generate 12-hour format options (1-12 for hours, AM/PM)
|
|
7016
|
+
for (let h = 1; h <= 12; h++) {
|
|
7017
|
+
for (let m = 0; m < 60; m += 15) {
|
|
7018
|
+
for (const mer of ['am', 'pm']) {
|
|
7019
|
+
// Convert 12-hour to 24-hour for comparison
|
|
7020
|
+
let hour24 = h;
|
|
7021
|
+
if (mer === 'am' && h === 12)
|
|
7022
|
+
hour24 = 0;
|
|
7023
|
+
else if (mer === 'pm' && h < 12)
|
|
7024
|
+
hour24 = h + 12;
|
|
7025
|
+
// Filter out times that would result in negative duration
|
|
7026
|
+
if (startDateTime && selectedDate && shouldFilterByDate) {
|
|
7027
|
+
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
7028
|
+
const optionDateTime = selectedDateObj
|
|
7029
|
+
.hour(hour24)
|
|
7030
|
+
.minute(m)
|
|
7031
|
+
.second(0)
|
|
7032
|
+
.millisecond(0);
|
|
7033
|
+
if (optionDateTime.isBefore(startDateTime)) {
|
|
7034
|
+
continue;
|
|
7035
|
+
}
|
|
7036
|
+
}
|
|
7037
|
+
// Calculate duration if startTime is provided
|
|
7038
|
+
let durationText;
|
|
7039
|
+
if (startDateTime && selectedDate) {
|
|
7040
|
+
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
7041
|
+
const optionDateTime = selectedDateObj
|
|
7042
|
+
.hour(hour24)
|
|
7043
|
+
.minute(m)
|
|
7044
|
+
.second(0)
|
|
7045
|
+
.millisecond(0);
|
|
7046
|
+
if (optionDateTime.isValid() &&
|
|
7047
|
+
optionDateTime.isAfter(startDateTime)) {
|
|
7048
|
+
const diffMs = optionDateTime.diff(startDateTime);
|
|
7049
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
7050
|
+
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
7051
|
+
const diffSeconds = Math.floor((diffMs % (1000 * 60)) / 1000);
|
|
7052
|
+
if (diffHours > 0 || diffMinutes > 0 || diffSeconds > 0) {
|
|
7053
|
+
let diffText = '';
|
|
7054
|
+
if (diffHours > 0) {
|
|
7055
|
+
diffText = `${diffHours}h ${diffMinutes}m`;
|
|
7056
|
+
}
|
|
7057
|
+
else if (diffMinutes > 0) {
|
|
7058
|
+
diffText = `${diffMinutes}m ${diffSeconds}s`;
|
|
7059
|
+
}
|
|
7060
|
+
else {
|
|
7061
|
+
diffText = `${diffSeconds}s`;
|
|
7062
|
+
}
|
|
7063
|
+
durationText = `+${diffText}`;
|
|
7064
|
+
}
|
|
7065
|
+
}
|
|
7066
|
+
}
|
|
7067
|
+
const hourDisplay = h.toString();
|
|
7068
|
+
const minuteDisplay = m.toString().padStart(2, '0');
|
|
7069
|
+
const timeDisplay = `${hourDisplay}:${minuteDisplay} ${mer.toUpperCase()}`;
|
|
7070
|
+
options.push({
|
|
7071
|
+
label: timeDisplay,
|
|
7072
|
+
value: `${h}:${m}:${mer}`,
|
|
7073
|
+
hour: h,
|
|
7074
|
+
minute: m,
|
|
7075
|
+
meridiem: mer,
|
|
7076
|
+
searchText: timeDisplay,
|
|
7077
|
+
durationText,
|
|
7078
|
+
});
|
|
7079
|
+
}
|
|
7080
|
+
}
|
|
7081
|
+
}
|
|
7082
|
+
// Sort 12-hour options by time (convert to 24-hour for proper chronological sorting)
|
|
7083
|
+
return options.sort((a, b) => {
|
|
7084
|
+
const a12 = a;
|
|
7085
|
+
const b12 = b;
|
|
7086
|
+
let hour24A = a12.hour;
|
|
7087
|
+
if (a12.meridiem === 'am' && a12.hour === 12)
|
|
7088
|
+
hour24A = 0;
|
|
7089
|
+
else if (a12.meridiem === 'pm' && a12.hour < 12)
|
|
7090
|
+
hour24A = a12.hour + 12;
|
|
7091
|
+
let hour24B = b12.hour;
|
|
7092
|
+
if (b12.meridiem === 'am' && b12.hour === 12)
|
|
7093
|
+
hour24B = 0;
|
|
7094
|
+
else if (b12.meridiem === 'pm' && b12.hour < 12)
|
|
7095
|
+
hour24B = b12.hour + 12;
|
|
7096
|
+
if (hour24A !== hour24B) {
|
|
7097
|
+
return hour24A - hour24B;
|
|
7098
|
+
}
|
|
7099
|
+
return a12.minute - b12.minute;
|
|
7100
|
+
});
|
|
7101
|
+
}
|
|
6461
7102
|
return options;
|
|
6462
|
-
}, [startTime, selectedDate, timezone]);
|
|
7103
|
+
}, [startTime, selectedDate, timezone, is24Hour]);
|
|
7104
|
+
// itemToString returns only the clean display text (no metadata)
|
|
7105
|
+
const itemToString = useMemo(() => {
|
|
7106
|
+
return (item) => {
|
|
7107
|
+
return item.searchText;
|
|
7108
|
+
};
|
|
7109
|
+
}, []);
|
|
7110
|
+
// Custom filter function
|
|
6463
7111
|
const { contains } = useFilter({ sensitivity: 'base' });
|
|
7112
|
+
const customTimeFilter = useMemo(() => {
|
|
7113
|
+
if (is24Hour) {
|
|
7114
|
+
return contains; // Simple contains filter for 24-hour format
|
|
7115
|
+
}
|
|
7116
|
+
// For 12-hour format, support both 12-hour and 24-hour input
|
|
7117
|
+
return (itemText, filterText) => {
|
|
7118
|
+
if (!filterText) {
|
|
7119
|
+
return true;
|
|
7120
|
+
}
|
|
7121
|
+
const lowerItemText = itemText.toLowerCase();
|
|
7122
|
+
const lowerFilterText = filterText.toLowerCase();
|
|
7123
|
+
if (lowerItemText.includes(lowerFilterText)) {
|
|
7124
|
+
return true;
|
|
7125
|
+
}
|
|
7126
|
+
const item = timeOptions.find((opt) => opt.searchText.toLowerCase() === lowerItemText);
|
|
7127
|
+
if (!item || !('meridiem' in item)) {
|
|
7128
|
+
return false;
|
|
7129
|
+
}
|
|
7130
|
+
// Convert item to 24-hour format for matching
|
|
7131
|
+
let hour24 = item.hour;
|
|
7132
|
+
if (item.meridiem === 'am' && item.hour === 12)
|
|
7133
|
+
hour24 = 0;
|
|
7134
|
+
else if (item.meridiem === 'pm' && item.hour < 12)
|
|
7135
|
+
hour24 = item.hour + 12;
|
|
7136
|
+
const hour24Str = hour24.toString().padStart(2, '0');
|
|
7137
|
+
const minuteStr = item.minute.toString().padStart(2, '0');
|
|
7138
|
+
const formats = [
|
|
7139
|
+
`${hour24Str}:${minuteStr}`,
|
|
7140
|
+
`${hour24Str}${minuteStr}`,
|
|
7141
|
+
hour24Str,
|
|
7142
|
+
`${hour24}:${minuteStr}`,
|
|
7143
|
+
hour24.toString(),
|
|
7144
|
+
];
|
|
7145
|
+
return formats.some((format) => format.toLowerCase().includes(lowerFilterText) ||
|
|
7146
|
+
lowerFilterText.includes(format.toLowerCase()));
|
|
7147
|
+
};
|
|
7148
|
+
}, [timeOptions, is24Hour, contains]);
|
|
6464
7149
|
const { collection, filter } = useListCollection({
|
|
6465
7150
|
initialItems: timeOptions,
|
|
6466
|
-
itemToString:
|
|
7151
|
+
itemToString: itemToString,
|
|
6467
7152
|
itemToValue: (item) => item.value,
|
|
6468
|
-
filter:
|
|
7153
|
+
filter: customTimeFilter,
|
|
6469
7154
|
});
|
|
6470
7155
|
// Get current value string for combobox
|
|
6471
7156
|
const currentValue = useMemo(() => {
|
|
6472
|
-
if (
|
|
6473
|
-
|
|
7157
|
+
if (is24Hour) {
|
|
7158
|
+
if (hour === null || minute === null || second === null) {
|
|
7159
|
+
return '';
|
|
7160
|
+
}
|
|
7161
|
+
return `${hour}:${minute}:${second}`;
|
|
7162
|
+
}
|
|
7163
|
+
else {
|
|
7164
|
+
if (hour === null || minute === null || meridiem === null) {
|
|
7165
|
+
return '';
|
|
7166
|
+
}
|
|
7167
|
+
return `${hour}:${minute}:${meridiem}`;
|
|
6474
7168
|
}
|
|
6475
|
-
|
|
6476
|
-
}, [hour, minute, meridiem]);
|
|
7169
|
+
}, [hour, minute, second, meridiem, is24Hour]);
|
|
6477
7170
|
// Calculate duration difference
|
|
6478
7171
|
const durationDiff = useMemo(() => {
|
|
6479
|
-
if (!startTime ||
|
|
6480
|
-
!selectedDate ||
|
|
6481
|
-
hour === null ||
|
|
6482
|
-
minute === null ||
|
|
6483
|
-
meridiem === null) {
|
|
7172
|
+
if (!startTime || !selectedDate || hour === null || minute === null) {
|
|
6484
7173
|
return null;
|
|
6485
7174
|
}
|
|
7175
|
+
if (is24Hour) {
|
|
7176
|
+
if (second === null)
|
|
7177
|
+
return null;
|
|
7178
|
+
}
|
|
7179
|
+
else {
|
|
7180
|
+
if (meridiem === null)
|
|
7181
|
+
return null;
|
|
7182
|
+
}
|
|
6486
7183
|
const startDateObj = dayjs(startTime).tz(timezone);
|
|
6487
7184
|
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6488
|
-
// Convert
|
|
7185
|
+
// Convert to 24-hour format
|
|
6489
7186
|
let hour24 = hour;
|
|
6490
|
-
if (
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
7187
|
+
if (!is24Hour && meridiem) {
|
|
7188
|
+
if (meridiem === 'am' && hour === 12)
|
|
7189
|
+
hour24 = 0;
|
|
7190
|
+
else if (meridiem === 'pm' && hour < 12)
|
|
7191
|
+
hour24 = hour + 12;
|
|
7192
|
+
}
|
|
6494
7193
|
const currentDateTime = selectedDateObj
|
|
6495
7194
|
.hour(hour24)
|
|
6496
7195
|
.minute(minute)
|
|
6497
|
-
.second(
|
|
7196
|
+
.second(is24Hour && second !== null && second !== undefined
|
|
7197
|
+
? second
|
|
7198
|
+
: 0)
|
|
6498
7199
|
.millisecond(0);
|
|
6499
7200
|
if (!startDateObj.isValid() || !currentDateTime.isValid()) {
|
|
6500
7201
|
return null;
|
|
@@ -6520,13 +7221,28 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
|
|
|
6520
7221
|
return `+${diffText}`;
|
|
6521
7222
|
}
|
|
6522
7223
|
return null;
|
|
6523
|
-
}, [
|
|
7224
|
+
}, [
|
|
7225
|
+
hour,
|
|
7226
|
+
minute,
|
|
7227
|
+
second,
|
|
7228
|
+
meridiem,
|
|
7229
|
+
startTime,
|
|
7230
|
+
selectedDate,
|
|
7231
|
+
timezone,
|
|
7232
|
+
is24Hour,
|
|
7233
|
+
]);
|
|
6524
7234
|
const handleClear = () => {
|
|
6525
7235
|
setHour(null);
|
|
6526
7236
|
setMinute(null);
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
7237
|
+
if (is24Hour && setSecond) {
|
|
7238
|
+
setSecond(null);
|
|
7239
|
+
handleTimeChange(null, null, null, null);
|
|
7240
|
+
}
|
|
7241
|
+
else if (!is24Hour && setMeridiem) {
|
|
7242
|
+
setMeridiem(null);
|
|
7243
|
+
handleTimeChange(null, null, null, null);
|
|
7244
|
+
}
|
|
7245
|
+
filter('');
|
|
6530
7246
|
};
|
|
6531
7247
|
const handleValueChange = (details) => {
|
|
6532
7248
|
if (details.value.length === 0) {
|
|
@@ -6538,71 +7254,165 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
|
|
|
6538
7254
|
if (selectedOption) {
|
|
6539
7255
|
setHour(selectedOption.hour);
|
|
6540
7256
|
setMinute(selectedOption.minute);
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
}
|
|
7257
|
+
filter('');
|
|
7258
|
+
if (is24Hour) {
|
|
7259
|
+
const opt24 = selectedOption;
|
|
7260
|
+
if (setSecond)
|
|
7261
|
+
setSecond(opt24.second);
|
|
7262
|
+
handleTimeChange(opt24.hour, opt24.minute, opt24.second, null);
|
|
7263
|
+
}
|
|
7264
|
+
else {
|
|
7265
|
+
const opt12 = selectedOption;
|
|
7266
|
+
if (setMeridiem)
|
|
7267
|
+
setMeridiem(opt12.meridiem);
|
|
7268
|
+
handleTimeChange(opt12.hour, opt12.minute, null, opt12.meridiem);
|
|
7269
|
+
}
|
|
6548
7270
|
}
|
|
6549
7271
|
};
|
|
6550
7272
|
// Parse input value and update state
|
|
6551
7273
|
const parseAndCommitInput = (value) => {
|
|
6552
7274
|
const trimmedValue = value.trim();
|
|
6553
|
-
// Filter the collection based on input
|
|
6554
7275
|
filter(trimmedValue);
|
|
6555
7276
|
if (!trimmedValue) {
|
|
6556
7277
|
return;
|
|
6557
7278
|
}
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
7279
|
+
if (is24Hour) {
|
|
7280
|
+
// Parse 24-hour format: "HH:mm:ss" or "HH:mm" or "HHmmss" or "HHmm"
|
|
7281
|
+
const timePattern = /^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?$/;
|
|
7282
|
+
const match = trimmedValue.match(timePattern);
|
|
7283
|
+
if (match) {
|
|
7284
|
+
const parsedHour = parseInt(match[1], 10);
|
|
7285
|
+
const parsedMinute = parseInt(match[2], 10);
|
|
7286
|
+
const parsedSecond = match[3] ? parseInt(match[3], 10) : 0;
|
|
7287
|
+
if (parsedHour >= 0 &&
|
|
7288
|
+
parsedHour <= 23 &&
|
|
7289
|
+
parsedMinute >= 0 &&
|
|
7290
|
+
parsedMinute <= 59 &&
|
|
7291
|
+
parsedSecond >= 0 &&
|
|
7292
|
+
parsedSecond <= 59) {
|
|
7293
|
+
setHour(parsedHour);
|
|
7294
|
+
setMinute(parsedMinute);
|
|
7295
|
+
if (setSecond)
|
|
7296
|
+
setSecond(parsedSecond);
|
|
7297
|
+
handleTimeChange(parsedHour, parsedMinute, parsedSecond, null);
|
|
7298
|
+
return;
|
|
7299
|
+
}
|
|
7300
|
+
}
|
|
7301
|
+
// Try numbers only format: "123045" or "1230"
|
|
7302
|
+
const numbersOnly = trimmedValue.replace(/[^0-9]/g, '');
|
|
7303
|
+
if (numbersOnly.length >= 4) {
|
|
7304
|
+
const parsedHour = parseInt(numbersOnly.slice(0, 2), 10);
|
|
7305
|
+
const parsedMinute = parseInt(numbersOnly.slice(2, 4), 10);
|
|
7306
|
+
const parsedSecond = numbersOnly.length >= 6 ? parseInt(numbersOnly.slice(4, 6), 10) : 0;
|
|
7307
|
+
if (parsedHour >= 0 &&
|
|
7308
|
+
parsedHour <= 23 &&
|
|
7309
|
+
parsedMinute >= 0 &&
|
|
7310
|
+
parsedMinute <= 59 &&
|
|
7311
|
+
parsedSecond >= 0 &&
|
|
7312
|
+
parsedSecond <= 59) {
|
|
7313
|
+
setHour(parsedHour);
|
|
7314
|
+
setMinute(parsedMinute);
|
|
7315
|
+
if (setSecond)
|
|
7316
|
+
setSecond(parsedSecond);
|
|
7317
|
+
handleTimeChange(parsedHour, parsedMinute, parsedSecond, null);
|
|
7318
|
+
return;
|
|
7319
|
+
}
|
|
6579
7320
|
}
|
|
6580
7321
|
}
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
7322
|
+
else {
|
|
7323
|
+
// Parse 24-hour format first (e.g., "13:30", "14:00", "1330", "1400")
|
|
7324
|
+
const timePattern24Hour = /^(\d{1,2}):?(\d{2})$/;
|
|
7325
|
+
const match24Hour = trimmedValue.match(timePattern24Hour);
|
|
7326
|
+
if (match24Hour) {
|
|
7327
|
+
const parsedHour24 = parseInt(match24Hour[1], 10);
|
|
7328
|
+
const parsedMinute = parseInt(match24Hour[2], 10);
|
|
7329
|
+
if (parsedHour24 >= 0 &&
|
|
7330
|
+
parsedHour24 <= 23 &&
|
|
7331
|
+
parsedMinute >= 0 &&
|
|
7332
|
+
parsedMinute <= 59) {
|
|
7333
|
+
// Convert 24-hour to 12-hour format
|
|
7334
|
+
let hour12;
|
|
7335
|
+
let meridiem;
|
|
7336
|
+
if (parsedHour24 === 0) {
|
|
7337
|
+
hour12 = 12;
|
|
7338
|
+
meridiem = 'am';
|
|
7339
|
+
}
|
|
7340
|
+
else if (parsedHour24 === 12) {
|
|
7341
|
+
hour12 = 12;
|
|
7342
|
+
meridiem = 'pm';
|
|
7343
|
+
}
|
|
7344
|
+
else if (parsedHour24 > 12) {
|
|
7345
|
+
hour12 = parsedHour24 - 12;
|
|
7346
|
+
meridiem = 'pm';
|
|
7347
|
+
}
|
|
7348
|
+
else {
|
|
7349
|
+
hour12 = parsedHour24;
|
|
7350
|
+
meridiem = 'am';
|
|
7351
|
+
}
|
|
7352
|
+
setHour(hour12);
|
|
7353
|
+
setMinute(parsedMinute);
|
|
7354
|
+
if (setMeridiem)
|
|
7355
|
+
setMeridiem(meridiem);
|
|
7356
|
+
handleTimeChange(hour12, parsedMinute, null, meridiem);
|
|
7357
|
+
return;
|
|
7358
|
+
}
|
|
7359
|
+
}
|
|
7360
|
+
// Parse formats like "1:30 PM", "1:30PM", "1:30 pm", "1:30pm"
|
|
7361
|
+
const timePattern12Hour = /^(\d{1,2}):(\d{1,2})\s*(am|pm|AM|PM)$/i;
|
|
7362
|
+
const match12Hour = trimmedValue.match(timePattern12Hour);
|
|
7363
|
+
if (match12Hour) {
|
|
7364
|
+
const parsedHour = parseInt(match12Hour[1], 10);
|
|
7365
|
+
const parsedMinute = parseInt(match12Hour[2], 10);
|
|
7366
|
+
const parsedMeridiem = match12Hour[3].toLowerCase();
|
|
6591
7367
|
if (parsedHour >= 1 &&
|
|
6592
7368
|
parsedHour <= 12 &&
|
|
6593
7369
|
parsedMinute >= 0 &&
|
|
6594
7370
|
parsedMinute <= 59) {
|
|
6595
7371
|
setHour(parsedHour);
|
|
6596
7372
|
setMinute(parsedMinute);
|
|
6597
|
-
setMeridiem
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
7373
|
+
if (setMeridiem)
|
|
7374
|
+
setMeridiem(parsedMeridiem);
|
|
7375
|
+
handleTimeChange(parsedHour, parsedMinute, null, parsedMeridiem);
|
|
7376
|
+
return;
|
|
7377
|
+
}
|
|
7378
|
+
}
|
|
7379
|
+
// Parse formats like "12am" or "1pm" (hour only with meridiem, no minutes)
|
|
7380
|
+
const timePatternHourOnly = /^(\d{1,2})\s*(am|pm|AM|PM)$/i;
|
|
7381
|
+
const matchHourOnly = trimmedValue.match(timePatternHourOnly);
|
|
7382
|
+
if (matchHourOnly) {
|
|
7383
|
+
const parsedHour = parseInt(matchHourOnly[1], 10);
|
|
7384
|
+
const parsedMeridiem = matchHourOnly[2].toLowerCase();
|
|
7385
|
+
if (parsedHour >= 1 && parsedHour <= 12) {
|
|
7386
|
+
setHour(parsedHour);
|
|
7387
|
+
setMinute(0); // Default to 0 minutes when only hour is provided
|
|
7388
|
+
if (setMeridiem)
|
|
7389
|
+
setMeridiem(parsedMeridiem);
|
|
7390
|
+
handleTimeChange(parsedHour, 0, null, parsedMeridiem);
|
|
6603
7391
|
return;
|
|
6604
7392
|
}
|
|
6605
7393
|
}
|
|
7394
|
+
// Try to parse formats like "130pm" or "130 pm" (without colon, with minutes)
|
|
7395
|
+
const timePatternNoColon = /^(\d{1,4})\s*(am|pm|AM|PM)$/i;
|
|
7396
|
+
const matchNoColon = trimmedValue.match(timePatternNoColon);
|
|
7397
|
+
if (matchNoColon) {
|
|
7398
|
+
const numbersOnly = matchNoColon[1];
|
|
7399
|
+
const parsedMeridiem = matchNoColon[2].toLowerCase();
|
|
7400
|
+
if (numbersOnly.length >= 3) {
|
|
7401
|
+
const parsedHour = parseInt(numbersOnly.slice(0, -2), 10);
|
|
7402
|
+
const parsedMinute = parseInt(numbersOnly.slice(-2), 10);
|
|
7403
|
+
if (parsedHour >= 1 &&
|
|
7404
|
+
parsedHour <= 12 &&
|
|
7405
|
+
parsedMinute >= 0 &&
|
|
7406
|
+
parsedMinute <= 59) {
|
|
7407
|
+
setHour(parsedHour);
|
|
7408
|
+
setMinute(parsedMinute);
|
|
7409
|
+
if (setMeridiem)
|
|
7410
|
+
setMeridiem(parsedMeridiem);
|
|
7411
|
+
handleTimeChange(parsedHour, parsedMinute, null, parsedMeridiem);
|
|
7412
|
+
return;
|
|
7413
|
+
}
|
|
7414
|
+
}
|
|
7415
|
+
}
|
|
6606
7416
|
}
|
|
6607
7417
|
// Parse failed, select first result
|
|
6608
7418
|
selectFirstResult();
|
|
@@ -6613,58 +7423,87 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
|
|
|
6613
7423
|
const firstItem = collection.items[0];
|
|
6614
7424
|
setHour(firstItem.hour);
|
|
6615
7425
|
setMinute(firstItem.minute);
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
}
|
|
7426
|
+
filter('');
|
|
7427
|
+
if (is24Hour) {
|
|
7428
|
+
const opt24 = firstItem;
|
|
7429
|
+
if (setSecond)
|
|
7430
|
+
setSecond(opt24.second);
|
|
7431
|
+
handleTimeChange(opt24.hour, opt24.minute, opt24.second, null);
|
|
7432
|
+
}
|
|
7433
|
+
else {
|
|
7434
|
+
const opt12 = firstItem;
|
|
7435
|
+
if (setMeridiem)
|
|
7436
|
+
setMeridiem(opt12.meridiem);
|
|
7437
|
+
handleTimeChange(opt12.hour, opt12.minute, null, opt12.meridiem);
|
|
7438
|
+
}
|
|
6623
7439
|
}
|
|
6624
7440
|
};
|
|
6625
7441
|
const handleInputValueChange = (details) => {
|
|
6626
|
-
|
|
7442
|
+
if (is24Hour) {
|
|
7443
|
+
setInputValue(details.inputValue);
|
|
7444
|
+
}
|
|
6627
7445
|
filter(details.inputValue);
|
|
6628
7446
|
};
|
|
6629
7447
|
const handleFocus = (e) => {
|
|
6630
|
-
// Select all text when focusing
|
|
6631
7448
|
e.target.select();
|
|
6632
7449
|
};
|
|
6633
7450
|
const handleBlur = (e) => {
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
|
|
7451
|
+
const inputVal = e.target.value;
|
|
7452
|
+
if (is24Hour) {
|
|
7453
|
+
setInputValue(inputVal);
|
|
7454
|
+
}
|
|
7455
|
+
if (inputVal) {
|
|
7456
|
+
parseAndCommitInput(inputVal);
|
|
6638
7457
|
}
|
|
6639
7458
|
};
|
|
6640
7459
|
const handleKeyDown = (e) => {
|
|
6641
|
-
// Commit input on Enter key
|
|
6642
7460
|
if (e.key === 'Enter') {
|
|
6643
7461
|
e.preventDefault();
|
|
6644
|
-
const
|
|
6645
|
-
if (
|
|
6646
|
-
|
|
7462
|
+
const inputVal = e.currentTarget.value;
|
|
7463
|
+
if (is24Hour) {
|
|
7464
|
+
setInputValue(inputVal);
|
|
7465
|
+
}
|
|
7466
|
+
if (inputVal) {
|
|
7467
|
+
parseAndCommitInput(inputVal);
|
|
6647
7468
|
}
|
|
6648
|
-
// Blur the input
|
|
6649
7469
|
e.currentTarget?.blur();
|
|
6650
7470
|
}
|
|
6651
7471
|
};
|
|
6652
|
-
return (jsx(Flex, { direction: "column", gap: 3, children: jsxs(Flex, { alignItems: "center", gap: "2", width: "auto", minWidth: "300px", children: [jsxs(Combobox.Root, { collection: collection, value: currentValue ? [currentValue] : [], onValueChange: handleValueChange, onInputValueChange: handleInputValueChange, allowCustomValue: true, selectionBehavior: "replace",
|
|
7472
|
+
return (jsx(Flex, { direction: "column", gap: 3, children: jsxs(Flex, { alignItems: "center", gap: "2", width: "auto", minWidth: "300px", children: [jsxs(Combobox.Root, { collection: collection, value: currentValue ? [currentValue] : [], onValueChange: handleValueChange, onInputValueChange: handleInputValueChange, allowCustomValue: true, selectionBehavior: "replace", flex: 1, children: [jsxs(Combobox.Control, { children: [jsx(InputGroup$1, { startElement: jsx(BsClock, {}), children: jsx(Combobox.Input, { value: is24Hour ? inputValue : undefined, placeholder: labels?.placeholder ?? (is24Hour ? 'HH:mm:ss' : 'hh:mm AM/PM'), onFocus: handleFocus, onBlur: handleBlur, onKeyDown: handleKeyDown }) }), jsx(Combobox.IndicatorGroup, { children: jsx(Combobox.Trigger, {}) })] }), jsx(Portal, { disabled: !portalled, children: jsx(Combobox.Positioner, { children: jsxs(Combobox.Content, { children: [jsx(Combobox.Empty, { children: labels?.emptyMessage ?? 'No time found' }), collection.items.map((item) => (jsxs(Combobox.Item, { item: item, children: [jsxs(Flex, { alignItems: "center", gap: 2, width: "100%", children: [jsx(Text, { flex: 1, children: item.label }), item.durationText && (jsx(Tag$1.Root, { size: "sm", children: jsx(Tag$1.Label, { children: item.durationText }) }))] }), jsx(Combobox.ItemIndicator, {})] }, item.value)))] }) }) })] }), durationDiff && (jsx(Tag$1.Root, { size: "sm", children: jsx(Tag$1.Label, { children: durationDiff }) }))] }) }));
|
|
6653
7473
|
};
|
|
6654
7474
|
|
|
6655
7475
|
dayjs.extend(timezone);
|
|
6656
7476
|
const TimePicker = ({ column, schema, prefix }) => {
|
|
6657
7477
|
const { watch, formState: { errors }, setValue, } = useFormContext();
|
|
6658
7478
|
const { timezone, insideDialog, timePickerLabels } = useSchemaContext();
|
|
6659
|
-
const { required, gridColumn = 'span 12', gridRow = 'span 1', timeFormat = 'HH:mm:ssZ', displayTimeFormat = 'hh:mm A', } = schema;
|
|
7479
|
+
const { required, gridColumn = 'span 12', gridRow = 'span 1', timeFormat = 'HH:mm:ssZ', displayTimeFormat = 'hh:mm A', startTimeField, selectedDateField, } = schema;
|
|
6660
7480
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
6661
7481
|
const colLabel = `${prefix}${column}`;
|
|
6662
7482
|
const formI18n = useFormI18n(column, prefix, schema);
|
|
6663
7483
|
const [open, setOpen] = useState(false);
|
|
6664
7484
|
const value = watch(colLabel);
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
7485
|
+
// Watch startTime and selectedDate fields for offset calculation
|
|
7486
|
+
const startTimeValue = startTimeField
|
|
7487
|
+
? watch(`${prefix}${startTimeField}`)
|
|
7488
|
+
: undefined;
|
|
7489
|
+
const selectedDateValue = selectedDateField
|
|
7490
|
+
? watch(`${prefix}${selectedDateField}`)
|
|
7491
|
+
: undefined;
|
|
7492
|
+
// Convert to ISO string format for startTime if it's a date-time string
|
|
7493
|
+
const startTime = startTimeValue
|
|
7494
|
+
? dayjs(startTimeValue).tz(timezone).isValid()
|
|
7495
|
+
? dayjs(startTimeValue).tz(timezone).toISOString()
|
|
7496
|
+
: undefined
|
|
7497
|
+
: undefined;
|
|
7498
|
+
// Convert selectedDate to YYYY-MM-DD format
|
|
7499
|
+
const selectedDate = selectedDateValue
|
|
7500
|
+
? dayjs(selectedDateValue).tz(timezone).isValid()
|
|
7501
|
+
? dayjs(selectedDateValue).tz(timezone).format('YYYY-MM-DD')
|
|
7502
|
+
: undefined
|
|
7503
|
+
: undefined;
|
|
7504
|
+
const displayedTime = dayjs(`1970-01-01T${value}`).tz(timezone).isValid()
|
|
7505
|
+
? dayjs(`1970-01-01T${value}`).tz(timezone).format(displayTimeFormat)
|
|
7506
|
+
: '';
|
|
6668
7507
|
// Parse the initial time parts from the time string (HH:mm:ssZ)
|
|
6669
7508
|
const parseTime = (time) => {
|
|
6670
7509
|
if (!time)
|
|
@@ -6717,884 +7556,898 @@ const TimePicker = ({ column, schema, prefix }) => {
|
|
|
6717
7556
|
return (jsx(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
6718
7557
|
gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: jsxs(Popover.Root, { open: open, onOpenChange: (e) => setOpen(e.open), closeOnInteractOutside: true, children: [jsx(Popover.Trigger, { asChild: true, children: jsxs(Button, { size: "sm", variant: "outline", onClick: () => {
|
|
6719
7558
|
setOpen(true);
|
|
6720
|
-
}, justifyContent: 'start', children: [jsx(IoMdClock, {}), !!value ? `${displayedTime}` : ''] }) }), insideDialog ? (jsx(Popover.Positioner, { children: jsx(Popover.Content, { maxH: "70vh", overflowY: "auto", children: jsx(Popover.Body, { overflow: "visible", children: jsx(TimePicker$1, { hour: hour, setHour: setHour, minute: minute, setMinute: setMinute, meridiem: meridiem, setMeridiem: setMeridiem, onChange: handleTimeChange, labels: timePickerLabels }) }) }) })) : (jsx(Portal, { children: jsx(Popover.Positioner, { children: jsx(Popover.Content, { children: jsx(Popover.Body, { children: jsx(TimePicker$1, { hour: hour, setHour: setHour, minute: minute, setMinute: setMinute, meridiem: meridiem, setMeridiem: setMeridiem, onChange: handleTimeChange, labels: timePickerLabels }) }) }) }) }))] }) }));
|
|
7559
|
+
}, justifyContent: 'start', children: [jsx(IoMdClock, {}), !!value ? `${displayedTime}` : ''] }) }), insideDialog ? (jsx(Popover.Positioner, { children: jsx(Popover.Content, { maxH: "70vh", overflowY: "auto", children: jsx(Popover.Body, { overflow: "visible", children: jsx(TimePicker$1, { hour: hour, setHour: setHour, minute: minute, setMinute: setMinute, meridiem: meridiem, setMeridiem: setMeridiem, onChange: handleTimeChange, startTime: startTime, selectedDate: selectedDate, timezone: timezone, portalled: false, labels: timePickerLabels }) }) }) })) : (jsx(Portal, { children: jsx(Popover.Positioner, { children: jsx(Popover.Content, { children: jsx(Popover.Body, { children: jsx(TimePicker$1, { format: "12h", hour: hour, setHour: setHour, minute: minute, setMinute: setMinute, meridiem: meridiem, setMeridiem: setMeridiem, onChange: handleTimeChange, startTime: startTime, selectedDate: selectedDate, timezone: timezone, portalled: false, labels: timePickerLabels }) }) }) }) }))] }) }));
|
|
6721
7560
|
};
|
|
6722
7561
|
|
|
6723
7562
|
dayjs.extend(utc);
|
|
6724
7563
|
dayjs.extend(timezone);
|
|
6725
7564
|
dayjs.extend(customParseFormat);
|
|
6726
|
-
function
|
|
7565
|
+
function DateTimePicker$1({ value, onChange, format = 'date-time', showSeconds = false, labels = {
|
|
6727
7566
|
monthNamesShort: [
|
|
6728
|
-
'
|
|
6729
|
-
'
|
|
6730
|
-
'
|
|
6731
|
-
'
|
|
7567
|
+
'January',
|
|
7568
|
+
'February',
|
|
7569
|
+
'March',
|
|
7570
|
+
'April',
|
|
6732
7571
|
'May',
|
|
6733
|
-
'
|
|
6734
|
-
'
|
|
6735
|
-
'
|
|
6736
|
-
'
|
|
6737
|
-
'
|
|
6738
|
-
'
|
|
6739
|
-
'
|
|
7572
|
+
'June',
|
|
7573
|
+
'July',
|
|
7574
|
+
'August',
|
|
7575
|
+
'September',
|
|
7576
|
+
'October',
|
|
7577
|
+
'November',
|
|
7578
|
+
'December',
|
|
6740
7579
|
],
|
|
6741
7580
|
weekdayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
|
6742
7581
|
backButtonLabel: 'Back',
|
|
6743
|
-
forwardButtonLabel: '
|
|
6744
|
-
}, timezone = 'Asia/Hong_Kong', minDate, maxDate,
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
7582
|
+
forwardButtonLabel: 'Forward',
|
|
7583
|
+
}, timePickerLabels, timezone: tz = 'Asia/Hong_Kong', startTime, minDate, maxDate, portalled = false, defaultDate, defaultTime, showQuickActions = false, quickActionLabels = {
|
|
7584
|
+
yesterday: 'Yesterday',
|
|
7585
|
+
today: 'Today',
|
|
7586
|
+
tomorrow: 'Tomorrow',
|
|
7587
|
+
plus7Days: '+7 Days',
|
|
7588
|
+
}, showTimezoneSelector = false, timezoneOffset: controlledTimezoneOffset, onTimezoneOffsetChange, }) {
|
|
7589
|
+
const is24Hour = format === 'iso-date-time' || showSeconds;
|
|
7590
|
+
// Labels are used in calendarLabels useMemo
|
|
7591
|
+
// Parse value to get date and time
|
|
7592
|
+
const parsedValue = useMemo(() => {
|
|
7593
|
+
if (!value)
|
|
7594
|
+
return null;
|
|
7595
|
+
const dateObj = dayjs(value).tz(tz);
|
|
7596
|
+
if (!dateObj.isValid())
|
|
7597
|
+
return null;
|
|
7598
|
+
return dateObj;
|
|
7599
|
+
}, [value, tz]);
|
|
7600
|
+
// Initialize date state
|
|
7601
|
+
const [selectedDate, setSelectedDate] = useState(() => {
|
|
7602
|
+
if (parsedValue) {
|
|
7603
|
+
return parsedValue.toDate();
|
|
7604
|
+
}
|
|
7605
|
+
if (defaultDate) {
|
|
7606
|
+
const defaultDateObj = dayjs(defaultDate).tz(tz);
|
|
7607
|
+
return defaultDateObj.isValid() ? defaultDateObj.toDate() : new Date();
|
|
7608
|
+
}
|
|
7609
|
+
return new Date();
|
|
7610
|
+
});
|
|
7611
|
+
// Initialize time state
|
|
7612
|
+
const [hour, setHour] = useState(() => {
|
|
7613
|
+
if (parsedValue) {
|
|
7614
|
+
return parsedValue.hour();
|
|
6759
7615
|
}
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
const selectedDate = value
|
|
6763
|
-
? typeof value === 'string'
|
|
6764
|
-
? dayjs(value).tz(timezone).isValid()
|
|
6765
|
-
? dayjs(value).tz(timezone).toDate()
|
|
6766
|
-
: new Date()
|
|
6767
|
-
: value
|
|
6768
|
-
: new Date();
|
|
6769
|
-
// Shared function to parse and validate input value
|
|
6770
|
-
const parseAndValidateInput = (inputVal) => {
|
|
6771
|
-
// If empty, clear the value
|
|
6772
|
-
if (!inputVal.trim()) {
|
|
6773
|
-
onChange?.(undefined);
|
|
6774
|
-
setInputValue('');
|
|
6775
|
-
return;
|
|
7616
|
+
if (defaultTime?.hour !== null && defaultTime?.hour !== undefined) {
|
|
7617
|
+
return defaultTime.hour;
|
|
6776
7618
|
}
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
if (
|
|
6781
|
-
|
|
7619
|
+
return null;
|
|
7620
|
+
});
|
|
7621
|
+
const [minute, setMinute] = useState(() => {
|
|
7622
|
+
if (parsedValue) {
|
|
7623
|
+
return parsedValue.minute();
|
|
6782
7624
|
}
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
parsedDate = dayjs(inputVal, dateFormat, true);
|
|
7625
|
+
if (defaultTime?.minute !== null && defaultTime?.minute !== undefined) {
|
|
7626
|
+
return defaultTime.minute;
|
|
6786
7627
|
}
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
// Invalid: before minDate, reset to prop value
|
|
6793
|
-
resetToPropValue();
|
|
6794
|
-
return;
|
|
6795
|
-
}
|
|
6796
|
-
if (maxDate && dateObj > maxDate) {
|
|
6797
|
-
// Invalid: after maxDate, reset to prop value
|
|
6798
|
-
resetToPropValue();
|
|
6799
|
-
return;
|
|
6800
|
-
}
|
|
6801
|
-
// Valid date - format and update
|
|
6802
|
-
const formattedDate = parsedDate.tz(timezone).format(dateFormat);
|
|
6803
|
-
const formattedDisplay = parsedDate.tz(timezone).format(displayFormat);
|
|
6804
|
-
onChange?.(formattedDate);
|
|
6805
|
-
setInputValue(formattedDisplay);
|
|
7628
|
+
return null;
|
|
7629
|
+
});
|
|
7630
|
+
const [second, setSecond] = useState(() => {
|
|
7631
|
+
if (parsedValue) {
|
|
7632
|
+
return parsedValue.second();
|
|
6806
7633
|
}
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
resetToPropValue();
|
|
7634
|
+
if (defaultTime?.second !== null && defaultTime?.second !== undefined) {
|
|
7635
|
+
return defaultTime.second;
|
|
6810
7636
|
}
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
const
|
|
6814
|
-
if (
|
|
6815
|
-
const
|
|
6816
|
-
|
|
6817
|
-
? dayjs(value).tz(timezone).format(displayFormat)
|
|
6818
|
-
: ''
|
|
6819
|
-
: dayjs(value).tz(timezone).format(displayFormat);
|
|
6820
|
-
setInputValue(formatted);
|
|
7637
|
+
return showSeconds ? 0 : null;
|
|
7638
|
+
});
|
|
7639
|
+
const [meridiem, setMeridiem] = useState(() => {
|
|
7640
|
+
if (parsedValue) {
|
|
7641
|
+
const h = parsedValue.hour();
|
|
7642
|
+
return h < 12 ? 'am' : 'pm';
|
|
6821
7643
|
}
|
|
6822
|
-
|
|
6823
|
-
|
|
7644
|
+
if (defaultTime?.meridiem !== null && defaultTime?.meridiem !== undefined) {
|
|
7645
|
+
return defaultTime.meridiem;
|
|
6824
7646
|
}
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
const
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
7647
|
+
return is24Hour ? null : 'am';
|
|
7648
|
+
});
|
|
7649
|
+
// Popover state - separate for date, time, and timezone
|
|
7650
|
+
const [datePopoverOpen, setDatePopoverOpen] = useState(false);
|
|
7651
|
+
const [timePopoverOpen, setTimePopoverOpen] = useState(false);
|
|
7652
|
+
const [timezonePopoverOpen, setTimezonePopoverOpen] = useState(false);
|
|
7653
|
+
const [calendarPopoverOpen, setCalendarPopoverOpen] = useState(false);
|
|
7654
|
+
// Timezone offset state (controlled or uncontrolled)
|
|
7655
|
+
const [internalTimezoneOffset, setInternalTimezoneOffset] = useState(() => {
|
|
7656
|
+
if (controlledTimezoneOffset !== undefined) {
|
|
7657
|
+
return controlledTimezoneOffset;
|
|
7658
|
+
}
|
|
7659
|
+
if (parsedValue) {
|
|
7660
|
+
return parsedValue.format('Z');
|
|
7661
|
+
}
|
|
7662
|
+
// Default to +08:00
|
|
7663
|
+
return '+08:00';
|
|
7664
|
+
});
|
|
7665
|
+
// Use controlled prop if provided, otherwise use internal state
|
|
7666
|
+
const timezoneOffset = controlledTimezoneOffset ?? internalTimezoneOffset;
|
|
7667
|
+
// Update internal state when controlled prop changes
|
|
7668
|
+
useEffect(() => {
|
|
7669
|
+
if (controlledTimezoneOffset !== undefined) {
|
|
7670
|
+
setInternalTimezoneOffset(controlledTimezoneOffset);
|
|
6839
7671
|
}
|
|
6840
|
-
};
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
return (jsxs(Popover.Root, { open: open, onOpenChange: (e) => setOpen(e.open), closeOnInteractOutside: true, autoFocus: false, children: [jsx(InputGroup, { endElement: jsx(Popover.Trigger, { asChild: true, children: jsx(IconButton, { variant: "ghost", size: "2xs", "aria-label": "Open calendar", onClick: () => setOpen(true), children: jsx(Icon, { children: jsx(MdDateRange, {}) }) }) }), children: jsx(Input, { value: inputValue, onChange: handleInputChange, onBlur: handleInputBlur, onKeyDown: handleKeyDown, placeholder: placeholder, readOnly: readOnly }) }), insideDialog ? (jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", minH: "25rem", children: jsx(Popover.Body, { children: datePickerContent }) }) })) : (jsx(Portal, { children: jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", minH: "25rem", children: jsx(Popover.Body, { children: datePickerContent }) }) }) }))] }));
|
|
6848
|
-
}
|
|
6849
|
-
|
|
6850
|
-
dayjs.extend(utc);
|
|
6851
|
-
dayjs.extend(timezone);
|
|
6852
|
-
function IsoTimePicker({ hour, setHour, minute, setMinute, second, setSecond,
|
|
6853
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
6854
|
-
onChange = (_newValue) => { }, startTime, selectedDate, timezone = 'Asia/Hong_Kong', portalled = true, labels = {
|
|
6855
|
-
placeholder: 'HH:mm:ss',
|
|
6856
|
-
emptyMessage: 'No time found',
|
|
6857
|
-
}, }) {
|
|
6858
|
-
// Generate time options (every 15 minutes, seconds always 0)
|
|
6859
|
-
const timeOptions = useMemo(() => {
|
|
6860
|
-
const options = [];
|
|
6861
|
-
// Get start time for comparison if provided
|
|
6862
|
-
let startDateTime = null;
|
|
6863
|
-
let shouldFilterByDate = false;
|
|
6864
|
-
if (startTime && selectedDate) {
|
|
6865
|
-
const startDateObj = dayjs(startTime).tz(timezone);
|
|
6866
|
-
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6867
|
-
if (startDateObj.isValid() && selectedDateObj.isValid()) {
|
|
6868
|
-
startDateTime = startDateObj;
|
|
6869
|
-
// Only filter if dates are the same
|
|
6870
|
-
shouldFilterByDate =
|
|
6871
|
-
startDateObj.format('YYYY-MM-DD') ===
|
|
6872
|
-
selectedDateObj.format('YYYY-MM-DD');
|
|
7672
|
+
}, [controlledTimezoneOffset]);
|
|
7673
|
+
// Sync timezone offset when value changes (only if uncontrolled)
|
|
7674
|
+
useEffect(() => {
|
|
7675
|
+
if (controlledTimezoneOffset === undefined && parsedValue) {
|
|
7676
|
+
const offsetFromValue = parsedValue.format('Z');
|
|
7677
|
+
if (offsetFromValue !== timezoneOffset) {
|
|
7678
|
+
setInternalTimezoneOffset(offsetFromValue);
|
|
6873
7679
|
}
|
|
6874
7680
|
}
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
.second(0)
|
|
6887
|
-
.millisecond(0);
|
|
6888
|
-
if (optionDateTime.isBefore(startDateTime)) {
|
|
6889
|
-
continue; // Skip this option as it would result in negative duration
|
|
6890
|
-
}
|
|
6891
|
-
}
|
|
6892
|
-
// Calculate duration if startTime is provided
|
|
6893
|
-
let durationText;
|
|
6894
|
-
if (startDateTime && selectedDate) {
|
|
6895
|
-
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6896
|
-
const optionDateTime = selectedDateObj
|
|
6897
|
-
.hour(h)
|
|
6898
|
-
.minute(m)
|
|
6899
|
-
.second(0)
|
|
6900
|
-
.millisecond(0);
|
|
6901
|
-
if (optionDateTime.isValid() &&
|
|
6902
|
-
optionDateTime.isAfter(startDateTime)) {
|
|
6903
|
-
const diffMs = optionDateTime.diff(startDateTime);
|
|
6904
|
-
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
6905
|
-
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
6906
|
-
const diffSeconds = Math.floor((diffMs % (1000 * 60)) / 1000);
|
|
6907
|
-
if (diffHours > 0 || diffMinutes > 0 || diffSeconds > 0) {
|
|
6908
|
-
let diffText = '';
|
|
6909
|
-
if (diffHours > 0) {
|
|
6910
|
-
diffText = `${diffHours}h ${diffMinutes}m`;
|
|
6911
|
-
}
|
|
6912
|
-
else if (diffMinutes > 0) {
|
|
6913
|
-
diffText = `${diffMinutes}m ${diffSeconds}s`;
|
|
6914
|
-
}
|
|
6915
|
-
else {
|
|
6916
|
-
diffText = `${diffSeconds}s`;
|
|
6917
|
-
}
|
|
6918
|
-
durationText = `+${diffText}`;
|
|
6919
|
-
}
|
|
6920
|
-
}
|
|
6921
|
-
}
|
|
6922
|
-
options.push({
|
|
6923
|
-
label: timeDisplay,
|
|
6924
|
-
value: `${h}:${m}:0`,
|
|
6925
|
-
hour: h,
|
|
6926
|
-
minute: m,
|
|
6927
|
-
second: 0,
|
|
6928
|
-
searchText: timeDisplay, // Use base time without duration for searching
|
|
6929
|
-
durationText,
|
|
6930
|
-
});
|
|
6931
|
-
}
|
|
7681
|
+
}, [parsedValue, controlledTimezoneOffset, timezoneOffset]);
|
|
7682
|
+
// Sync timezone offset when value changes
|
|
7683
|
+
// Generate timezone offset options (UTC-12 to UTC+14)
|
|
7684
|
+
const timezoneOffsetOptions = useMemo(() => {
|
|
7685
|
+
const options = [];
|
|
7686
|
+
for (let offset = -12; offset <= 14; offset++) {
|
|
7687
|
+
const sign = offset >= 0 ? '+' : '-';
|
|
7688
|
+
const hours = Math.abs(offset).toString().padStart(2, '0');
|
|
7689
|
+
const value = `${sign}${hours}:00`;
|
|
7690
|
+
const label = `UTC${sign}${hours}:00`;
|
|
7691
|
+
options.push({ value, label });
|
|
6932
7692
|
}
|
|
6933
7693
|
return options;
|
|
6934
|
-
}, [
|
|
6935
|
-
|
|
6936
|
-
const { collection
|
|
6937
|
-
initialItems:
|
|
6938
|
-
itemToString: (item) => item.
|
|
7694
|
+
}, []);
|
|
7695
|
+
// Create collection for Select
|
|
7696
|
+
const { collection: timezoneCollection } = useListCollection({
|
|
7697
|
+
initialItems: timezoneOffsetOptions,
|
|
7698
|
+
itemToString: (item) => item.label,
|
|
6939
7699
|
itemToValue: (item) => item.value,
|
|
6940
|
-
filter: contains,
|
|
6941
7700
|
});
|
|
6942
|
-
//
|
|
6943
|
-
const
|
|
6944
|
-
if (
|
|
6945
|
-
return
|
|
6946
|
-
|
|
6947
|
-
return
|
|
6948
|
-
}, [
|
|
6949
|
-
//
|
|
6950
|
-
const
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
return null;
|
|
6957
|
-
}
|
|
6958
|
-
const startDateObj = dayjs(startTime).tz(timezone);
|
|
6959
|
-
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6960
|
-
const currentDateTime = selectedDateObj
|
|
6961
|
-
.hour(hour)
|
|
6962
|
-
.minute(minute)
|
|
6963
|
-
.second(second ?? 0)
|
|
6964
|
-
.millisecond(0);
|
|
6965
|
-
if (!startDateObj.isValid() || !currentDateTime.isValid()) {
|
|
6966
|
-
return null;
|
|
6967
|
-
}
|
|
6968
|
-
const diffMs = currentDateTime.diff(startDateObj);
|
|
6969
|
-
if (diffMs < 0) {
|
|
6970
|
-
return null;
|
|
7701
|
+
// Ensure timezoneOffset value is valid (exists in collection)
|
|
7702
|
+
const validTimezoneOffset = useMemo(() => {
|
|
7703
|
+
if (!timezoneOffset)
|
|
7704
|
+
return undefined;
|
|
7705
|
+
const exists = timezoneOffsetOptions.some((opt) => opt.value === timezoneOffset);
|
|
7706
|
+
return exists ? timezoneOffset : undefined;
|
|
7707
|
+
}, [timezoneOffset, timezoneOffsetOptions]);
|
|
7708
|
+
// Date input state
|
|
7709
|
+
const [dateInputValue, setDateInputValue] = useState('');
|
|
7710
|
+
// Sync date input value with selected date
|
|
7711
|
+
useEffect(() => {
|
|
7712
|
+
if (selectedDate) {
|
|
7713
|
+
const formatted = dayjs(selectedDate).tz(tz).format('YYYY-MM-DD');
|
|
7714
|
+
setDateInputValue(formatted);
|
|
6971
7715
|
}
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
const diffSeconds = Math.floor((diffMs % (1000 * 60)) / 1000);
|
|
6975
|
-
if (diffHours > 0 || diffMinutes > 0 || diffSeconds > 0) {
|
|
6976
|
-
let diffText = '';
|
|
6977
|
-
if (diffHours > 0) {
|
|
6978
|
-
diffText = `${diffHours}h ${diffMinutes}m`;
|
|
6979
|
-
}
|
|
6980
|
-
else if (diffMinutes > 0) {
|
|
6981
|
-
diffText = `${diffMinutes}m ${diffSeconds}s`;
|
|
6982
|
-
}
|
|
6983
|
-
else {
|
|
6984
|
-
diffText = `${diffSeconds}s`;
|
|
6985
|
-
}
|
|
6986
|
-
return `+${diffText}`;
|
|
7716
|
+
else {
|
|
7717
|
+
setDateInputValue('');
|
|
6987
7718
|
}
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
const
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
onChange({ hour: null, minute: null, second: null });
|
|
6996
|
-
};
|
|
6997
|
-
const handleValueChange = (details) => {
|
|
6998
|
-
if (details.value.length === 0) {
|
|
6999
|
-
handleClear();
|
|
7719
|
+
}, [selectedDate, tz]);
|
|
7720
|
+
// Parse and validate date input
|
|
7721
|
+
const parseAndValidateDateInput = (inputVal) => {
|
|
7722
|
+
// If empty, clear the value
|
|
7723
|
+
if (!inputVal.trim()) {
|
|
7724
|
+
setSelectedDate(null);
|
|
7725
|
+
updateDateTime(null, hour, minute, second, meridiem);
|
|
7000
7726
|
return;
|
|
7001
7727
|
}
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
setSecond(selectedOption.second);
|
|
7008
|
-
filter(''); // Reset filter after selection
|
|
7009
|
-
onChange({
|
|
7010
|
-
hour: selectedOption.hour,
|
|
7011
|
-
minute: selectedOption.minute,
|
|
7012
|
-
second: selectedOption.second,
|
|
7013
|
-
});
|
|
7014
|
-
}
|
|
7015
|
-
};
|
|
7016
|
-
// Parse input value and update state
|
|
7017
|
-
const parseAndCommitInput = (value) => {
|
|
7018
|
-
const trimmedValue = value.trim();
|
|
7019
|
-
// Filter the collection based on input
|
|
7020
|
-
filter(trimmedValue);
|
|
7021
|
-
if (!trimmedValue) {
|
|
7022
|
-
return;
|
|
7728
|
+
// Try parsing with common date formats
|
|
7729
|
+
let parsedDate = dayjs(inputVal, 'YYYY-MM-DD', true);
|
|
7730
|
+
// If that fails, try other common formats
|
|
7731
|
+
if (!parsedDate.isValid()) {
|
|
7732
|
+
parsedDate = dayjs(inputVal);
|
|
7023
7733
|
}
|
|
7024
|
-
//
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
parsedSecond <= 59) {
|
|
7038
|
-
setHour(parsedHour);
|
|
7039
|
-
setMinute(parsedMinute);
|
|
7040
|
-
setSecond(parsedSecond);
|
|
7041
|
-
onChange({
|
|
7042
|
-
hour: parsedHour,
|
|
7043
|
-
minute: parsedMinute,
|
|
7044
|
-
second: parsedSecond,
|
|
7045
|
-
});
|
|
7734
|
+
// If valid, check constraints and update
|
|
7735
|
+
if (parsedDate.isValid()) {
|
|
7736
|
+
const dateObj = parsedDate.tz(tz).toDate();
|
|
7737
|
+
// Check min/max constraints
|
|
7738
|
+
if (minDate && dateObj < minDate) {
|
|
7739
|
+
// Invalid: before minDate, reset to current selected date
|
|
7740
|
+
if (selectedDate) {
|
|
7741
|
+
const formatted = dayjs(selectedDate).tz(tz).format('YYYY-MM-DD');
|
|
7742
|
+
setDateInputValue(formatted);
|
|
7743
|
+
}
|
|
7744
|
+
else {
|
|
7745
|
+
setDateInputValue('');
|
|
7746
|
+
}
|
|
7046
7747
|
return;
|
|
7047
7748
|
}
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
// Validate ranges
|
|
7057
|
-
if (parsedHour >= 0 &&
|
|
7058
|
-
parsedHour <= 23 &&
|
|
7059
|
-
parsedMinute >= 0 &&
|
|
7060
|
-
parsedMinute <= 59 &&
|
|
7061
|
-
parsedSecond >= 0 &&
|
|
7062
|
-
parsedSecond <= 59) {
|
|
7063
|
-
setHour(parsedHour);
|
|
7064
|
-
setMinute(parsedMinute);
|
|
7065
|
-
setSecond(parsedSecond);
|
|
7066
|
-
onChange({
|
|
7067
|
-
hour: parsedHour,
|
|
7068
|
-
minute: parsedMinute,
|
|
7069
|
-
second: parsedSecond,
|
|
7070
|
-
});
|
|
7071
|
-
return;
|
|
7749
|
+
if (maxDate && dateObj > maxDate) {
|
|
7750
|
+
// Invalid: after maxDate, reset to current selected date
|
|
7751
|
+
if (selectedDate) {
|
|
7752
|
+
const formatted = dayjs(selectedDate).tz(tz).format('YYYY-MM-DD');
|
|
7753
|
+
setDateInputValue(formatted);
|
|
7754
|
+
}
|
|
7755
|
+
else {
|
|
7756
|
+
setDateInputValue('');
|
|
7072
7757
|
}
|
|
7758
|
+
return;
|
|
7073
7759
|
}
|
|
7760
|
+
// Valid date - update selected date
|
|
7761
|
+
setSelectedDate(dateObj);
|
|
7762
|
+
updateDateTime(dateObj, hour, minute, second, meridiem);
|
|
7763
|
+
// Format and update input value
|
|
7764
|
+
const formatted = parsedDate.tz(tz).format('YYYY-MM-DD');
|
|
7765
|
+
setDateInputValue(formatted);
|
|
7074
7766
|
}
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
setSecond(firstItem.second);
|
|
7085
|
-
filter(''); // Reset filter after selection
|
|
7086
|
-
onChange({
|
|
7087
|
-
hour: firstItem.hour,
|
|
7088
|
-
minute: firstItem.minute,
|
|
7089
|
-
second: firstItem.second,
|
|
7090
|
-
});
|
|
7767
|
+
else {
|
|
7768
|
+
// Invalid date - reset to current selected date
|
|
7769
|
+
if (selectedDate) {
|
|
7770
|
+
const formatted = dayjs(selectedDate).tz(tz).format('YYYY-MM-DD');
|
|
7771
|
+
setDateInputValue(formatted);
|
|
7772
|
+
}
|
|
7773
|
+
else {
|
|
7774
|
+
setDateInputValue('');
|
|
7775
|
+
}
|
|
7091
7776
|
}
|
|
7092
7777
|
};
|
|
7093
|
-
const
|
|
7094
|
-
|
|
7095
|
-
filter(details.inputValue);
|
|
7778
|
+
const handleDateInputChange = (e) => {
|
|
7779
|
+
setDateInputValue(e.target.value);
|
|
7096
7780
|
};
|
|
7097
|
-
const
|
|
7098
|
-
|
|
7099
|
-
e.target.select();
|
|
7781
|
+
const handleDateInputBlur = () => {
|
|
7782
|
+
parseAndValidateDateInput(dateInputValue);
|
|
7100
7783
|
};
|
|
7101
|
-
const
|
|
7102
|
-
// Parse and commit the input value when losing focus
|
|
7103
|
-
const inputValue = e.target.value;
|
|
7104
|
-
if (inputValue) {
|
|
7105
|
-
parseAndCommitInput(inputValue);
|
|
7106
|
-
}
|
|
7107
|
-
};
|
|
7108
|
-
const handleKeyDown = (e) => {
|
|
7109
|
-
// Commit input on Enter key
|
|
7784
|
+
const handleDateInputKeyDown = (e) => {
|
|
7110
7785
|
if (e.key === 'Enter') {
|
|
7111
7786
|
e.preventDefault();
|
|
7112
|
-
|
|
7113
|
-
if (inputValue) {
|
|
7114
|
-
parseAndCommitInput(inputValue);
|
|
7115
|
-
}
|
|
7116
|
-
// Blur the input
|
|
7117
|
-
e.currentTarget?.blur();
|
|
7787
|
+
parseAndValidateDateInput(dateInputValue);
|
|
7118
7788
|
}
|
|
7119
7789
|
};
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
dayjs.
|
|
7124
|
-
dayjs.
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
'Jun',
|
|
7133
|
-
'Jul',
|
|
7134
|
-
'Aug',
|
|
7135
|
-
'Sep',
|
|
7136
|
-
'Oct',
|
|
7137
|
-
'Nov',
|
|
7138
|
-
'Dec',
|
|
7139
|
-
],
|
|
7140
|
-
weekdayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
|
7141
|
-
backButtonLabel: 'Back',
|
|
7142
|
-
forwardButtonLabel: 'Next',
|
|
7143
|
-
}, timePickerLabels, timezone = 'Asia/Hong_Kong', startTime, minDate, maxDate, portalled = false, }) {
|
|
7144
|
-
console.log('[DateTimePicker] Component initialized with props:', {
|
|
7145
|
-
value,
|
|
7146
|
-
format,
|
|
7147
|
-
showSeconds,
|
|
7148
|
-
timezone,
|
|
7149
|
-
startTime,
|
|
7150
|
-
minDate,
|
|
7151
|
-
maxDate,
|
|
7152
|
-
});
|
|
7153
|
-
// Initialize selectedDate from value prop, converting ISO to YYYY-MM-DD format
|
|
7154
|
-
const getDateString = useCallback((val) => {
|
|
7155
|
-
if (!val)
|
|
7156
|
-
return '';
|
|
7157
|
-
const dateObj = dayjs(val).tz(timezone);
|
|
7158
|
-
return dateObj.isValid() ? dateObj.format('YYYY-MM-DD') : '';
|
|
7159
|
-
}, [timezone]);
|
|
7160
|
-
const [selectedDate, setSelectedDate] = useState(getDateString(value));
|
|
7161
|
-
// Helper to get time values from value prop with timezone
|
|
7162
|
-
const getTimeFromValue = useCallback((val) => {
|
|
7163
|
-
console.log('[DateTimePicker] getTimeFromValue called:', {
|
|
7164
|
-
val,
|
|
7165
|
-
timezone,
|
|
7166
|
-
showSeconds,
|
|
7167
|
-
});
|
|
7168
|
-
if (!val) {
|
|
7169
|
-
console.log('[DateTimePicker] No value provided, returning nulls');
|
|
7170
|
-
return {
|
|
7171
|
-
hour12: null,
|
|
7172
|
-
minute: null,
|
|
7173
|
-
meridiem: null,
|
|
7174
|
-
hour24: null,
|
|
7175
|
-
second: null,
|
|
7176
|
-
};
|
|
7177
|
-
}
|
|
7178
|
-
const dateObj = dayjs(val).tz(timezone);
|
|
7179
|
-
console.log('[DateTimePicker] Parsed date object:', {
|
|
7180
|
-
original: val,
|
|
7181
|
-
timezone,
|
|
7182
|
-
isValid: dateObj.isValid(),
|
|
7183
|
-
formatted: dateObj.format('YYYY-MM-DD HH:mm:ss Z'),
|
|
7184
|
-
hour24: dateObj.hour(),
|
|
7185
|
-
minute: dateObj.minute(),
|
|
7186
|
-
second: dateObj.second(),
|
|
7187
|
-
});
|
|
7188
|
-
if (!dateObj.isValid()) {
|
|
7189
|
-
console.log('[DateTimePicker] Invalid date object, returning nulls');
|
|
7190
|
-
return {
|
|
7191
|
-
hour12: null,
|
|
7192
|
-
minute: null,
|
|
7193
|
-
meridiem: null,
|
|
7194
|
-
hour24: null,
|
|
7195
|
-
second: null,
|
|
7196
|
-
};
|
|
7197
|
-
}
|
|
7198
|
-
const hour24Value = dateObj.hour();
|
|
7199
|
-
const hour12Value = hour24Value % 12 || 12;
|
|
7200
|
-
const minuteValue = dateObj.minute();
|
|
7201
|
-
const meridiemValue = hour24Value >= 12 ? 'pm' : 'am';
|
|
7202
|
-
const secondValue = showSeconds ? dateObj.second() : null;
|
|
7203
|
-
const result = {
|
|
7204
|
-
hour12: hour12Value,
|
|
7205
|
-
minute: minuteValue,
|
|
7206
|
-
meridiem: meridiemValue,
|
|
7207
|
-
hour24: hour24Value,
|
|
7208
|
-
second: secondValue,
|
|
7209
|
-
};
|
|
7210
|
-
console.log('[DateTimePicker] Extracted time values:', result);
|
|
7211
|
-
return result;
|
|
7212
|
-
}, [timezone, showSeconds]);
|
|
7213
|
-
const initialTime = getTimeFromValue(value);
|
|
7214
|
-
console.log('[DateTimePicker] Initial time from value:', {
|
|
7215
|
-
value,
|
|
7216
|
-
initialTime,
|
|
7217
|
-
});
|
|
7218
|
-
// Time state for 12-hour format
|
|
7219
|
-
const [hour12, setHour12] = useState(initialTime.hour12);
|
|
7220
|
-
const [minute, setMinute] = useState(initialTime.minute);
|
|
7221
|
-
const [meridiem, setMeridiem] = useState(initialTime.meridiem);
|
|
7222
|
-
// Time state for 24-hour format
|
|
7223
|
-
const [hour24, setHour24] = useState(initialTime.hour24);
|
|
7224
|
-
const [second, setSecond] = useState(initialTime.second);
|
|
7225
|
-
// Sync selectedDate and time states when value prop changes
|
|
7226
|
-
useEffect(() => {
|
|
7227
|
-
console.log('[DateTimePicker] useEffect triggered - value changed:', {
|
|
7228
|
-
value,
|
|
7229
|
-
timezone,
|
|
7230
|
-
format,
|
|
7231
|
-
});
|
|
7232
|
-
// If value is null, undefined, or invalid, clear all fields
|
|
7233
|
-
if (!value || value === null || value === undefined) {
|
|
7234
|
-
console.log('[DateTimePicker] Value is null/undefined, clearing all fields');
|
|
7235
|
-
setSelectedDate('');
|
|
7236
|
-
setHour12(null);
|
|
7237
|
-
setMinute(null);
|
|
7238
|
-
setMeridiem(null);
|
|
7239
|
-
setHour24(null);
|
|
7240
|
-
setSecond(null);
|
|
7241
|
-
return;
|
|
7242
|
-
}
|
|
7243
|
-
// Check if value is valid
|
|
7244
|
-
const dateObj = dayjs(value).tz(timezone);
|
|
7245
|
-
if (!dateObj.isValid()) {
|
|
7246
|
-
console.log('[DateTimePicker] Invalid value, clearing all fields');
|
|
7247
|
-
setSelectedDate('');
|
|
7248
|
-
setHour12(null);
|
|
7249
|
-
setMinute(null);
|
|
7250
|
-
setMeridiem(null);
|
|
7251
|
-
setHour24(null);
|
|
7252
|
-
setSecond(null);
|
|
7253
|
-
return;
|
|
7790
|
+
// Helper functions to get dates in the correct timezone
|
|
7791
|
+
const getToday = () => dayjs().tz(tz).startOf('day').toDate();
|
|
7792
|
+
const getYesterday = () => dayjs().tz(tz).subtract(1, 'day').startOf('day').toDate();
|
|
7793
|
+
const getTomorrow = () => dayjs().tz(tz).add(1, 'day').startOf('day').toDate();
|
|
7794
|
+
const getPlus7Days = () => dayjs().tz(tz).add(7, 'day').startOf('day').toDate();
|
|
7795
|
+
// Check if a date is within min/max constraints
|
|
7796
|
+
const isDateValid = (date) => {
|
|
7797
|
+
if (minDate) {
|
|
7798
|
+
const minDateStart = dayjs(minDate).tz(tz).startOf('day').toDate();
|
|
7799
|
+
const dateStart = dayjs(date).tz(tz).startOf('day').toDate();
|
|
7800
|
+
if (dateStart < minDateStart)
|
|
7801
|
+
return false;
|
|
7254
7802
|
}
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
timeData,
|
|
7261
|
-
});
|
|
7262
|
-
setHour12(timeData.hour12);
|
|
7263
|
-
setMinute(timeData.minute);
|
|
7264
|
-
setMeridiem(timeData.meridiem);
|
|
7265
|
-
setHour24(timeData.hour24);
|
|
7266
|
-
setSecond(timeData.second);
|
|
7267
|
-
}, [value, getTimeFromValue, getDateString, timezone]);
|
|
7268
|
-
const handleDateChange = (date) => {
|
|
7269
|
-
console.log('[DateTimePicker] handleDateChange called:', {
|
|
7270
|
-
date,
|
|
7271
|
-
timezone,
|
|
7272
|
-
showSeconds,
|
|
7273
|
-
currentTimeStates: { hour12, minute, meridiem, hour24, second },
|
|
7274
|
-
});
|
|
7275
|
-
// If date is empty or invalid, clear all fields
|
|
7276
|
-
if (!date || date === '') {
|
|
7277
|
-
console.log('[DateTimePicker] Empty date, clearing all fields');
|
|
7278
|
-
setSelectedDate('');
|
|
7279
|
-
setHour12(null);
|
|
7280
|
-
setMinute(null);
|
|
7281
|
-
setMeridiem(null);
|
|
7282
|
-
setHour24(null);
|
|
7283
|
-
setSecond(null);
|
|
7284
|
-
onChange?.(undefined);
|
|
7285
|
-
return;
|
|
7803
|
+
if (maxDate) {
|
|
7804
|
+
const maxDateStart = dayjs(maxDate).tz(tz).startOf('day').toDate();
|
|
7805
|
+
const dateStart = dayjs(date).tz(tz).startOf('day').toDate();
|
|
7806
|
+
if (dateStart > maxDateStart)
|
|
7807
|
+
return false;
|
|
7286
7808
|
}
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
});
|
|
7297
|
-
if (!dateObj.isValid()) {
|
|
7298
|
-
console.warn('[DateTimePicker] Invalid date object in handleDateChange, clearing fields');
|
|
7299
|
-
setSelectedDate('');
|
|
7300
|
-
setHour12(null);
|
|
7301
|
-
setMinute(null);
|
|
7302
|
-
setMeridiem(null);
|
|
7303
|
-
setHour24(null);
|
|
7304
|
-
setSecond(null);
|
|
7305
|
-
onChange?.(undefined);
|
|
7306
|
-
return;
|
|
7809
|
+
return true;
|
|
7810
|
+
};
|
|
7811
|
+
// Handle quick action button clicks
|
|
7812
|
+
const handleQuickActionClick = (date) => {
|
|
7813
|
+
if (isDateValid(date)) {
|
|
7814
|
+
setSelectedDate(date);
|
|
7815
|
+
updateDateTime(date, hour, minute, second, meridiem);
|
|
7816
|
+
// Close the calendar popover if open
|
|
7817
|
+
setCalendarPopoverOpen(false);
|
|
7307
7818
|
}
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
|
|
7819
|
+
};
|
|
7820
|
+
// Display text for buttons
|
|
7821
|
+
const dateDisplayText = useMemo(() => {
|
|
7822
|
+
if (!selectedDate)
|
|
7823
|
+
return 'Select date';
|
|
7824
|
+
return dayjs(selectedDate).tz(tz).format('YYYY-MM-DD');
|
|
7825
|
+
}, [selectedDate, tz]);
|
|
7826
|
+
const timeDisplayText = useMemo(() => {
|
|
7827
|
+
if (hour === null || minute === null)
|
|
7828
|
+
return 'Select time';
|
|
7829
|
+
if (is24Hour) {
|
|
7830
|
+
// 24-hour format: never show meridiem, always use 24-hour format (0-23)
|
|
7831
|
+
const hour24 = hour >= 0 && hour <= 23 ? hour : hour % 24;
|
|
7832
|
+
const s = second ?? 0;
|
|
7833
|
+
if (showSeconds) {
|
|
7834
|
+
return `${hour24.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
7835
|
+
}
|
|
7836
|
+
return `${hour24.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
|
|
7313
7837
|
}
|
|
7314
7838
|
else {
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
7839
|
+
// 12-hour format: always show meridiem (AM/PM)
|
|
7840
|
+
const hour12 = hour >= 1 && hour <= 12 ? hour : hour % 12;
|
|
7841
|
+
if (meridiem === null)
|
|
7842
|
+
return 'Select time';
|
|
7843
|
+
const hourDisplay = hour12.toString();
|
|
7844
|
+
const minuteDisplay = minute.toString().padStart(2, '0');
|
|
7845
|
+
return `${hourDisplay}:${minuteDisplay} ${meridiem.toUpperCase()}`;
|
|
7846
|
+
}
|
|
7847
|
+
}, [hour, minute, second, meridiem, is24Hour, showSeconds]);
|
|
7848
|
+
const timezoneDisplayText = useMemo(() => {
|
|
7849
|
+
if (!showTimezoneSelector)
|
|
7850
|
+
return '';
|
|
7851
|
+
// Show offset as is (e.g., "+08:00")
|
|
7852
|
+
return timezoneOffset;
|
|
7853
|
+
}, [timezoneOffset, showTimezoneSelector]);
|
|
7854
|
+
// Update selectedDate when value changes externally
|
|
7855
|
+
useEffect(() => {
|
|
7856
|
+
if (parsedValue) {
|
|
7857
|
+
setSelectedDate(parsedValue.toDate());
|
|
7858
|
+
setHour(parsedValue.hour());
|
|
7859
|
+
setMinute(parsedValue.minute());
|
|
7860
|
+
setSecond(parsedValue.second());
|
|
7861
|
+
if (!is24Hour) {
|
|
7862
|
+
const h = parsedValue.hour();
|
|
7863
|
+
setMeridiem(h < 12 ? 'am' : 'pm');
|
|
7864
|
+
}
|
|
7318
7865
|
}
|
|
7319
|
-
};
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
if (
|
|
7333
|
-
|
|
7866
|
+
}, [parsedValue, is24Hour]);
|
|
7867
|
+
// Combine date and time and call onChange
|
|
7868
|
+
const updateDateTime = (newDate, newHour, newMinute, newSecond, newMeridiem, timezoneOffsetOverride) => {
|
|
7869
|
+
if (!newDate || newHour === null || newMinute === null) {
|
|
7870
|
+
onChange?.(undefined);
|
|
7871
|
+
return;
|
|
7872
|
+
}
|
|
7873
|
+
// Convert 12-hour to 24-hour if needed
|
|
7874
|
+
let hour24 = newHour;
|
|
7875
|
+
if (!is24Hour && newMeridiem) {
|
|
7876
|
+
// In 12-hour format, hour should be 1-12
|
|
7877
|
+
// If hour is > 12, it might already be in 24-hour format, convert it first
|
|
7878
|
+
let hour12 = newHour;
|
|
7879
|
+
if (newHour > 12) {
|
|
7880
|
+
// Hour is in 24-hour format, convert to 12-hour first
|
|
7881
|
+
if (newHour === 12) {
|
|
7882
|
+
hour12 = 12;
|
|
7883
|
+
}
|
|
7884
|
+
else {
|
|
7885
|
+
hour12 = newHour - 12;
|
|
7886
|
+
}
|
|
7887
|
+
}
|
|
7888
|
+
// Now convert 12-hour to 24-hour format (0-23)
|
|
7889
|
+
if (newMeridiem === 'am') {
|
|
7890
|
+
if (hour12 === 12) {
|
|
7891
|
+
hour24 = 0; // 12 AM = 0:00
|
|
7892
|
+
}
|
|
7893
|
+
else {
|
|
7894
|
+
hour24 = hour12; // 1-11 AM = 1-11
|
|
7895
|
+
}
|
|
7334
7896
|
}
|
|
7335
7897
|
else {
|
|
7336
|
-
//
|
|
7337
|
-
|
|
7898
|
+
// PM
|
|
7899
|
+
if (hour12 === 12) {
|
|
7900
|
+
hour24 = 12; // 12 PM = 12:00
|
|
7901
|
+
}
|
|
7902
|
+
else {
|
|
7903
|
+
hour24 = hour12 + 12; // 1-11 PM = 13-23
|
|
7904
|
+
}
|
|
7338
7905
|
}
|
|
7339
7906
|
}
|
|
7340
|
-
else {
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
//
|
|
7348
|
-
if (
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7907
|
+
else if (!is24Hour && !newMeridiem) {
|
|
7908
|
+
// If in 12-hour mode but no meridiem, assume the hour is already in 12-hour format
|
|
7909
|
+
// and default to AM (or keep as is if it's a valid 12-hour value)
|
|
7910
|
+
// This shouldn't happen in normal flow, but handle it gracefully
|
|
7911
|
+
hour24 = newHour;
|
|
7912
|
+
}
|
|
7913
|
+
// If timezone selector is enabled, create date-time without timezone conversion
|
|
7914
|
+
// to ensure the selected timestamp matches the picker values exactly
|
|
7915
|
+
if (showTimezoneSelector) {
|
|
7916
|
+
// Use override if provided, otherwise use state value
|
|
7917
|
+
const offsetToUse = timezoneOffsetOverride ?? timezoneOffset;
|
|
7918
|
+
// Create date-time from the Date object without timezone conversion
|
|
7919
|
+
// Extract year, month, day from the date
|
|
7920
|
+
const year = newDate.getFullYear();
|
|
7921
|
+
const month = newDate.getMonth();
|
|
7922
|
+
const day = newDate.getDate();
|
|
7923
|
+
// Create a date-time string with the exact values from the picker
|
|
7924
|
+
const formattedDateTime = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}T${String(hour24).padStart(2, '0')}:${String(newMinute).padStart(2, '0')}:${String(newSecond ?? 0).padStart(2, '0')}`;
|
|
7925
|
+
onChange?.(`${formattedDateTime}${offsetToUse}`);
|
|
7926
|
+
return;
|
|
7927
|
+
}
|
|
7928
|
+
// Normal mode: use timezone conversion
|
|
7929
|
+
let dateTime = dayjs(newDate)
|
|
7930
|
+
.tz(tz)
|
|
7931
|
+
.hour(hour24)
|
|
7932
|
+
.minute(newMinute)
|
|
7933
|
+
.second(newSecond ?? 0)
|
|
7934
|
+
.millisecond(0);
|
|
7935
|
+
if (!dateTime.isValid()) {
|
|
7356
7936
|
onChange?.(undefined);
|
|
7357
7937
|
return;
|
|
7358
7938
|
}
|
|
7359
|
-
|
|
7360
|
-
if (
|
|
7361
|
-
|
|
7939
|
+
// Format based on format prop
|
|
7940
|
+
if (format === 'iso-date-time') {
|
|
7941
|
+
onChange?.(dateTime.format('YYYY-MM-DDTHH:mm:ss'));
|
|
7362
7942
|
}
|
|
7363
7943
|
else {
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
setHour12(null);
|
|
7367
|
-
setMinute(null);
|
|
7368
|
-
setMeridiem(null);
|
|
7369
|
-
setHour24(null);
|
|
7370
|
-
setSecond(null);
|
|
7371
|
-
onChange?.(undefined);
|
|
7944
|
+
// date-time format with timezone
|
|
7945
|
+
onChange?.(dateTime.format('YYYY-MM-DDTHH:mm:ssZ'));
|
|
7372
7946
|
}
|
|
7373
7947
|
};
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
setMinute(null);
|
|
7386
|
-
setMeridiem(null);
|
|
7387
|
-
setHour24(null);
|
|
7388
|
-
setSecond(null);
|
|
7389
|
-
onChange?.(undefined);
|
|
7390
|
-
return;
|
|
7948
|
+
// Handle date selection
|
|
7949
|
+
const handleDateSelected = ({ date, }) => {
|
|
7950
|
+
setSelectedDate(date);
|
|
7951
|
+
updateDateTime(date, hour, minute, second, meridiem);
|
|
7952
|
+
};
|
|
7953
|
+
// Handle time change
|
|
7954
|
+
const handleTimeChange = (newHour, newMinute, newSecond, newMeridiem) => {
|
|
7955
|
+
setHour(newHour);
|
|
7956
|
+
setMinute(newMinute);
|
|
7957
|
+
if (is24Hour) {
|
|
7958
|
+
setSecond(newSecond);
|
|
7391
7959
|
}
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
if (!dateObj.isValid()) {
|
|
7395
|
-
console.warn('[DateTimePicker] Invalid date object in updateDateTime, clearing fields:', date);
|
|
7396
|
-
setSelectedDate('');
|
|
7397
|
-
setHour12(null);
|
|
7398
|
-
setMinute(null);
|
|
7399
|
-
setMeridiem(null);
|
|
7400
|
-
setHour24(null);
|
|
7401
|
-
setSecond(null);
|
|
7402
|
-
onChange?.(undefined);
|
|
7403
|
-
return;
|
|
7960
|
+
else {
|
|
7961
|
+
setMeridiem(newMeridiem);
|
|
7404
7962
|
}
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7963
|
+
if (selectedDate) {
|
|
7964
|
+
updateDateTime(selectedDate, newHour, newMinute, newSecond, newMeridiem);
|
|
7965
|
+
}
|
|
7966
|
+
};
|
|
7967
|
+
// Calendar hook
|
|
7968
|
+
const calendarProps = useCalendar({
|
|
7969
|
+
selected: selectedDate || undefined,
|
|
7970
|
+
date: selectedDate || undefined,
|
|
7971
|
+
minDate,
|
|
7972
|
+
maxDate,
|
|
7973
|
+
monthsToDisplay: 1,
|
|
7974
|
+
onDateSelected: handleDateSelected,
|
|
7975
|
+
});
|
|
7976
|
+
// Convert DateTimePickerLabels to DatePickerLabels format
|
|
7977
|
+
const calendarLabels = useMemo(() => ({
|
|
7978
|
+
monthNamesShort: labels.monthNamesShort || [
|
|
7979
|
+
'Jan',
|
|
7980
|
+
'Feb',
|
|
7981
|
+
'Mar',
|
|
7982
|
+
'Apr',
|
|
7983
|
+
'May',
|
|
7984
|
+
'Jun',
|
|
7985
|
+
'Jul',
|
|
7986
|
+
'Aug',
|
|
7987
|
+
'Sep',
|
|
7988
|
+
'Oct',
|
|
7989
|
+
'Nov',
|
|
7990
|
+
'Dec',
|
|
7991
|
+
],
|
|
7992
|
+
weekdayNamesShort: labels.weekdayNamesShort || [
|
|
7993
|
+
'Sun',
|
|
7994
|
+
'Mon',
|
|
7995
|
+
'Tue',
|
|
7996
|
+
'Wed',
|
|
7997
|
+
'Thu',
|
|
7998
|
+
'Fri',
|
|
7999
|
+
'Sat',
|
|
8000
|
+
],
|
|
8001
|
+
backButtonLabel: labels.backButtonLabel || 'Back',
|
|
8002
|
+
forwardButtonLabel: labels.forwardButtonLabel || 'Forward',
|
|
8003
|
+
todayLabel: quickActionLabels.today || 'Today',
|
|
8004
|
+
yesterdayLabel: quickActionLabels.yesterday || 'Yesterday',
|
|
8005
|
+
tomorrowLabel: quickActionLabels.tomorrow || 'Tomorrow',
|
|
8006
|
+
}), [labels, quickActionLabels]);
|
|
8007
|
+
// Generate time options
|
|
8008
|
+
const timeOptions = useMemo(() => {
|
|
8009
|
+
const options = [];
|
|
8010
|
+
// Get start time for comparison if provided
|
|
8011
|
+
let startDateTime = null;
|
|
8012
|
+
let shouldFilterByDate = false;
|
|
8013
|
+
if (startTime && selectedDate) {
|
|
8014
|
+
const startDateObj = dayjs(startTime).tz(tz);
|
|
8015
|
+
const selectedDateObj = dayjs(selectedDate).tz(tz);
|
|
8016
|
+
if (startDateObj.isValid() && selectedDateObj.isValid()) {
|
|
8017
|
+
startDateTime = startDateObj;
|
|
8018
|
+
shouldFilterByDate =
|
|
8019
|
+
startDateObj.format('YYYY-MM-DD') ===
|
|
8020
|
+
selectedDateObj.format('YYYY-MM-DD');
|
|
8021
|
+
}
|
|
8022
|
+
}
|
|
8023
|
+
if (is24Hour) {
|
|
8024
|
+
// Generate 24-hour format options
|
|
8025
|
+
for (let h = 0; h < 24; h++) {
|
|
8026
|
+
for (let m = 0; m < 60; m += 15) {
|
|
8027
|
+
// Filter out times that would result in negative duration
|
|
8028
|
+
if (startDateTime && selectedDate && shouldFilterByDate) {
|
|
8029
|
+
const selectedDateObj = dayjs(selectedDate).tz(tz);
|
|
8030
|
+
const optionDateTime = selectedDateObj
|
|
8031
|
+
.hour(h)
|
|
8032
|
+
.minute(m)
|
|
8033
|
+
.second(0)
|
|
8034
|
+
.millisecond(0);
|
|
8035
|
+
if (optionDateTime.isBefore(startDateTime)) {
|
|
8036
|
+
continue;
|
|
8037
|
+
}
|
|
8038
|
+
}
|
|
8039
|
+
// Calculate duration if startTime is provided
|
|
8040
|
+
let durationText;
|
|
8041
|
+
if (startDateTime && selectedDate) {
|
|
8042
|
+
const selectedDateObj = dayjs(selectedDate).tz(tz);
|
|
8043
|
+
const optionDateTime = selectedDateObj
|
|
8044
|
+
.hour(h)
|
|
8045
|
+
.minute(m)
|
|
8046
|
+
.second(0)
|
|
8047
|
+
.millisecond(0);
|
|
8048
|
+
if (optionDateTime.isValid() &&
|
|
8049
|
+
optionDateTime.isAfter(startDateTime)) {
|
|
8050
|
+
const diffMs = optionDateTime.diff(startDateTime);
|
|
8051
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
8052
|
+
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
8053
|
+
const diffSeconds = Math.floor((diffMs % (1000 * 60)) / 1000);
|
|
8054
|
+
if (diffHours > 0 || diffMinutes > 0 || diffSeconds > 0) {
|
|
8055
|
+
let diffText = '';
|
|
8056
|
+
if (diffHours > 0) {
|
|
8057
|
+
diffText = `${diffHours}h ${diffMinutes}m`;
|
|
8058
|
+
}
|
|
8059
|
+
else if (diffMinutes > 0) {
|
|
8060
|
+
diffText = `${diffMinutes}m ${diffSeconds}s`;
|
|
8061
|
+
}
|
|
8062
|
+
else {
|
|
8063
|
+
diffText = `${diffSeconds}s`;
|
|
8064
|
+
}
|
|
8065
|
+
durationText = `+${diffText}`;
|
|
8066
|
+
}
|
|
8067
|
+
}
|
|
8068
|
+
}
|
|
8069
|
+
const s = showSeconds ? 0 : 0;
|
|
8070
|
+
const timeDisplay = showSeconds
|
|
8071
|
+
? `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:00`
|
|
8072
|
+
: `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
|
|
8073
|
+
options.push({
|
|
8074
|
+
label: timeDisplay,
|
|
8075
|
+
value: `${h}:${m}:${s}`,
|
|
8076
|
+
hour: h,
|
|
8077
|
+
minute: m,
|
|
8078
|
+
second: s,
|
|
8079
|
+
searchText: timeDisplay,
|
|
8080
|
+
durationText,
|
|
8081
|
+
});
|
|
8082
|
+
}
|
|
7423
8083
|
}
|
|
7424
|
-
console.log('[DateTimePicker] ISO format - setting time on date:', {
|
|
7425
|
-
h,
|
|
7426
|
-
m,
|
|
7427
|
-
s,
|
|
7428
|
-
showSeconds,
|
|
7429
|
-
});
|
|
7430
|
-
if (h !== null)
|
|
7431
|
-
newDate.setHours(h);
|
|
7432
|
-
if (m !== null)
|
|
7433
|
-
newDate.setMinutes(m);
|
|
7434
|
-
newDate.setSeconds(s ?? 0);
|
|
7435
8084
|
}
|
|
7436
8085
|
else {
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
8086
|
+
// Generate 12-hour format options
|
|
8087
|
+
for (let h = 1; h <= 12; h++) {
|
|
8088
|
+
for (let m = 0; m < 60; m += 15) {
|
|
8089
|
+
for (const mer of ['am', 'pm']) {
|
|
8090
|
+
// Convert 12-hour to 24-hour for comparison
|
|
8091
|
+
let hour24 = h;
|
|
8092
|
+
if (mer === 'am' && h === 12)
|
|
8093
|
+
hour24 = 0;
|
|
8094
|
+
else if (mer === 'pm' && h < 12)
|
|
8095
|
+
hour24 = h + 12;
|
|
8096
|
+
// Filter out times that would result in negative duration
|
|
8097
|
+
if (startDateTime && selectedDate && shouldFilterByDate) {
|
|
8098
|
+
const selectedDateObj = dayjs(selectedDate).tz(tz);
|
|
8099
|
+
const optionDateTime = selectedDateObj
|
|
8100
|
+
.hour(hour24)
|
|
8101
|
+
.minute(m)
|
|
8102
|
+
.second(0)
|
|
8103
|
+
.millisecond(0);
|
|
8104
|
+
if (optionDateTime.isBefore(startDateTime)) {
|
|
8105
|
+
continue;
|
|
8106
|
+
}
|
|
8107
|
+
}
|
|
8108
|
+
// Calculate duration if startTime is provided
|
|
8109
|
+
let durationText;
|
|
8110
|
+
if (startDateTime && selectedDate) {
|
|
8111
|
+
const selectedDateObj = dayjs(selectedDate).tz(tz);
|
|
8112
|
+
const optionDateTime = selectedDateObj
|
|
8113
|
+
.hour(hour24)
|
|
8114
|
+
.minute(m)
|
|
8115
|
+
.second(0)
|
|
8116
|
+
.millisecond(0);
|
|
8117
|
+
if (optionDateTime.isValid() &&
|
|
8118
|
+
optionDateTime.isAfter(startDateTime)) {
|
|
8119
|
+
const diffMs = optionDateTime.diff(startDateTime);
|
|
8120
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
8121
|
+
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
8122
|
+
const diffSeconds = Math.floor((diffMs % (1000 * 60)) / 1000);
|
|
8123
|
+
if (diffHours > 0 || diffMinutes > 0 || diffSeconds > 0) {
|
|
8124
|
+
let diffText = '';
|
|
8125
|
+
if (diffHours > 0) {
|
|
8126
|
+
diffText = `${diffHours}h ${diffMinutes}m`;
|
|
8127
|
+
}
|
|
8128
|
+
else if (diffMinutes > 0) {
|
|
8129
|
+
diffText = `${diffMinutes}m ${diffSeconds}s`;
|
|
8130
|
+
}
|
|
8131
|
+
else {
|
|
8132
|
+
diffText = `${diffSeconds}s`;
|
|
8133
|
+
}
|
|
8134
|
+
durationText = `+${diffText}`;
|
|
8135
|
+
}
|
|
8136
|
+
}
|
|
8137
|
+
}
|
|
8138
|
+
const hourDisplay = h.toString();
|
|
8139
|
+
const minuteDisplay = m.toString().padStart(2, '0');
|
|
8140
|
+
const timeDisplay = `${hourDisplay}:${minuteDisplay} ${mer.toUpperCase()}`;
|
|
8141
|
+
options.push({
|
|
8142
|
+
label: timeDisplay,
|
|
8143
|
+
value: `${h}:${m}:${mer}`,
|
|
8144
|
+
hour: h,
|
|
8145
|
+
minute: m,
|
|
8146
|
+
meridiem: mer,
|
|
8147
|
+
searchText: timeDisplay,
|
|
8148
|
+
durationText,
|
|
8149
|
+
});
|
|
8150
|
+
}
|
|
8151
|
+
}
|
|
7457
8152
|
}
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
8153
|
+
// Sort 12-hour options by time
|
|
8154
|
+
return options.sort((a, b) => {
|
|
8155
|
+
const a12 = a;
|
|
8156
|
+
const b12 = b;
|
|
8157
|
+
let hour24A = a12.hour;
|
|
8158
|
+
if (a12.meridiem === 'am' && a12.hour === 12)
|
|
8159
|
+
hour24A = 0;
|
|
8160
|
+
else if (a12.meridiem === 'pm' && a12.hour < 12)
|
|
8161
|
+
hour24A = a12.hour + 12;
|
|
8162
|
+
let hour24B = b12.hour;
|
|
8163
|
+
if (b12.meridiem === 'am' && b12.hour === 12)
|
|
8164
|
+
hour24B = 0;
|
|
8165
|
+
else if (b12.meridiem === 'pm' && b12.hour < 12)
|
|
8166
|
+
hour24B = b12.hour + 12;
|
|
8167
|
+
if (hour24A !== hour24B) {
|
|
8168
|
+
return hour24A - hour24B;
|
|
8169
|
+
}
|
|
8170
|
+
return a12.minute - b12.minute;
|
|
7462
8171
|
});
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
8172
|
+
}
|
|
8173
|
+
return options;
|
|
8174
|
+
}, [startTime, selectedDate, tz, is24Hour, showSeconds]);
|
|
8175
|
+
// Time picker combobox setup
|
|
8176
|
+
const itemToString = useMemo(() => {
|
|
8177
|
+
return (item) => {
|
|
8178
|
+
return item.searchText;
|
|
8179
|
+
};
|
|
8180
|
+
}, []);
|
|
8181
|
+
const { contains } = useFilter({ sensitivity: 'base' });
|
|
8182
|
+
const customTimeFilter = useMemo(() => {
|
|
8183
|
+
if (is24Hour) {
|
|
8184
|
+
return contains;
|
|
8185
|
+
}
|
|
8186
|
+
return (itemText, filterText) => {
|
|
8187
|
+
if (!filterText) {
|
|
8188
|
+
return true;
|
|
7475
8189
|
}
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
});
|
|
8190
|
+
const lowerItemText = itemText.toLowerCase();
|
|
8191
|
+
const lowerFilterText = filterText.toLowerCase();
|
|
8192
|
+
if (lowerItemText.includes(lowerFilterText)) {
|
|
8193
|
+
return true;
|
|
7481
8194
|
}
|
|
7482
|
-
|
|
7483
|
-
|
|
8195
|
+
const item = timeOptions.find((opt) => opt.searchText.toLowerCase() === lowerItemText);
|
|
8196
|
+
if (!item || !('meridiem' in item)) {
|
|
8197
|
+
return false;
|
|
7484
8198
|
}
|
|
7485
|
-
|
|
7486
|
-
|
|
8199
|
+
let hour24 = item.hour;
|
|
8200
|
+
if (item.meridiem === 'am' && item.hour === 12)
|
|
8201
|
+
hour24 = 0;
|
|
8202
|
+
else if (item.meridiem === 'pm' && item.hour < 12)
|
|
8203
|
+
hour24 = item.hour + 12;
|
|
8204
|
+
const hour24Str = hour24.toString().padStart(2, '0');
|
|
8205
|
+
const minuteStr = item.minute.toString().padStart(2, '0');
|
|
8206
|
+
const formats = [
|
|
8207
|
+
`${hour24Str}:${minuteStr}`,
|
|
8208
|
+
`${hour24Str}${minuteStr}`,
|
|
8209
|
+
hour24Str,
|
|
8210
|
+
`${hour24}:${minuteStr}`,
|
|
8211
|
+
hour24.toString(),
|
|
8212
|
+
];
|
|
8213
|
+
return formats.some((format) => format.toLowerCase().includes(lowerFilterText) ||
|
|
8214
|
+
lowerFilterText.includes(format.toLowerCase()));
|
|
8215
|
+
};
|
|
8216
|
+
}, [timeOptions, is24Hour, contains]);
|
|
8217
|
+
const { collection, filter } = useListCollection({
|
|
8218
|
+
initialItems: timeOptions,
|
|
8219
|
+
itemToString: itemToString,
|
|
8220
|
+
itemToValue: (item) => item.value,
|
|
8221
|
+
filter: customTimeFilter,
|
|
8222
|
+
});
|
|
8223
|
+
// Get current value string for combobox (must match option.value format)
|
|
8224
|
+
const currentTimeValue = useMemo(() => {
|
|
8225
|
+
if (is24Hour) {
|
|
8226
|
+
if (hour === null || minute === null) {
|
|
8227
|
+
return '';
|
|
7487
8228
|
}
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
const finalISO = dayjs(newDate).tz(timezone).toISOString();
|
|
7491
|
-
console.log('[DateTimePicker] Final ISO string to emit:', {
|
|
7492
|
-
newDate: newDate.toISOString(),
|
|
7493
|
-
timezone,
|
|
7494
|
-
finalISO,
|
|
7495
|
-
});
|
|
7496
|
-
onChange?.(finalISO);
|
|
7497
|
-
};
|
|
7498
|
-
const handleClear = () => {
|
|
7499
|
-
setSelectedDate('');
|
|
7500
|
-
setHour12(null);
|
|
7501
|
-
setHour24(null);
|
|
7502
|
-
setMinute(null);
|
|
7503
|
-
setSecond(null);
|
|
7504
|
-
setMeridiem(null);
|
|
7505
|
-
onChange?.(undefined);
|
|
7506
|
-
};
|
|
7507
|
-
const isISO = format === 'iso-date-time';
|
|
7508
|
-
// Normalize startTime to ignore milliseconds
|
|
7509
|
-
const normalizedStartTime = startTime
|
|
7510
|
-
? dayjs(startTime).tz(timezone).millisecond(0).toISOString()
|
|
7511
|
-
: undefined;
|
|
7512
|
-
// Determine minDate: prioritize explicit minDate prop, then fall back to startTime
|
|
7513
|
-
const effectiveMinDate = minDate
|
|
7514
|
-
? minDate
|
|
7515
|
-
: normalizedStartTime && dayjs(normalizedStartTime).tz(timezone).isValid()
|
|
7516
|
-
? dayjs(normalizedStartTime).tz(timezone).startOf('day').toDate()
|
|
7517
|
-
: undefined;
|
|
7518
|
-
// Log current state before render
|
|
7519
|
-
useEffect(() => {
|
|
7520
|
-
console.log('[DateTimePicker] Current state before render:', {
|
|
7521
|
-
isISO,
|
|
7522
|
-
hour12,
|
|
7523
|
-
minute,
|
|
7524
|
-
meridiem,
|
|
7525
|
-
hour24,
|
|
7526
|
-
second,
|
|
7527
|
-
selectedDate,
|
|
7528
|
-
normalizedStartTime,
|
|
7529
|
-
timezone,
|
|
7530
|
-
});
|
|
7531
|
-
}, [
|
|
7532
|
-
isISO,
|
|
7533
|
-
hour12,
|
|
7534
|
-
minute,
|
|
7535
|
-
meridiem,
|
|
7536
|
-
hour24,
|
|
7537
|
-
second,
|
|
7538
|
-
selectedDate,
|
|
7539
|
-
normalizedStartTime,
|
|
7540
|
-
timezone,
|
|
7541
|
-
]);
|
|
7542
|
-
// Compute display text from current state
|
|
7543
|
-
const displayText = useMemo(() => {
|
|
7544
|
-
if (!selectedDate)
|
|
7545
|
-
return null;
|
|
7546
|
-
const dateObj = dayjs.tz(selectedDate, timezone);
|
|
7547
|
-
if (!dateObj.isValid())
|
|
7548
|
-
return null;
|
|
7549
|
-
if (isISO) {
|
|
7550
|
-
// For ISO format, use hour24, minute, second
|
|
7551
|
-
if (hour24 === null || minute === null)
|
|
7552
|
-
return null;
|
|
7553
|
-
const dateTimeObj = dateObj
|
|
7554
|
-
.hour(hour24)
|
|
7555
|
-
.minute(minute)
|
|
7556
|
-
.second(second ?? 0);
|
|
7557
|
-
return dateTimeObj.format(showSeconds ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm');
|
|
8229
|
+
const s = second ?? 0;
|
|
8230
|
+
return `${hour}:${minute}:${s}`;
|
|
7558
8231
|
}
|
|
7559
8232
|
else {
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
|
|
7568
|
-
|
|
7569
|
-
|
|
7570
|
-
|
|
8233
|
+
if (hour === null || minute === null || meridiem === null) {
|
|
8234
|
+
return '';
|
|
8235
|
+
}
|
|
8236
|
+
return `${hour}:${minute}:${meridiem}`;
|
|
8237
|
+
}
|
|
8238
|
+
}, [hour, minute, second, meridiem, is24Hour]);
|
|
8239
|
+
// Parse custom time input formats like "1400", "2pm", "14:00", "2:00 PM"
|
|
8240
|
+
const parseCustomTimeInput = (input) => {
|
|
8241
|
+
if (!input || !input.trim()) {
|
|
8242
|
+
return { hour: null, minute: null, second: null, meridiem: null };
|
|
8243
|
+
}
|
|
8244
|
+
const trimmed = input.trim().toLowerCase();
|
|
8245
|
+
// Try parsing 4-digit format without colon: "1400" -> 14:00
|
|
8246
|
+
const fourDigitMatch = trimmed.match(/^(\d{4})$/);
|
|
8247
|
+
if (fourDigitMatch) {
|
|
8248
|
+
const digits = fourDigitMatch[1];
|
|
8249
|
+
const hour = parseInt(digits.substring(0, 2), 10);
|
|
8250
|
+
const minute = parseInt(digits.substring(2, 4), 10);
|
|
8251
|
+
if (hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59) {
|
|
8252
|
+
if (is24Hour) {
|
|
8253
|
+
return { hour, minute, second: 0, meridiem: null };
|
|
8254
|
+
}
|
|
8255
|
+
else {
|
|
8256
|
+
// Convert to 12-hour format
|
|
8257
|
+
let hour12 = hour;
|
|
8258
|
+
let meridiem;
|
|
8259
|
+
if (hour === 0) {
|
|
8260
|
+
hour12 = 12;
|
|
8261
|
+
meridiem = 'am';
|
|
8262
|
+
}
|
|
8263
|
+
else if (hour === 12) {
|
|
8264
|
+
hour12 = 12;
|
|
8265
|
+
meridiem = 'pm';
|
|
8266
|
+
}
|
|
8267
|
+
else if (hour > 12) {
|
|
8268
|
+
hour12 = hour - 12;
|
|
8269
|
+
meridiem = 'pm';
|
|
8270
|
+
}
|
|
8271
|
+
else {
|
|
8272
|
+
hour12 = hour;
|
|
8273
|
+
meridiem = 'am';
|
|
8274
|
+
}
|
|
8275
|
+
return { hour: hour12, minute, second: null, meridiem };
|
|
8276
|
+
}
|
|
8277
|
+
}
|
|
7571
8278
|
}
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
7588
|
-
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
8279
|
+
// Try parsing hour with meridiem: "2pm", "14pm", "2am"
|
|
8280
|
+
const hourMeridiemMatch = trimmed.match(/^(\d{1,2})\s*(am|pm)$/);
|
|
8281
|
+
if (hourMeridiemMatch && !is24Hour) {
|
|
8282
|
+
const hour12 = parseInt(hourMeridiemMatch[1], 10);
|
|
8283
|
+
const meridiem = hourMeridiemMatch[2];
|
|
8284
|
+
if (hour12 >= 1 && hour12 <= 12) {
|
|
8285
|
+
return { hour: hour12, minute: 0, second: null, meridiem };
|
|
8286
|
+
}
|
|
8287
|
+
}
|
|
8288
|
+
// Try parsing 24-hour format with hour only: "14" -> 14:00
|
|
8289
|
+
const hourOnlyMatch = trimmed.match(/^(\d{1,2})$/);
|
|
8290
|
+
if (hourOnlyMatch && is24Hour) {
|
|
8291
|
+
const hour = parseInt(hourOnlyMatch[1], 10);
|
|
8292
|
+
if (hour >= 0 && hour <= 23) {
|
|
8293
|
+
return { hour, minute: 0, second: 0, meridiem: null };
|
|
8294
|
+
}
|
|
8295
|
+
}
|
|
8296
|
+
// Try parsing standard formats: "14:00", "2:00 PM"
|
|
8297
|
+
const time24Pattern = /^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?$/;
|
|
8298
|
+
const match24 = trimmed.match(time24Pattern);
|
|
8299
|
+
if (match24) {
|
|
8300
|
+
const hour24 = parseInt(match24[1], 10);
|
|
8301
|
+
const minute = parseInt(match24[2], 10);
|
|
8302
|
+
const second = match24[3] ? parseInt(match24[3], 10) : 0;
|
|
8303
|
+
if (hour24 >= 0 &&
|
|
8304
|
+
hour24 <= 23 &&
|
|
8305
|
+
minute >= 0 &&
|
|
8306
|
+
minute <= 59 &&
|
|
8307
|
+
second >= 0 &&
|
|
8308
|
+
second <= 59) {
|
|
8309
|
+
if (is24Hour) {
|
|
8310
|
+
return { hour: hour24, minute, second, meridiem: null };
|
|
8311
|
+
}
|
|
8312
|
+
else {
|
|
8313
|
+
// Convert to 12-hour format
|
|
8314
|
+
let hour12 = hour24;
|
|
8315
|
+
let meridiem;
|
|
8316
|
+
if (hour24 === 0) {
|
|
8317
|
+
hour12 = 12;
|
|
8318
|
+
meridiem = 'am';
|
|
8319
|
+
}
|
|
8320
|
+
else if (hour24 === 12) {
|
|
8321
|
+
hour12 = 12;
|
|
8322
|
+
meridiem = 'pm';
|
|
8323
|
+
}
|
|
8324
|
+
else if (hour24 > 12) {
|
|
8325
|
+
hour12 = hour24 - 12;
|
|
8326
|
+
meridiem = 'pm';
|
|
7592
8327
|
}
|
|
7593
8328
|
else {
|
|
7594
|
-
|
|
7595
|
-
|
|
8329
|
+
hour12 = hour24;
|
|
8330
|
+
meridiem = 'am';
|
|
8331
|
+
}
|
|
8332
|
+
return { hour: hour12, minute, second: null, meridiem };
|
|
8333
|
+
}
|
|
8334
|
+
}
|
|
8335
|
+
}
|
|
8336
|
+
// Try parsing 12-hour format: "2:00 PM", "2:00PM"
|
|
8337
|
+
const time12Pattern = /^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?\s*(am|pm)$/;
|
|
8338
|
+
const match12 = trimmed.match(time12Pattern);
|
|
8339
|
+
if (match12 && !is24Hour) {
|
|
8340
|
+
const hour12 = parseInt(match12[1], 10);
|
|
8341
|
+
const minute = parseInt(match12[2], 10);
|
|
8342
|
+
const second = match12[3] ? parseInt(match12[3], 10) : null;
|
|
8343
|
+
const meridiem = match12[4];
|
|
8344
|
+
if (hour12 >= 1 &&
|
|
8345
|
+
hour12 <= 12 &&
|
|
8346
|
+
minute >= 0 &&
|
|
8347
|
+
minute <= 59 &&
|
|
8348
|
+
(second === null || (second >= 0 && second <= 59))) {
|
|
8349
|
+
return { hour: hour12, minute, second, meridiem };
|
|
8350
|
+
}
|
|
8351
|
+
}
|
|
8352
|
+
return { hour: null, minute: null, second: null, meridiem: null };
|
|
8353
|
+
};
|
|
8354
|
+
const handleTimeValueChange = (details) => {
|
|
8355
|
+
if (details.value.length === 0) {
|
|
8356
|
+
handleTimeChange(null, null, null, null);
|
|
8357
|
+
filter('');
|
|
8358
|
+
return;
|
|
8359
|
+
}
|
|
8360
|
+
const selectedValue = details.value[0];
|
|
8361
|
+
const selectedOption = timeOptions.find((opt) => opt.value === selectedValue);
|
|
8362
|
+
if (selectedOption) {
|
|
8363
|
+
filter('');
|
|
8364
|
+
if (is24Hour) {
|
|
8365
|
+
const opt24 = selectedOption;
|
|
8366
|
+
handleTimeChange(opt24.hour, opt24.minute, opt24.second, null);
|
|
8367
|
+
}
|
|
8368
|
+
else {
|
|
8369
|
+
const opt12 = selectedOption;
|
|
8370
|
+
handleTimeChange(opt12.hour, opt12.minute, null, opt12.meridiem);
|
|
8371
|
+
}
|
|
8372
|
+
}
|
|
8373
|
+
};
|
|
8374
|
+
// Track the current input value for Enter key handling
|
|
8375
|
+
const [timeInputValue, setTimeInputValue] = useState('');
|
|
8376
|
+
const handleTimeInputChange = (details) => {
|
|
8377
|
+
// Store the input value and filter
|
|
8378
|
+
setTimeInputValue(details.inputValue);
|
|
8379
|
+
filter(details.inputValue);
|
|
8380
|
+
};
|
|
8381
|
+
const handleTimeInputKeyDown = (e) => {
|
|
8382
|
+
if (e.key === 'Enter') {
|
|
8383
|
+
e.preventDefault();
|
|
8384
|
+
// Use the stored input value
|
|
8385
|
+
const parsed = parseCustomTimeInput(timeInputValue);
|
|
8386
|
+
if (parsed.hour !== null && parsed.minute !== null) {
|
|
8387
|
+
if (is24Hour) {
|
|
8388
|
+
handleTimeChange(parsed.hour, parsed.minute, parsed.second, null);
|
|
8389
|
+
}
|
|
8390
|
+
else {
|
|
8391
|
+
if (parsed.meridiem !== null) {
|
|
8392
|
+
handleTimeChange(parsed.hour, parsed.minute, null, parsed.meridiem);
|
|
7596
8393
|
}
|
|
7597
|
-
}
|
|
8394
|
+
}
|
|
8395
|
+
// Clear the filter and input value after applying
|
|
8396
|
+
filter('');
|
|
8397
|
+
setTimeInputValue('');
|
|
8398
|
+
// Close the popover if value is valid
|
|
8399
|
+
setTimePopoverOpen(false);
|
|
8400
|
+
}
|
|
8401
|
+
}
|
|
8402
|
+
};
|
|
8403
|
+
return (jsxs(Flex, { direction: "row", gap: 2, align: "center", children: [jsxs(Popover.Root, { open: datePopoverOpen, onOpenChange: (e) => setDatePopoverOpen(e.open), closeOnInteractOutside: true, autoFocus: false, children: [jsx(Popover.Trigger, { asChild: true, children: jsxs(Button$1, { size: "sm", variant: "outline", onClick: () => setDatePopoverOpen(true), justifyContent: "start", children: [jsx(MdDateRange, {}), dateDisplayText] }) }), portalled ? (jsx(Portal, { children: jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", children: jsx(Popover.Body, { p: 4, children: jsxs(Grid, { gap: 4, children: [jsx(InputGroup$1, { endElement: jsxs(Popover.Root, { open: calendarPopoverOpen, onOpenChange: (e) => setCalendarPopoverOpen(e.open), closeOnInteractOutside: true, autoFocus: false, children: [jsx(Popover.Trigger, { asChild: true, children: jsx(Button$1, { variant: "ghost", size: "xs", "aria-label": "Open calendar", onClick: () => setCalendarPopoverOpen(true), children: jsx(MdDateRange, {}) }) }), jsx(Portal, { children: jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", zIndex: 1500, children: jsx(Popover.Body, { p: 4, children: jsx(DatePickerContext.Provider, { value: { labels: calendarLabels }, children: jsx(Calendar, { ...calendarProps, firstDayOfWeek: 0 }) }) }) }) }) })] }), children: jsx(Input, { value: dateInputValue, onChange: handleDateInputChange, onBlur: handleDateInputBlur, onKeyDown: handleDateInputKeyDown, placeholder: "YYYY-MM-DD" }) }), showQuickActions && (jsxs(Grid, { templateColumns: "repeat(4, 1fr)", gap: 2, children: [jsx(Button$1, { size: "sm", variant: "outline", onClick: () => handleQuickActionClick(getYesterday()), disabled: !isDateValid(getYesterday()), children: quickActionLabels.yesterday }), jsx(Button$1, { size: "sm", variant: "outline", onClick: () => handleQuickActionClick(getToday()), disabled: !isDateValid(getToday()), children: quickActionLabels.today }), jsx(Button$1, { size: "sm", variant: "outline", onClick: () => handleQuickActionClick(getTomorrow()), disabled: !isDateValid(getTomorrow()), children: quickActionLabels.tomorrow }), jsx(Button$1, { size: "sm", variant: "outline", onClick: () => handleQuickActionClick(getPlus7Days()), disabled: !isDateValid(getPlus7Days()), children: quickActionLabels.plus7Days })] }))] }) }) }) }) })) : (jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", children: jsx(Popover.Body, { p: 4, children: jsxs(Grid, { gap: 4, children: [jsx(InputGroup$1, { endElement: jsxs(Popover.Root, { open: calendarPopoverOpen, onOpenChange: (e) => setCalendarPopoverOpen(e.open), closeOnInteractOutside: true, autoFocus: false, children: [jsx(Popover.Trigger, { asChild: true, children: jsx(Button$1, { variant: "ghost", size: "xs", "aria-label": "Open calendar", onClick: () => setCalendarPopoverOpen(true), children: jsx(MdDateRange, {}) }) }), jsx(Portal, { children: jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", zIndex: 1700, children: jsx(Popover.Body, { p: 4, children: jsx(DatePickerContext.Provider, { value: { labels: calendarLabels }, children: jsx(Calendar, { ...calendarProps, firstDayOfWeek: 0 }) }) }) }) }) })] }), children: jsx(Input, { value: dateInputValue, onChange: handleDateInputChange, onBlur: handleDateInputBlur, onKeyDown: handleDateInputKeyDown, placeholder: "YYYY-MM-DD" }) }), showQuickActions && (jsxs(Grid, { templateColumns: "repeat(4, 1fr)", gap: 2, children: [jsx(Button$1, { size: "sm", variant: "outline", onClick: () => handleQuickActionClick(getYesterday()), disabled: !isDateValid(getYesterday()), children: quickActionLabels.yesterday }), jsx(Button$1, { size: "sm", variant: "outline", onClick: () => handleQuickActionClick(getToday()), disabled: !isDateValid(getToday()), children: quickActionLabels.today }), jsx(Button$1, { size: "sm", variant: "outline", onClick: () => handleQuickActionClick(getTomorrow()), disabled: !isDateValid(getTomorrow()), children: quickActionLabels.tomorrow }), jsx(Button$1, { size: "sm", variant: "outline", onClick: () => handleQuickActionClick(getPlus7Days()), disabled: !isDateValid(getPlus7Days()), children: quickActionLabels.plus7Days })] }))] }) }) }) }))] }), jsxs(Popover.Root, { open: timePopoverOpen, onOpenChange: (e) => setTimePopoverOpen(e.open), closeOnInteractOutside: true, autoFocus: false, children: [jsx(Popover.Trigger, { asChild: true, children: jsxs(Button$1, { size: "sm", variant: "outline", onClick: () => setTimePopoverOpen(true), justifyContent: "start", children: [jsx(BsClock, {}), timeDisplayText] }) }), portalled ? (jsx(Portal, { children: jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", minW: "300px", children: jsx(Popover.Body, { p: 4, children: jsx(Grid, { gap: 2, children: jsxs(Combobox.Root, { value: currentTimeValue ? [currentTimeValue] : [], onValueChange: handleTimeValueChange, onInputValueChange: handleTimeInputChange, collection: collection, allowCustomValue: true, children: [jsxs(Combobox.Control, { children: [jsx(InputGroup$1, { startElement: jsx(BsClock, {}), children: jsx(Combobox.Input, { placeholder: timePickerLabels?.placeholder ??
|
|
8404
|
+
(is24Hour ? 'HH:mm' : 'hh:mm AM/PM'), onKeyDown: handleTimeInputKeyDown }) }), jsx(Combobox.IndicatorGroup, { children: jsx(Combobox.Trigger, {}) })] }), jsx(Portal, { disabled: true, children: jsx(Combobox.Positioner, { children: jsxs(Combobox.Content, { children: [jsx(Combobox.Empty, { children: timePickerLabels?.emptyMessage ??
|
|
8405
|
+
'No time found' }), collection.items.map((item) => {
|
|
8406
|
+
const option = item;
|
|
8407
|
+
return (jsxs(Combobox.Item, { item: item, children: [jsxs(Flex, { justify: "space-between", align: "center", w: "100%", children: [jsx(Text, { children: option.label }), option.durationText && (jsx(Text, { fontSize: "xs", color: "gray.500", children: option.durationText }))] }), jsx(Combobox.ItemIndicator, {})] }, option.value));
|
|
8408
|
+
})] }) }) })] }) }) }) }) }) })) : (jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", minW: "300px", children: jsx(Popover.Body, { p: 4, children: jsx(Grid, { gap: 2, children: jsxs(Combobox.Root, { value: currentTimeValue ? [currentTimeValue] : [], onValueChange: handleTimeValueChange, onInputValueChange: handleTimeInputChange, collection: collection, allowCustomValue: true, children: [jsxs(Combobox.Control, { children: [jsx(InputGroup$1, { startElement: jsx(BsClock, {}), children: jsx(Combobox.Input, { placeholder: timePickerLabels?.placeholder ??
|
|
8409
|
+
(is24Hour ? 'HH:mm' : 'hh:mm AM/PM'), onKeyDown: handleTimeInputKeyDown }) }), jsx(Combobox.IndicatorGroup, { children: jsx(Combobox.Trigger, {}) })] }), jsx(Portal, { disabled: true, children: jsx(Combobox.Positioner, { children: jsxs(Combobox.Content, { children: [jsx(Combobox.Empty, { children: timePickerLabels?.emptyMessage ?? 'No time found' }), collection.items.map((item) => {
|
|
8410
|
+
const option = item;
|
|
8411
|
+
return (jsxs(Combobox.Item, { item: item, children: [jsxs(Flex, { justify: "space-between", align: "center", w: "100%", children: [jsx(Text, { children: option.label }), option.durationText && (jsx(Text, { fontSize: "xs", color: "gray.500", children: option.durationText }))] }), jsx(Combobox.ItemIndicator, {})] }, option.value));
|
|
8412
|
+
})] }) }) })] }) }) }) }) }))] }), showTimezoneSelector && (jsxs(Popover.Root, { open: timezonePopoverOpen, onOpenChange: (e) => setTimezonePopoverOpen(e.open), closeOnInteractOutside: true, autoFocus: false, children: [jsx(Popover.Trigger, { asChild: true, children: jsx(Button$1, { size: "sm", variant: "outline", onClick: () => setTimezonePopoverOpen(true), justifyContent: "start", children: timezoneDisplayText || 'Select timezone' }) }), portalled ? (jsx(Portal, { children: jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", minW: "250px", children: jsx(Popover.Body, { p: 4, children: jsx(Grid, { gap: 2, children: jsxs(Select.Root, { size: "sm", collection: timezoneCollection, value: validTimezoneOffset ? [validTimezoneOffset] : [], onValueChange: (e) => {
|
|
8413
|
+
const newOffset = e.value[0];
|
|
8414
|
+
if (newOffset) {
|
|
8415
|
+
// Update controlled or internal state
|
|
8416
|
+
if (onTimezoneOffsetChange) {
|
|
8417
|
+
onTimezoneOffsetChange(newOffset);
|
|
8418
|
+
}
|
|
8419
|
+
else {
|
|
8420
|
+
setInternalTimezoneOffset(newOffset);
|
|
8421
|
+
}
|
|
8422
|
+
// Update date-time with new offset (pass it directly to avoid stale state)
|
|
8423
|
+
if (selectedDate &&
|
|
8424
|
+
hour !== null &&
|
|
8425
|
+
minute !== null) {
|
|
8426
|
+
updateDateTime(selectedDate, hour, minute, second, meridiem, newOffset);
|
|
8427
|
+
}
|
|
8428
|
+
// Close popover after selection
|
|
8429
|
+
setTimezonePopoverOpen(false);
|
|
8430
|
+
}
|
|
8431
|
+
}, children: [jsxs(Select.Control, { children: [jsx(Select.Trigger, {}), jsx(Select.IndicatorGroup, { children: jsx(Select.Indicator, {}) })] }), jsx(Select.Positioner, { children: jsx(Select.Content, { children: timezoneCollection.items.map((item) => (jsxs(Select.Item, { item: item, children: [jsx(Select.ItemText, { children: item.label }), jsx(Select.ItemIndicator, {})] }, item.value))) }) })] }) }) }) }) }) })) : (jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", minW: "250px", children: jsx(Popover.Body, { p: 4, children: jsx(Grid, { gap: 2, children: jsxs(Select.Root, { size: "sm", collection: timezoneCollection, value: validTimezoneOffset ? [validTimezoneOffset] : [], onValueChange: (e) => {
|
|
8432
|
+
const newOffset = e.value[0];
|
|
8433
|
+
if (newOffset) {
|
|
8434
|
+
// Update controlled or internal state
|
|
8435
|
+
if (onTimezoneOffsetChange) {
|
|
8436
|
+
onTimezoneOffsetChange(newOffset);
|
|
8437
|
+
}
|
|
8438
|
+
else {
|
|
8439
|
+
setInternalTimezoneOffset(newOffset);
|
|
8440
|
+
}
|
|
8441
|
+
// Update date-time with new offset (pass it directly to avoid stale state)
|
|
8442
|
+
if (selectedDate &&
|
|
8443
|
+
hour !== null &&
|
|
8444
|
+
minute !== null) {
|
|
8445
|
+
updateDateTime(selectedDate, hour, minute, second, meridiem, newOffset);
|
|
8446
|
+
}
|
|
8447
|
+
// Close popover after selection
|
|
8448
|
+
setTimezonePopoverOpen(false);
|
|
8449
|
+
}
|
|
8450
|
+
}, children: [jsxs(Select.Control, { children: [jsx(Select.Trigger, {}), jsx(Select.IndicatorGroup, { children: jsx(Select.Indicator, {}) })] }), jsx(Select.Positioner, { children: jsx(Select.Content, { children: timezoneCollection.items.map((item) => (jsxs(Select.Item, { item: item, children: [jsx(Select.ItemText, { children: item.label }), jsx(Select.ItemIndicator, {})] }, item.value))) }) })] }) }) }) }) }))] }))] }));
|
|
7598
8451
|
}
|
|
7599
8452
|
|
|
7600
8453
|
dayjs.extend(utc);
|
|
@@ -7605,14 +8458,15 @@ const DateTimePicker = ({ column, schema, prefix, }) => {
|
|
|
7605
8458
|
const formI18n = useFormI18n(column, prefix, schema);
|
|
7606
8459
|
const { required, gridColumn = 'span 12', gridRow = 'span 1', displayDateFormat = 'YYYY-MM-DD HH:mm:ss',
|
|
7607
8460
|
// with timezone
|
|
7608
|
-
dateFormat = 'YYYY-MM-DD[T]HH:mm:ssZ', } = schema;
|
|
8461
|
+
dateFormat = 'YYYY-MM-DD[T]HH:mm:ssZ', dateTimePicker, } = schema;
|
|
7609
8462
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
7610
8463
|
const colLabel = formI18n.colLabel;
|
|
7611
|
-
|
|
8464
|
+
useState(false);
|
|
7612
8465
|
const selectedDate = watch(colLabel);
|
|
7613
|
-
|
|
8466
|
+
selectedDate && dayjs(selectedDate).tz(timezone).isValid()
|
|
7614
8467
|
? dayjs(selectedDate).tz(timezone).format(displayDateFormat)
|
|
7615
8468
|
: '';
|
|
8469
|
+
// Set default date on mount if no value exists
|
|
7616
8470
|
const dateTimePickerLabelsConfig = {
|
|
7617
8471
|
monthNamesShort: dateTimePickerLabels?.monthNamesShort ?? [
|
|
7618
8472
|
'January',
|
|
@@ -7652,11 +8506,9 @@ const DateTimePicker = ({ column, schema, prefix, }) => {
|
|
|
7652
8506
|
else {
|
|
7653
8507
|
setValue(colLabel, undefined);
|
|
7654
8508
|
}
|
|
7655
|
-
}, timezone: timezone, labels: dateTimePickerLabelsConfig, timePickerLabels: timePickerLabels }));
|
|
8509
|
+
}, timezone: timezone, labels: dateTimePickerLabelsConfig, timePickerLabels: timePickerLabels, showQuickActions: dateTimePicker?.showQuickActions ?? false, quickActionLabels: dateTimePicker?.quickActionLabels, showTimezoneSelector: dateTimePicker?.showTimezoneSelector ?? false }));
|
|
7656
8510
|
return (jsx(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
7657
|
-
gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children:
|
|
7658
|
-
setOpen(true);
|
|
7659
|
-
}, justifyContent: 'start', children: [jsx(MdDateRange, {}), displayDate || ''] }) }), insideDialog ? (jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", minW: "450px", minH: "25rem", children: jsx(Popover.Body, { children: dateTimePickerContent }) }) })) : (jsx(Portal, { children: jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", minW: "450px", minH: "25rem", children: jsx(Popover.Body, { children: dateTimePickerContent }) }) }) }))] }) }));
|
|
8511
|
+
gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: dateTimePickerContent }));
|
|
7660
8512
|
};
|
|
7661
8513
|
|
|
7662
8514
|
const SchemaRenderer = ({ schema, prefix, column, }) => {
|
|
@@ -7777,7 +8629,7 @@ const BooleanViewer = ({ schema, column, prefix, }) => {
|
|
|
7777
8629
|
const value = watch(colLabel);
|
|
7778
8630
|
const formI18n = useFormI18n(column, prefix, schema);
|
|
7779
8631
|
return (jsxs(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
7780
|
-
gridRow, children: [jsx(Text, { children: value ?
|
|
8632
|
+
gridRow, children: [jsx(Text, { children: value ? 'True' : 'False' }), errors[`${column}`] && (jsx(Text, { color: 'red.400', children: formI18n.required() }))] }));
|
|
7781
8633
|
};
|
|
7782
8634
|
|
|
7783
8635
|
const CustomViewer = ({ column, schema, prefix }) => {
|
|
@@ -7809,23 +8661,22 @@ const DateViewer = ({ column, schema, prefix }) => {
|
|
|
7809
8661
|
|
|
7810
8662
|
const EnumViewer = ({ column, isMultiple = false, schema, prefix, }) => {
|
|
7811
8663
|
const { watch, formState: { errors }, } = useFormContext();
|
|
7812
|
-
const formI18n = useFormI18n(column, prefix);
|
|
8664
|
+
const formI18n = useFormI18n(column, prefix, schema);
|
|
7813
8665
|
const { required } = schema;
|
|
7814
8666
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
7815
|
-
const { gridColumn =
|
|
8667
|
+
const { gridColumn = 'span 12', gridRow = 'span 1', renderDisplay } = schema;
|
|
7816
8668
|
const colLabel = formI18n.colLabel;
|
|
7817
8669
|
const watchEnum = watch(colLabel);
|
|
7818
8670
|
const watchEnums = (watch(colLabel) ?? []);
|
|
7819
|
-
|
|
7820
|
-
|
|
8671
|
+
const renderDisplayFunction = renderDisplay || defaultRenderDisplay;
|
|
8672
|
+
return (jsxs(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
8673
|
+
gridRow, children: [isMultiple && (jsx(Flex, { flexFlow: 'wrap', gap: 1, children: watchEnums.map((enumValue) => {
|
|
7821
8674
|
const item = enumValue;
|
|
7822
8675
|
if (item === undefined) {
|
|
7823
8676
|
return jsx(Fragment, { children: "undefined" });
|
|
7824
8677
|
}
|
|
7825
|
-
return (jsx(Tag, { size: "lg", children:
|
|
7826
|
-
|
|
7827
|
-
: formI18n.t(item) }, item));
|
|
7828
|
-
}) })), !isMultiple && jsx(Text, { children: formI18n.t(watchEnum) }), errors[`${column}`] && (jsx(Text, { color: "red.400", children: formI18n.required() }))] }));
|
|
8678
|
+
return (jsx(Tag, { size: "lg", children: renderDisplayFunction(item) }, item));
|
|
8679
|
+
}) })), !isMultiple && jsx(Text, { children: renderDisplayFunction(watchEnum) }), errors[`${column}`] && (jsx(Text, { color: 'red.400', children: formI18n.required() }))] }));
|
|
7829
8680
|
};
|
|
7830
8681
|
|
|
7831
8682
|
const FileViewer = ({ column, schema, prefix }) => {
|
|
@@ -7945,31 +8796,35 @@ const StringViewer = ({ column, schema, prefix, }) => {
|
|
|
7945
8796
|
|
|
7946
8797
|
const TagViewer = ({ column, schema, prefix }) => {
|
|
7947
8798
|
const { watch, formState: { errors }, setValue, } = useFormContext();
|
|
7948
|
-
const { serverUrl } = useSchemaContext();
|
|
7949
8799
|
if (schema.properties == undefined) {
|
|
7950
|
-
throw new Error(
|
|
8800
|
+
throw new Error('schema properties undefined when using DatePicker');
|
|
7951
8801
|
}
|
|
7952
|
-
const { gridColumn, gridRow, in_table, object_id_column } = schema;
|
|
8802
|
+
const { gridColumn, gridRow, in_table, object_id_column, tagPicker } = schema;
|
|
7953
8803
|
if (in_table === undefined) {
|
|
7954
|
-
throw new Error(
|
|
8804
|
+
throw new Error('in_table is undefined when using TagPicker');
|
|
7955
8805
|
}
|
|
7956
8806
|
if (object_id_column === undefined) {
|
|
7957
|
-
throw new Error(
|
|
8807
|
+
throw new Error('object_id_column is undefined when using TagPicker');
|
|
8808
|
+
}
|
|
8809
|
+
if (!tagPicker?.queryFn) {
|
|
8810
|
+
throw new Error('tagPicker.queryFn is required in schema. serverUrl has been removed.');
|
|
7958
8811
|
}
|
|
7959
8812
|
const query = useQuery({
|
|
7960
8813
|
queryKey: [`tagpicker`, in_table],
|
|
7961
8814
|
queryFn: async () => {
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
in_table: "tables_tags_view",
|
|
8815
|
+
const result = await tagPicker.queryFn({
|
|
8816
|
+
in_table: 'tables_tags_view',
|
|
7965
8817
|
where: [
|
|
7966
8818
|
{
|
|
7967
|
-
id:
|
|
8819
|
+
id: 'table_name',
|
|
7968
8820
|
value: [in_table],
|
|
7969
8821
|
},
|
|
7970
8822
|
],
|
|
7971
8823
|
limit: 100,
|
|
8824
|
+
offset: 0,
|
|
8825
|
+
searching: '',
|
|
7972
8826
|
});
|
|
8827
|
+
return result.data || { data: [] };
|
|
7973
8828
|
},
|
|
7974
8829
|
staleTime: 10000,
|
|
7975
8830
|
});
|
|
@@ -7977,17 +8832,19 @@ const TagViewer = ({ column, schema, prefix }) => {
|
|
|
7977
8832
|
const existingTagsQuery = useQuery({
|
|
7978
8833
|
queryKey: [`existing`, { in_table, object_id_column }, object_id],
|
|
7979
8834
|
queryFn: async () => {
|
|
7980
|
-
|
|
7981
|
-
serverUrl,
|
|
8835
|
+
const result = await tagPicker.queryFn({
|
|
7982
8836
|
in_table: in_table,
|
|
7983
8837
|
where: [
|
|
7984
8838
|
{
|
|
7985
8839
|
id: object_id_column,
|
|
7986
|
-
value: object_id[0],
|
|
8840
|
+
value: [object_id[0]],
|
|
7987
8841
|
},
|
|
7988
8842
|
],
|
|
7989
8843
|
limit: 100,
|
|
8844
|
+
offset: 0,
|
|
8845
|
+
searching: '',
|
|
7990
8846
|
});
|
|
8847
|
+
return result.data || { data: [] };
|
|
7991
8848
|
},
|
|
7992
8849
|
enabled: object_id != undefined,
|
|
7993
8850
|
staleTime: 10000,
|
|
@@ -7998,9 +8855,9 @@ const TagViewer = ({ column, schema, prefix }) => {
|
|
|
7998
8855
|
if (!!object_id === false) {
|
|
7999
8856
|
return jsx(Fragment, {});
|
|
8000
8857
|
}
|
|
8001
|
-
return (jsxs(Flex, { flexFlow:
|
|
8858
|
+
return (jsxs(Flex, { flexFlow: 'column', gap: 4, gridColumn,
|
|
8002
8859
|
gridRow, children: [isFetching && jsx(Fragment, { children: "isFetching" }), isLoading && jsx(Fragment, { children: "isLoading" }), isPending && jsx(Fragment, { children: "isPending" }), isError && jsx(Fragment, { children: "isError" }), dataList.map(({ parent_tag_name, all_tags, is_mutually_exclusive }) => {
|
|
8003
|
-
return (jsxs(Flex, { flexFlow:
|
|
8860
|
+
return (jsxs(Flex, { flexFlow: 'column', gap: 2, children: [jsx(Text, { children: parent_tag_name }), is_mutually_exclusive && (jsx(RadioCardRoot, { defaultValue: "next", variant: 'surface', onValueChange: (tagIds) => {
|
|
8004
8861
|
const existedTags = Object.values(all_tags)
|
|
8005
8862
|
.filter(({ id }) => {
|
|
8006
8863
|
return existingTagList.some(({ tag_id }) => tag_id === id);
|
|
@@ -8012,20 +8869,20 @@ const TagViewer = ({ column, schema, prefix }) => {
|
|
|
8012
8869
|
tagIds.value,
|
|
8013
8870
|
]);
|
|
8014
8871
|
setValue(`${column}.${parent_tag_name}.old`, existedTags);
|
|
8015
|
-
}, children: jsx(Flex, { flexFlow:
|
|
8872
|
+
}, children: jsx(Flex, { flexFlow: 'wrap', gap: 2, children: Object.entries(all_tags).map(([tagName, { id }]) => {
|
|
8016
8873
|
if (existingTagList.some(({ tag_id }) => tag_id === id)) {
|
|
8017
|
-
return (jsx(RadioCardItem, { label: tagName, value: id, flex:
|
|
8874
|
+
return (jsx(RadioCardItem, { label: tagName, value: id, flex: '0 0 0%', disabled: true }, `${tagName}-${id}`));
|
|
8018
8875
|
}
|
|
8019
|
-
return (jsx(RadioCardItem, { label: tagName, value: id, flex:
|
|
8876
|
+
return (jsx(RadioCardItem, { label: tagName, value: id, flex: '0 0 0%', colorPalette: 'blue' }, `${tagName}-${id}`));
|
|
8020
8877
|
}) }) })), !is_mutually_exclusive && (jsx(CheckboxGroup, { onValueChange: (tagIds) => {
|
|
8021
8878
|
setValue(`${column}.${parent_tag_name}.current`, tagIds);
|
|
8022
|
-
}, children: jsx(Flex, { flexFlow:
|
|
8879
|
+
}, children: jsx(Flex, { flexFlow: 'wrap', gap: 2, children: Object.entries(all_tags).map(([tagName, { id }]) => {
|
|
8023
8880
|
if (existingTagList.some(({ tag_id }) => tag_id === id)) {
|
|
8024
|
-
return (jsx(CheckboxCard, { label: tagName, value: id, flex:
|
|
8881
|
+
return (jsx(CheckboxCard, { label: tagName, value: id, flex: '0 0 0%', disabled: true, colorPalette: 'blue' }, `${tagName}-${id}`));
|
|
8025
8882
|
}
|
|
8026
|
-
return (jsx(CheckboxCard, { label: tagName, value: id, flex:
|
|
8883
|
+
return (jsx(CheckboxCard, { label: tagName, value: id, flex: '0 0 0%' }, `${tagName}-${id}`));
|
|
8027
8884
|
}) }) }))] }, `tag-${parent_tag_name}`));
|
|
8028
|
-
}), errors[`${column}`] && (jsx(Text, { color:
|
|
8885
|
+
}), errors[`${column}`] && (jsx(Text, { color: 'red.400', children: (errors[`${column}`]?.message ?? 'No error message') }))] }));
|
|
8029
8886
|
};
|
|
8030
8887
|
|
|
8031
8888
|
const TextAreaViewer = ({ column, schema, prefix, }) => {
|
|
@@ -8232,6 +9089,17 @@ const FormBody = () => {
|
|
|
8232
9089
|
|
|
8233
9090
|
const FormTitle = () => {
|
|
8234
9091
|
const { schema } = useSchemaContext();
|
|
9092
|
+
// Debug log when form title is missing
|
|
9093
|
+
if (!schema.title) {
|
|
9094
|
+
console.debug('[Form Title] Missing title in root schema. Add title property to schema.', {
|
|
9095
|
+
schema: {
|
|
9096
|
+
type: schema.type,
|
|
9097
|
+
properties: schema.properties
|
|
9098
|
+
? Object.keys(schema.properties)
|
|
9099
|
+
: undefined,
|
|
9100
|
+
},
|
|
9101
|
+
});
|
|
9102
|
+
}
|
|
8235
9103
|
return jsx(Heading, { children: schema.title ?? 'Form' });
|
|
8236
9104
|
};
|
|
8237
9105
|
|
|
@@ -9395,4 +10263,4 @@ function DataTableServer({ columns, enableRowSelection = true, enableMultiRowSel
|
|
|
9395
10263
|
}, children: jsx(DataTableServerContext.Provider, { value: { url: url ?? '', query }, children: children }) }));
|
|
9396
10264
|
}
|
|
9397
10265
|
|
|
9398
|
-
export { CalendarDisplay, CardHeader, DataDisplay, DataTable, DataTableServer, DatePickerInput, DefaultCardTitle, DefaultForm, DefaultTable, DefaultTableServer, DensityToggleButton, EditSortingButton, EmptyState, ErrorAlert, FilterDialog, FormBody, FormRoot, FormTitle, GlobalFilter, MediaLibraryBrowser, PageSizeControl, Pagination, RecordDisplay, ReloadButton, ResetFilteringButton, ResetSelectionButton, ResetSortingButton, RowCountText, SelectAllRowsToggle, Table, TableBody, TableCardContainer, TableCards, TableComponent, TableControls, TableDataDisplay, TableFilter, TableFilterTags, TableFooter, TableHeader, TableLoadingComponent, TableSelector, TableSorter, TableViewer, TextCell, ViewDialog, buildErrorMessages, buildFieldErrors, buildRequiredErrors, convertToAjvErrorsFormat, createErrorMessage, defaultRenderDisplay, getColumns, getMultiDates, getRangeDates, idPickerSanityCheck, useDataTable, useDataTableContext, useDataTableServer, useForm, widthSanityCheck };
|
|
10266
|
+
export { CalendarDisplay, CardHeader, DataDisplay, DataTable, DataTableServer, DatePickerContext, DatePickerInput, DefaultCardTitle, DefaultForm, DefaultTable, DefaultTableServer, DensityToggleButton, EditSortingButton, EmptyState, ErrorAlert, FilterDialog, FormBody, FormRoot, FormTitle, GlobalFilter, MediaLibraryBrowser, PageSizeControl, Pagination, RecordDisplay, ReloadButton, ResetFilteringButton, ResetSelectionButton, ResetSortingButton, RowCountText, SelectAllRowsToggle, Table, TableBody, TableCardContainer, TableCards, TableComponent, TableControls, TableDataDisplay, TableFilter, TableFilterTags, TableFooter, TableHeader, TableLoadingComponent, TableSelector, TableSorter, TableViewer, TextCell, ViewDialog, buildErrorMessages, buildFieldErrors, buildRequiredErrors, convertToAjvErrorsFormat, createErrorMessage, defaultRenderDisplay, getColumns, getMultiDates, getRangeDates, idPickerSanityCheck, useDataTable, useDataTableContext, useDataTableServer, useForm, widthSanityCheck };
|