@bsol-oss/react-datatable5 13.0.1-beta.1 → 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 +2110 -1270
- package/dist/index.mjs +2113 -1274
- 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 +7 -23
- 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,106 +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
|
-
/**
|
|
5635
|
-
* Load initial values for IdPicker fields into idMap
|
|
5636
|
-
* Uses customQueryFn if available, otherwise falls back to getTableData
|
|
5637
|
-
*
|
|
5638
|
-
* @param params - Configuration for loading initial values
|
|
5639
|
-
* @returns Promise with fetched data and idMap
|
|
5640
|
-
*/
|
|
5641
|
-
const loadInitialValues = async ({ ids, foreign_key, serverUrl, setIdMap, }) => {
|
|
5642
|
-
if (!ids || ids.length === 0) {
|
|
5643
|
-
return { data: { data: [], count: 0 }, idMap: {} };
|
|
5644
|
-
}
|
|
5645
|
-
const { table, column: column_ref, customQueryFn } = foreign_key;
|
|
5646
|
-
// Filter out IDs that are already in idMap (optional optimization)
|
|
5647
|
-
// For now, we'll fetch all requested IDs to ensure consistency
|
|
5648
|
-
if (customQueryFn) {
|
|
5649
|
-
const { data, idMap: returnedIdMap } = await customQueryFn({
|
|
5650
|
-
searching: '',
|
|
5651
|
-
limit: ids.length,
|
|
5652
|
-
offset: 0,
|
|
5653
|
-
where: [
|
|
5654
|
-
{
|
|
5655
|
-
id: column_ref,
|
|
5656
|
-
value: ids.length === 1 ? ids[0] : ids, // CustomQueryFn accepts string | string[]
|
|
5657
|
-
},
|
|
5658
|
-
],
|
|
5659
|
-
});
|
|
5660
|
-
// Update idMap with returned values
|
|
5661
|
-
if (returnedIdMap && Object.keys(returnedIdMap).length > 0) {
|
|
5662
|
-
setIdMap((state) => {
|
|
5663
|
-
return { ...state, ...returnedIdMap };
|
|
5664
|
-
});
|
|
5665
|
-
}
|
|
5666
|
-
return { data, idMap: returnedIdMap || {} };
|
|
5667
|
-
}
|
|
5668
|
-
// Fallback to default getTableData
|
|
5669
|
-
const data = await getTableData({
|
|
5670
|
-
serverUrl,
|
|
5671
|
-
searching: '',
|
|
5672
|
-
in_table: table,
|
|
5673
|
-
limit: ids.length,
|
|
5674
|
-
offset: 0,
|
|
5675
|
-
where: [
|
|
5676
|
-
{
|
|
5677
|
-
id: column_ref,
|
|
5678
|
-
value: ids, // Always pass as array
|
|
5679
|
-
},
|
|
5680
|
-
],
|
|
5681
|
-
});
|
|
5682
|
-
// Build idMap from fetched data
|
|
5683
|
-
const newMap = Object.fromEntries((data ?? { data: [] }).data.map((item) => {
|
|
5684
|
-
return [
|
|
5685
|
-
item[column_ref],
|
|
5686
|
-
{
|
|
5687
|
-
...item,
|
|
5688
|
-
},
|
|
5689
|
-
];
|
|
5690
|
-
}));
|
|
5691
|
-
// Update idMap state
|
|
5692
|
-
setIdMap((state) => {
|
|
5693
|
-
return { ...state, ...newMap };
|
|
5694
|
-
});
|
|
5695
|
-
return { data: data, idMap: newMap };
|
|
5696
|
-
};
|
|
5697
5992
|
const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
|
|
5698
5993
|
const { watch, getValues, formState: { errors }, setValue, } = useFormContext();
|
|
5699
|
-
const {
|
|
5700
|
-
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
|
+
}
|
|
5701
6001
|
const { table, column: column_ref, customQueryFn, } = foreign_key;
|
|
5702
6002
|
const [searchText, setSearchText] = useState('');
|
|
5703
6003
|
const [debouncedSearchText, setDebouncedSearchText] = useState('');
|
|
@@ -5742,60 +6042,58 @@ const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
|
|
|
5742
6042
|
const missingIdsKey = useMemo(() => {
|
|
5743
6043
|
return JSON.stringify([...missingIds].sort());
|
|
5744
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]);
|
|
5745
6052
|
// Query to fetch initial values that are missing from idMap
|
|
5746
6053
|
// This query runs automatically when missingIds.length > 0 and updates idMap
|
|
5747
6054
|
const initialValuesQuery = useQuery({
|
|
5748
|
-
queryKey: [`idpicker-initial`, column, missingIdsKey],
|
|
6055
|
+
queryKey: [`idpicker-initial`, column, missingIdsKey, idMapStateKey],
|
|
5749
6056
|
queryFn: async () => {
|
|
5750
6057
|
if (missingIds.length === 0) {
|
|
5751
6058
|
return { data: [], count: 0 };
|
|
5752
6059
|
}
|
|
5753
|
-
// Use
|
|
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 };
|
|
6064
|
+
}
|
|
5754
6065
|
const result = await loadInitialValues({
|
|
5755
6066
|
ids: missingIds,
|
|
5756
6067
|
foreign_key: foreign_key,
|
|
5757
|
-
serverUrl,
|
|
5758
6068
|
setIdMap,
|
|
5759
6069
|
});
|
|
5760
6070
|
return result.data;
|
|
5761
6071
|
},
|
|
5762
6072
|
enabled: missingIds.length > 0, // Only fetch if there are missing IDs
|
|
5763
|
-
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
|
|
5764
6076
|
});
|
|
5765
6077
|
const { isLoading: isLoadingInitialValues, isFetching: isFetchingInitialValues, } = initialValuesQuery;
|
|
5766
6078
|
// Query for search results (async loading)
|
|
5767
6079
|
const query = useQuery({
|
|
5768
6080
|
queryKey: [`idpicker`, { column, searchText: debouncedSearchText, limit }],
|
|
5769
6081
|
queryFn: async () => {
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
limit: limit,
|
|
5774
|
-
offset: 0,
|
|
5775
|
-
});
|
|
5776
|
-
setIdMap((state) => {
|
|
5777
|
-
return { ...state, ...idMap };
|
|
5778
|
-
});
|
|
5779
|
-
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.`);
|
|
5780
6085
|
}
|
|
5781
|
-
const data = await
|
|
5782
|
-
serverUrl,
|
|
6086
|
+
const { data, idMap } = await customQueryFn({
|
|
5783
6087
|
searching: debouncedSearchText ?? '',
|
|
5784
|
-
in_table: table,
|
|
5785
6088
|
limit: limit,
|
|
5786
6089
|
offset: 0,
|
|
5787
6090
|
});
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
{
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
];
|
|
5795
|
-
}));
|
|
5796
|
-
setIdMap((state) => {
|
|
5797
|
-
return { ...state, ...newMap };
|
|
5798
|
-
});
|
|
6091
|
+
// Update idMap with returned values
|
|
6092
|
+
if (idMap && Object.keys(idMap).length > 0) {
|
|
6093
|
+
setIdMap((state) => {
|
|
6094
|
+
return { ...state, ...idMap };
|
|
6095
|
+
});
|
|
6096
|
+
}
|
|
5799
6097
|
return data;
|
|
5800
6098
|
},
|
|
5801
6099
|
enabled: true, // Always enabled for combobox
|
|
@@ -5827,17 +6125,51 @@ const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
|
|
|
5827
6125
|
// Depend on idMapKey which only changes when items we care about change
|
|
5828
6126
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5829
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
|
+
};
|
|
5830
6147
|
// Transform data for combobox collection
|
|
5831
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)
|
|
5832
6150
|
// raw item is stored for custom rendering
|
|
5833
6151
|
// Also include items from idMap that match currentValue (for initial values display)
|
|
5834
6152
|
const comboboxItems = useMemo(() => {
|
|
5835
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
|
+
};
|
|
5836
6166
|
const itemsFromDataList = dataList.map((item) => {
|
|
5837
6167
|
const rendered = renderFn(item);
|
|
6168
|
+
const label = typeof rendered === 'string' ? rendered : JSON.stringify(item); // Use string for filtering
|
|
5838
6169
|
return {
|
|
5839
|
-
label
|
|
5840
|
-
|
|
6170
|
+
label, // Use string for filtering
|
|
6171
|
+
displayLabel: getDisplayString(rendered, label), // String representation for input display
|
|
6172
|
+
value: itemToValueFn(item),
|
|
5841
6173
|
raw: item,
|
|
5842
6174
|
};
|
|
5843
6175
|
});
|
|
@@ -5846,25 +6178,28 @@ const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
|
|
|
5846
6178
|
const itemsFromIdMap = idMapItems
|
|
5847
6179
|
.map((item) => {
|
|
5848
6180
|
// Check if this item is already in itemsFromDataList
|
|
5849
|
-
const alreadyIncluded = itemsFromDataList.some((i) => i.value ===
|
|
6181
|
+
const alreadyIncluded = itemsFromDataList.some((i) => i.value === itemToValueFn(item));
|
|
5850
6182
|
if (alreadyIncluded)
|
|
5851
6183
|
return null;
|
|
5852
6184
|
const rendered = renderFn(item);
|
|
6185
|
+
const label = typeof rendered === 'string' ? rendered : JSON.stringify(item);
|
|
5853
6186
|
return {
|
|
5854
|
-
label
|
|
5855
|
-
|
|
6187
|
+
label,
|
|
6188
|
+
displayLabel: getDisplayString(rendered, label), // String representation for input display
|
|
6189
|
+
value: itemToValueFn(item),
|
|
5856
6190
|
raw: item,
|
|
5857
6191
|
};
|
|
5858
6192
|
})
|
|
5859
6193
|
.filter((item) => item !== null);
|
|
5860
6194
|
return [...itemsFromIdMap, ...itemsFromDataList];
|
|
5861
|
-
}, [dataList, column_ref, renderDisplay, idMapItems]);
|
|
6195
|
+
}, [dataList, column_ref, renderDisplay, idMapItems, itemToValueFn]);
|
|
5862
6196
|
// Use filter hook for combobox
|
|
5863
6197
|
const { contains } = useFilter({ sensitivity: 'base' });
|
|
5864
6198
|
// Create collection for combobox
|
|
6199
|
+
// itemToString uses displayLabel to show rendered display in input when selected
|
|
5865
6200
|
const { collection, filter, set } = useListCollection({
|
|
5866
6201
|
initialItems: comboboxItems,
|
|
5867
|
-
itemToString: (item) => item.
|
|
6202
|
+
itemToString: (item) => item.displayLabel, // Use displayLabel for selected value display
|
|
5868
6203
|
itemToValue: (item) => item.value,
|
|
5869
6204
|
filter: contains,
|
|
5870
6205
|
});
|
|
@@ -5919,6 +6254,10 @@ const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
|
|
|
5919
6254
|
idPickerLabels,
|
|
5920
6255
|
insideDialog: insideDialog ?? false,
|
|
5921
6256
|
renderDisplay,
|
|
6257
|
+
itemToValue: itemToValueFn,
|
|
6258
|
+
itemToString: itemToStringFn,
|
|
6259
|
+
loadInitialValues: loadInitialValues ??
|
|
6260
|
+
(async () => ({ data: { data: [], count: 0 }, idMap: {} })), // Fallback if not provided
|
|
5922
6261
|
column_ref,
|
|
5923
6262
|
errors,
|
|
5924
6263
|
setValue,
|
|
@@ -5927,60 +6266,69 @@ const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
|
|
|
5927
6266
|
|
|
5928
6267
|
const IdPickerSingle = ({ column, schema, prefix, }) => {
|
|
5929
6268
|
const formI18n = useFormI18n(column, prefix, schema);
|
|
5930
|
-
const { required, gridColumn = 'span 12', gridRow = 'span 1'
|
|
6269
|
+
const { required, gridColumn = 'span 12', gridRow = 'span 1' } = schema;
|
|
5931
6270
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
5932
|
-
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({
|
|
5933
6272
|
column,
|
|
5934
6273
|
schema,
|
|
5935
6274
|
prefix,
|
|
5936
6275
|
isMultiple: false,
|
|
5937
6276
|
});
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
const
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
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 ? (
|
|
5965
6322
|
// Show skeleton items to prevent UI shift
|
|
5966
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
|
|
5967
6324
|
? idPickerLabels?.emptySearchResult ?? 'No results found'
|
|
5968
6325
|
: idPickerLabels?.initialResults ??
|
|
5969
|
-
'Start typing to search' })) : (jsx(Fragment, { children: collection.items.map((item, index) => (
|
|
5970
|
-
? renderDisplayFunction(item.raw)
|
|
5971
|
-
: 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 ? (
|
|
5972
|
-
// Show skeleton items to prevent UI shift
|
|
5973
|
-
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
|
|
5974
|
-
? idPickerLabels?.emptySearchResult ?? 'No results found'
|
|
5975
|
-
: idPickerLabels?.initialResults ??
|
|
5976
|
-
'Start typing to search' })) : (jsx(Fragment, { children: collection.items.map((item, index) => (jsxs(Combobox.Item, { item: item, children: [jsx(Combobox.ItemText, { children: !!renderDisplayFunction === true
|
|
5977
|
-
? renderDisplayFunction(item.raw)
|
|
5978
|
-
: 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}`))) })) }) }) }))] }) }));
|
|
5979
6327
|
};
|
|
5980
6328
|
|
|
5981
6329
|
const IdPickerMultiple = ({ column, schema, prefix, }) => {
|
|
5982
6330
|
const formI18n = useFormI18n(column, prefix, schema);
|
|
5983
|
-
const { required, gridColumn = 'span 12', gridRow = 'span 1'
|
|
6331
|
+
const { required, gridColumn = 'span 12', gridRow = 'span 1' } = schema;
|
|
5984
6332
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
5985
6333
|
const { colLabel, currentValue, searchText, setSearchText, isLoading, isFetching, isPending, isError, isSearching, isLoadingInitialValues, isFetchingInitialValues, missingIds, collection, idMap, idPickerLabels, insideDialog, renderDisplay: renderDisplayFn, errors, setValue, } = useIdPickerData({
|
|
5986
6334
|
column,
|
|
@@ -5994,7 +6342,8 @@ const IdPickerMultiple = ({ column, schema, prefix, }) => {
|
|
|
5994
6342
|
const handleValueChange = (details) => {
|
|
5995
6343
|
setValue(colLabel, details.value);
|
|
5996
6344
|
};
|
|
5997
|
-
|
|
6345
|
+
// Use renderDisplay from hook (which comes from schema) or fallback to default
|
|
6346
|
+
const renderDisplayFunction = renderDisplayFn || defaultRenderDisplay;
|
|
5998
6347
|
return (jsxs(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
5999
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) => {
|
|
6000
6349
|
const item = idMap[id];
|
|
@@ -6019,16 +6368,12 @@ const IdPickerMultiple = ({ column, schema, prefix, }) => {
|
|
|
6019
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
|
|
6020
6369
|
? idPickerLabels?.emptySearchResult ?? 'No results found'
|
|
6021
6370
|
: idPickerLabels?.initialResults ??
|
|
6022
|
-
'Start typing to search' })) : (jsx(Fragment, { children: collection.items.map((item, index) => (jsxs(Combobox.Item, { item: item, children: [jsx(Combobox.
|
|
6023
|
-
? renderDisplayFunction(item.raw)
|
|
6024
|
-
: 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 ? (
|
|
6025
6372
|
// Show skeleton items to prevent UI shift
|
|
6026
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
|
|
6027
6374
|
? idPickerLabels?.emptySearchResult ?? 'No results found'
|
|
6028
6375
|
: idPickerLabels?.initialResults ??
|
|
6029
|
-
'Start typing to search' })) : (jsx(Fragment, { children: collection.items.map((item, index) => (jsxs(Combobox.Item, { item: item, children: [jsx(Combobox.
|
|
6030
|
-
? renderDisplayFunction(item.raw)
|
|
6031
|
-
: 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}`))) })) }) }) }))] })] }));
|
|
6032
6377
|
};
|
|
6033
6378
|
|
|
6034
6379
|
const NumberInputRoot = React.forwardRef(function NumberInput$1(props, ref) {
|
|
@@ -6212,31 +6557,35 @@ RadioCard.ItemIndicator;
|
|
|
6212
6557
|
|
|
6213
6558
|
const TagPicker = ({ column, schema, prefix }) => {
|
|
6214
6559
|
const { watch, formState: { errors }, setValue, } = useFormContext();
|
|
6215
|
-
const { serverUrl } = useSchemaContext();
|
|
6216
6560
|
if (schema.properties == undefined) {
|
|
6217
|
-
throw new Error(
|
|
6561
|
+
throw new Error('schema properties undefined when using DatePicker');
|
|
6218
6562
|
}
|
|
6219
|
-
const { gridColumn, gridRow, in_table, object_id_column } = schema;
|
|
6563
|
+
const { gridColumn, gridRow, in_table, object_id_column, tagPicker } = schema;
|
|
6220
6564
|
if (in_table === undefined) {
|
|
6221
|
-
throw new Error(
|
|
6565
|
+
throw new Error('in_table is undefined when using TagPicker');
|
|
6222
6566
|
}
|
|
6223
6567
|
if (object_id_column === undefined) {
|
|
6224
|
-
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.');
|
|
6225
6572
|
}
|
|
6226
6573
|
const query = useQuery({
|
|
6227
6574
|
queryKey: [`tagpicker`, in_table],
|
|
6228
6575
|
queryFn: async () => {
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
in_table: "tables_tags_view",
|
|
6576
|
+
const result = await tagPicker.queryFn({
|
|
6577
|
+
in_table: 'tables_tags_view',
|
|
6232
6578
|
where: [
|
|
6233
6579
|
{
|
|
6234
|
-
id:
|
|
6580
|
+
id: 'table_name',
|
|
6235
6581
|
value: [in_table],
|
|
6236
6582
|
},
|
|
6237
6583
|
],
|
|
6238
6584
|
limit: 100,
|
|
6585
|
+
offset: 0,
|
|
6586
|
+
searching: '',
|
|
6239
6587
|
});
|
|
6588
|
+
return result.data || { data: [] };
|
|
6240
6589
|
},
|
|
6241
6590
|
staleTime: 10000,
|
|
6242
6591
|
});
|
|
@@ -6244,17 +6593,19 @@ const TagPicker = ({ column, schema, prefix }) => {
|
|
|
6244
6593
|
const existingTagsQuery = useQuery({
|
|
6245
6594
|
queryKey: [`existing`, { in_table, object_id_column }, object_id],
|
|
6246
6595
|
queryFn: async () => {
|
|
6247
|
-
|
|
6248
|
-
serverUrl,
|
|
6596
|
+
const result = await tagPicker.queryFn({
|
|
6249
6597
|
in_table: in_table,
|
|
6250
6598
|
where: [
|
|
6251
6599
|
{
|
|
6252
6600
|
id: object_id_column,
|
|
6253
|
-
value: object_id[0],
|
|
6601
|
+
value: [object_id[0]],
|
|
6254
6602
|
},
|
|
6255
6603
|
],
|
|
6256
6604
|
limit: 100,
|
|
6605
|
+
offset: 0,
|
|
6606
|
+
searching: '',
|
|
6257
6607
|
});
|
|
6608
|
+
return result.data || { data: [] };
|
|
6258
6609
|
},
|
|
6259
6610
|
enabled: object_id != undefined,
|
|
6260
6611
|
staleTime: 10000,
|
|
@@ -6265,9 +6616,9 @@ const TagPicker = ({ column, schema, prefix }) => {
|
|
|
6265
6616
|
if (!!object_id === false) {
|
|
6266
6617
|
return jsx(Fragment, {});
|
|
6267
6618
|
}
|
|
6268
|
-
return (jsxs(Flex, { flexFlow:
|
|
6619
|
+
return (jsxs(Flex, { flexFlow: 'column', gap: 4, gridColumn,
|
|
6269
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 }) => {
|
|
6270
|
-
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) => {
|
|
6271
6622
|
const existedTags = Object.values(all_tags)
|
|
6272
6623
|
.filter(({ id }) => {
|
|
6273
6624
|
return existingTagList.some(({ tag_id }) => tag_id === id);
|
|
@@ -6279,20 +6630,20 @@ const TagPicker = ({ column, schema, prefix }) => {
|
|
|
6279
6630
|
tagIds.value,
|
|
6280
6631
|
]);
|
|
6281
6632
|
setValue(`${column}.${parent_tag_name}.old`, existedTags);
|
|
6282
|
-
}, children: jsx(Flex, { flexFlow:
|
|
6633
|
+
}, children: jsx(Flex, { flexFlow: 'wrap', gap: 2, children: Object.entries(all_tags).map(([tagName, { id }]) => {
|
|
6283
6634
|
if (existingTagList.some(({ tag_id }) => tag_id === id)) {
|
|
6284
|
-
return (jsx(RadioCardItem, { label: tagName, value: id, flex:
|
|
6635
|
+
return (jsx(RadioCardItem, { label: tagName, value: id, flex: '0 0 0%', disabled: true }, `${tagName}-${id}`));
|
|
6285
6636
|
}
|
|
6286
|
-
return (jsx(RadioCardItem, { label: tagName, value: id, flex:
|
|
6637
|
+
return (jsx(RadioCardItem, { label: tagName, value: id, flex: '0 0 0%', colorPalette: 'blue' }, `${tagName}-${id}`));
|
|
6287
6638
|
}) }) })), !is_mutually_exclusive && (jsx(CheckboxGroup, { onValueChange: (tagIds) => {
|
|
6288
6639
|
setValue(`${column}.${parent_tag_name}.current`, tagIds);
|
|
6289
|
-
}, children: jsx(Flex, { flexFlow:
|
|
6640
|
+
}, children: jsx(Flex, { flexFlow: 'wrap', gap: 2, children: Object.entries(all_tags).map(([tagName, { id }]) => {
|
|
6290
6641
|
if (existingTagList.some(({ tag_id }) => tag_id === id)) {
|
|
6291
|
-
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}`));
|
|
6292
6643
|
}
|
|
6293
|
-
return (jsx(CheckboxCard, { label: tagName, value: id, flex:
|
|
6644
|
+
return (jsx(CheckboxCard, { label: tagName, value: id, flex: '0 0 0%' }, `${tagName}-${id}`));
|
|
6294
6645
|
}) }) }))] }, `tag-${parent_tag_name}`));
|
|
6295
|
-
}), errors[`${column}`] && (jsx(Text, { color:
|
|
6646
|
+
}), errors[`${column}`] && (jsx(Text, { color: 'red.400', children: (errors[`${column}`]?.message ?? 'No error message') }))] }));
|
|
6296
6647
|
};
|
|
6297
6648
|
|
|
6298
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) => {
|
|
@@ -6399,11 +6750,193 @@ const TextAreaInput = ({ column, schema, prefix, }) => {
|
|
|
6399
6750
|
|
|
6400
6751
|
dayjs.extend(utc);
|
|
6401
6752
|
dayjs.extend(timezone);
|
|
6402
|
-
const TimePicker$1 = (
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
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
|
|
6407
6940
|
const timeOptions = useMemo(() => {
|
|
6408
6941
|
const options = [];
|
|
6409
6942
|
// Get start time for comparison if provided
|
|
@@ -6414,32 +6947,25 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
|
|
|
6414
6947
|
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6415
6948
|
if (startDateObj.isValid() && selectedDateObj.isValid()) {
|
|
6416
6949
|
startDateTime = startDateObj;
|
|
6417
|
-
// Only filter if dates are the same
|
|
6418
6950
|
shouldFilterByDate =
|
|
6419
6951
|
startDateObj.format('YYYY-MM-DD') ===
|
|
6420
6952
|
selectedDateObj.format('YYYY-MM-DD');
|
|
6421
6953
|
}
|
|
6422
6954
|
}
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
for (let
|
|
6426
|
-
for (
|
|
6427
|
-
//
|
|
6428
|
-
let hour24 = h;
|
|
6429
|
-
if (mer === 'am' && h === 12)
|
|
6430
|
-
hour24 = 0;
|
|
6431
|
-
else if (mer === 'pm' && h < 12)
|
|
6432
|
-
hour24 = h + 12;
|
|
6433
|
-
// 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
|
|
6434
6960
|
if (startDateTime && selectedDate && shouldFilterByDate) {
|
|
6435
6961
|
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6436
6962
|
const optionDateTime = selectedDateObj
|
|
6437
|
-
.hour(
|
|
6963
|
+
.hour(h)
|
|
6438
6964
|
.minute(m)
|
|
6439
6965
|
.second(0)
|
|
6440
6966
|
.millisecond(0);
|
|
6441
6967
|
if (optionDateTime.isBefore(startDateTime)) {
|
|
6442
|
-
continue;
|
|
6968
|
+
continue;
|
|
6443
6969
|
}
|
|
6444
6970
|
}
|
|
6445
6971
|
// Calculate duration if startTime is provided
|
|
@@ -6447,7 +6973,7 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
|
|
|
6447
6973
|
if (startDateTime && selectedDate) {
|
|
6448
6974
|
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6449
6975
|
const optionDateTime = selectedDateObj
|
|
6450
|
-
.hour(
|
|
6976
|
+
.hour(h)
|
|
6451
6977
|
.minute(m)
|
|
6452
6978
|
.second(0)
|
|
6453
6979
|
.millisecond(0);
|
|
@@ -6472,58 +6998,204 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
|
|
|
6472
6998
|
}
|
|
6473
6999
|
}
|
|
6474
7000
|
}
|
|
6475
|
-
const
|
|
6476
|
-
const minuteDisplay = m.toString().padStart(2, '0');
|
|
6477
|
-
const timeDisplay = `${hourDisplay}:${minuteDisplay} ${mer.toUpperCase()}`;
|
|
7001
|
+
const timeDisplay = `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:00`;
|
|
6478
7002
|
options.push({
|
|
6479
7003
|
label: timeDisplay,
|
|
6480
|
-
value: `${h}:${m}
|
|
7004
|
+
value: `${h}:${m}:0`,
|
|
6481
7005
|
hour: h,
|
|
6482
7006
|
minute: m,
|
|
6483
|
-
|
|
6484
|
-
searchText: timeDisplay,
|
|
7007
|
+
second: 0,
|
|
7008
|
+
searchText: timeDisplay,
|
|
6485
7009
|
durationText,
|
|
6486
7010
|
});
|
|
6487
7011
|
}
|
|
6488
7012
|
}
|
|
6489
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
|
+
}
|
|
6490
7102
|
return options;
|
|
6491
|
-
}, [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
|
|
6492
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]);
|
|
6493
7149
|
const { collection, filter } = useListCollection({
|
|
6494
7150
|
initialItems: timeOptions,
|
|
6495
|
-
itemToString:
|
|
7151
|
+
itemToString: itemToString,
|
|
6496
7152
|
itemToValue: (item) => item.value,
|
|
6497
|
-
filter:
|
|
7153
|
+
filter: customTimeFilter,
|
|
6498
7154
|
});
|
|
6499
7155
|
// Get current value string for combobox
|
|
6500
7156
|
const currentValue = useMemo(() => {
|
|
6501
|
-
if (
|
|
6502
|
-
|
|
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}`;
|
|
6503
7168
|
}
|
|
6504
|
-
|
|
6505
|
-
}, [hour, minute, meridiem]);
|
|
7169
|
+
}, [hour, minute, second, meridiem, is24Hour]);
|
|
6506
7170
|
// Calculate duration difference
|
|
6507
7171
|
const durationDiff = useMemo(() => {
|
|
6508
|
-
if (!startTime ||
|
|
6509
|
-
!selectedDate ||
|
|
6510
|
-
hour === null ||
|
|
6511
|
-
minute === null ||
|
|
6512
|
-
meridiem === null) {
|
|
7172
|
+
if (!startTime || !selectedDate || hour === null || minute === null) {
|
|
6513
7173
|
return null;
|
|
6514
7174
|
}
|
|
7175
|
+
if (is24Hour) {
|
|
7176
|
+
if (second === null)
|
|
7177
|
+
return null;
|
|
7178
|
+
}
|
|
7179
|
+
else {
|
|
7180
|
+
if (meridiem === null)
|
|
7181
|
+
return null;
|
|
7182
|
+
}
|
|
6515
7183
|
const startDateObj = dayjs(startTime).tz(timezone);
|
|
6516
7184
|
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6517
|
-
// Convert
|
|
7185
|
+
// Convert to 24-hour format
|
|
6518
7186
|
let hour24 = hour;
|
|
6519
|
-
if (
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
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
|
+
}
|
|
6523
7193
|
const currentDateTime = selectedDateObj
|
|
6524
7194
|
.hour(hour24)
|
|
6525
7195
|
.minute(minute)
|
|
6526
|
-
.second(
|
|
7196
|
+
.second(is24Hour && second !== null && second !== undefined
|
|
7197
|
+
? second
|
|
7198
|
+
: 0)
|
|
6527
7199
|
.millisecond(0);
|
|
6528
7200
|
if (!startDateObj.isValid() || !currentDateTime.isValid()) {
|
|
6529
7201
|
return null;
|
|
@@ -6549,13 +7221,28 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
|
|
|
6549
7221
|
return `+${diffText}`;
|
|
6550
7222
|
}
|
|
6551
7223
|
return null;
|
|
6552
|
-
}, [
|
|
7224
|
+
}, [
|
|
7225
|
+
hour,
|
|
7226
|
+
minute,
|
|
7227
|
+
second,
|
|
7228
|
+
meridiem,
|
|
7229
|
+
startTime,
|
|
7230
|
+
selectedDate,
|
|
7231
|
+
timezone,
|
|
7232
|
+
is24Hour,
|
|
7233
|
+
]);
|
|
6553
7234
|
const handleClear = () => {
|
|
6554
7235
|
setHour(null);
|
|
6555
7236
|
setMinute(null);
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
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('');
|
|
6559
7246
|
};
|
|
6560
7247
|
const handleValueChange = (details) => {
|
|
6561
7248
|
if (details.value.length === 0) {
|
|
@@ -6567,71 +7254,165 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
|
|
|
6567
7254
|
if (selectedOption) {
|
|
6568
7255
|
setHour(selectedOption.hour);
|
|
6569
7256
|
setMinute(selectedOption.minute);
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
}
|
|
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
|
+
}
|
|
6577
7270
|
}
|
|
6578
7271
|
};
|
|
6579
7272
|
// Parse input value and update state
|
|
6580
7273
|
const parseAndCommitInput = (value) => {
|
|
6581
7274
|
const trimmedValue = value.trim();
|
|
6582
|
-
// Filter the collection based on input
|
|
6583
7275
|
filter(trimmedValue);
|
|
6584
7276
|
if (!trimmedValue) {
|
|
6585
7277
|
return;
|
|
6586
7278
|
}
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
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
|
+
}
|
|
6608
7320
|
}
|
|
6609
7321
|
}
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
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();
|
|
6620
7367
|
if (parsedHour >= 1 &&
|
|
6621
7368
|
parsedHour <= 12 &&
|
|
6622
7369
|
parsedMinute >= 0 &&
|
|
6623
7370
|
parsedMinute <= 59) {
|
|
6624
7371
|
setHour(parsedHour);
|
|
6625
7372
|
setMinute(parsedMinute);
|
|
6626
|
-
setMeridiem
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
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);
|
|
6632
7391
|
return;
|
|
6633
7392
|
}
|
|
6634
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
|
+
}
|
|
6635
7416
|
}
|
|
6636
7417
|
// Parse failed, select first result
|
|
6637
7418
|
selectFirstResult();
|
|
@@ -6642,58 +7423,87 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
|
|
|
6642
7423
|
const firstItem = collection.items[0];
|
|
6643
7424
|
setHour(firstItem.hour);
|
|
6644
7425
|
setMinute(firstItem.minute);
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
}
|
|
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
|
+
}
|
|
6652
7439
|
}
|
|
6653
7440
|
};
|
|
6654
7441
|
const handleInputValueChange = (details) => {
|
|
6655
|
-
|
|
7442
|
+
if (is24Hour) {
|
|
7443
|
+
setInputValue(details.inputValue);
|
|
7444
|
+
}
|
|
6656
7445
|
filter(details.inputValue);
|
|
6657
7446
|
};
|
|
6658
7447
|
const handleFocus = (e) => {
|
|
6659
|
-
// Select all text when focusing
|
|
6660
7448
|
e.target.select();
|
|
6661
7449
|
};
|
|
6662
7450
|
const handleBlur = (e) => {
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
7451
|
+
const inputVal = e.target.value;
|
|
7452
|
+
if (is24Hour) {
|
|
7453
|
+
setInputValue(inputVal);
|
|
7454
|
+
}
|
|
7455
|
+
if (inputVal) {
|
|
7456
|
+
parseAndCommitInput(inputVal);
|
|
6667
7457
|
}
|
|
6668
7458
|
};
|
|
6669
7459
|
const handleKeyDown = (e) => {
|
|
6670
|
-
// Commit input on Enter key
|
|
6671
7460
|
if (e.key === 'Enter') {
|
|
6672
7461
|
e.preventDefault();
|
|
6673
|
-
const
|
|
6674
|
-
if (
|
|
6675
|
-
|
|
7462
|
+
const inputVal = e.currentTarget.value;
|
|
7463
|
+
if (is24Hour) {
|
|
7464
|
+
setInputValue(inputVal);
|
|
7465
|
+
}
|
|
7466
|
+
if (inputVal) {
|
|
7467
|
+
parseAndCommitInput(inputVal);
|
|
6676
7468
|
}
|
|
6677
|
-
// Blur the input
|
|
6678
7469
|
e.currentTarget?.blur();
|
|
6679
7470
|
}
|
|
6680
7471
|
};
|
|
6681
|
-
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 }) }))] }) }));
|
|
6682
7473
|
};
|
|
6683
7474
|
|
|
6684
7475
|
dayjs.extend(timezone);
|
|
6685
7476
|
const TimePicker = ({ column, schema, prefix }) => {
|
|
6686
7477
|
const { watch, formState: { errors }, setValue, } = useFormContext();
|
|
6687
7478
|
const { timezone, insideDialog, timePickerLabels } = useSchemaContext();
|
|
6688
|
-
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;
|
|
6689
7480
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
6690
7481
|
const colLabel = `${prefix}${column}`;
|
|
6691
7482
|
const formI18n = useFormI18n(column, prefix, schema);
|
|
6692
7483
|
const [open, setOpen] = useState(false);
|
|
6693
7484
|
const value = watch(colLabel);
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
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
|
+
: '';
|
|
6697
7507
|
// Parse the initial time parts from the time string (HH:mm:ssZ)
|
|
6698
7508
|
const parseTime = (time) => {
|
|
6699
7509
|
if (!time)
|
|
@@ -6746,884 +7556,898 @@ const TimePicker = ({ column, schema, prefix }) => {
|
|
|
6746
7556
|
return (jsx(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
6747
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: () => {
|
|
6748
7558
|
setOpen(true);
|
|
6749
|
-
}, 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 }) }) }) }) }))] }) }));
|
|
6750
7560
|
};
|
|
6751
7561
|
|
|
6752
7562
|
dayjs.extend(utc);
|
|
6753
7563
|
dayjs.extend(timezone);
|
|
6754
7564
|
dayjs.extend(customParseFormat);
|
|
6755
|
-
function
|
|
7565
|
+
function DateTimePicker$1({ value, onChange, format = 'date-time', showSeconds = false, labels = {
|
|
6756
7566
|
monthNamesShort: [
|
|
6757
|
-
'
|
|
6758
|
-
'
|
|
6759
|
-
'
|
|
6760
|
-
'
|
|
7567
|
+
'January',
|
|
7568
|
+
'February',
|
|
7569
|
+
'March',
|
|
7570
|
+
'April',
|
|
6761
7571
|
'May',
|
|
6762
|
-
'
|
|
6763
|
-
'
|
|
6764
|
-
'
|
|
6765
|
-
'
|
|
6766
|
-
'
|
|
6767
|
-
'
|
|
6768
|
-
'
|
|
7572
|
+
'June',
|
|
7573
|
+
'July',
|
|
7574
|
+
'August',
|
|
7575
|
+
'September',
|
|
7576
|
+
'October',
|
|
7577
|
+
'November',
|
|
7578
|
+
'December',
|
|
6769
7579
|
],
|
|
6770
7580
|
weekdayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
|
6771
7581
|
backButtonLabel: 'Back',
|
|
6772
|
-
forwardButtonLabel: '
|
|
6773
|
-
}, timezone = 'Asia/Hong_Kong', minDate, maxDate,
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
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();
|
|
7615
|
+
}
|
|
7616
|
+
if (defaultTime?.hour !== null && defaultTime?.hour !== undefined) {
|
|
7617
|
+
return defaultTime.hour;
|
|
7618
|
+
}
|
|
7619
|
+
return null;
|
|
7620
|
+
});
|
|
7621
|
+
const [minute, setMinute] = useState(() => {
|
|
7622
|
+
if (parsedValue) {
|
|
7623
|
+
return parsedValue.minute();
|
|
7624
|
+
}
|
|
7625
|
+
if (defaultTime?.minute !== null && defaultTime?.minute !== undefined) {
|
|
7626
|
+
return defaultTime.minute;
|
|
7627
|
+
}
|
|
7628
|
+
return null;
|
|
7629
|
+
});
|
|
7630
|
+
const [second, setSecond] = useState(() => {
|
|
7631
|
+
if (parsedValue) {
|
|
7632
|
+
return parsedValue.second();
|
|
7633
|
+
}
|
|
7634
|
+
if (defaultTime?.second !== null && defaultTime?.second !== undefined) {
|
|
7635
|
+
return defaultTime.second;
|
|
7636
|
+
}
|
|
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';
|
|
7643
|
+
}
|
|
7644
|
+
if (defaultTime?.meridiem !== null && defaultTime?.meridiem !== undefined) {
|
|
7645
|
+
return defaultTime.meridiem;
|
|
7646
|
+
}
|
|
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
|
|
6777
7668
|
useEffect(() => {
|
|
6778
|
-
if (
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
7669
|
+
if (controlledTimezoneOffset !== undefined) {
|
|
7670
|
+
setInternalTimezoneOffset(controlledTimezoneOffset);
|
|
7671
|
+
}
|
|
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);
|
|
7679
|
+
}
|
|
7680
|
+
}
|
|
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 });
|
|
7692
|
+
}
|
|
7693
|
+
return options;
|
|
7694
|
+
}, []);
|
|
7695
|
+
// Create collection for Select
|
|
7696
|
+
const { collection: timezoneCollection } = useListCollection({
|
|
7697
|
+
initialItems: timezoneOffsetOptions,
|
|
7698
|
+
itemToString: (item) => item.label,
|
|
7699
|
+
itemToValue: (item) => item.value,
|
|
7700
|
+
});
|
|
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);
|
|
6785
7715
|
}
|
|
6786
7716
|
else {
|
|
6787
|
-
|
|
7717
|
+
setDateInputValue('');
|
|
6788
7718
|
}
|
|
6789
|
-
}, [
|
|
6790
|
-
//
|
|
6791
|
-
const
|
|
6792
|
-
? typeof value === 'string'
|
|
6793
|
-
? dayjs(value).tz(timezone).isValid()
|
|
6794
|
-
? dayjs(value).tz(timezone).toDate()
|
|
6795
|
-
: new Date()
|
|
6796
|
-
: value
|
|
6797
|
-
: new Date();
|
|
6798
|
-
// Shared function to parse and validate input value
|
|
6799
|
-
const parseAndValidateInput = (inputVal) => {
|
|
7719
|
+
}, [selectedDate, tz]);
|
|
7720
|
+
// Parse and validate date input
|
|
7721
|
+
const parseAndValidateDateInput = (inputVal) => {
|
|
6800
7722
|
// If empty, clear the value
|
|
6801
7723
|
if (!inputVal.trim()) {
|
|
6802
|
-
|
|
6803
|
-
|
|
7724
|
+
setSelectedDate(null);
|
|
7725
|
+
updateDateTime(null, hour, minute, second, meridiem);
|
|
6804
7726
|
return;
|
|
6805
7727
|
}
|
|
6806
|
-
// Try parsing with
|
|
6807
|
-
let parsedDate = dayjs(inputVal,
|
|
6808
|
-
// If that fails, try common
|
|
7728
|
+
// Try parsing with common date formats
|
|
7729
|
+
let parsedDate = dayjs(inputVal, 'YYYY-MM-DD', true);
|
|
7730
|
+
// If that fails, try other common formats
|
|
6809
7731
|
if (!parsedDate.isValid()) {
|
|
6810
7732
|
parsedDate = dayjs(inputVal);
|
|
6811
7733
|
}
|
|
6812
|
-
// If still invalid, try parsing with dateFormat
|
|
6813
|
-
if (!parsedDate.isValid()) {
|
|
6814
|
-
parsedDate = dayjs(inputVal, dateFormat, true);
|
|
6815
|
-
}
|
|
6816
7734
|
// If valid, check constraints and update
|
|
6817
7735
|
if (parsedDate.isValid()) {
|
|
6818
|
-
const dateObj = parsedDate.tz(
|
|
7736
|
+
const dateObj = parsedDate.tz(tz).toDate();
|
|
6819
7737
|
// Check min/max constraints
|
|
6820
7738
|
if (minDate && dateObj < minDate) {
|
|
6821
|
-
// Invalid: before minDate, reset to
|
|
6822
|
-
|
|
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
|
+
}
|
|
6823
7747
|
return;
|
|
6824
7748
|
}
|
|
6825
7749
|
if (maxDate && dateObj > maxDate) {
|
|
6826
|
-
// Invalid: after maxDate, reset to
|
|
6827
|
-
|
|
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('');
|
|
7757
|
+
}
|
|
6828
7758
|
return;
|
|
6829
7759
|
}
|
|
6830
|
-
// Valid date -
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
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);
|
|
6835
7766
|
}
|
|
6836
7767
|
else {
|
|
6837
|
-
// Invalid date - reset to
|
|
6838
|
-
|
|
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
|
+
}
|
|
6839
7776
|
}
|
|
6840
7777
|
};
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
if (value) {
|
|
6844
|
-
const formatted = typeof value === 'string'
|
|
6845
|
-
? dayjs(value).tz(timezone).isValid()
|
|
6846
|
-
? dayjs(value).tz(timezone).format(displayFormat)
|
|
6847
|
-
: ''
|
|
6848
|
-
: dayjs(value).tz(timezone).format(displayFormat);
|
|
6849
|
-
setInputValue(formatted);
|
|
6850
|
-
}
|
|
6851
|
-
else {
|
|
6852
|
-
setInputValue('');
|
|
6853
|
-
}
|
|
7778
|
+
const handleDateInputChange = (e) => {
|
|
7779
|
+
setDateInputValue(e.target.value);
|
|
6854
7780
|
};
|
|
6855
|
-
const
|
|
6856
|
-
|
|
6857
|
-
setInputValue(e.target.value);
|
|
7781
|
+
const handleDateInputBlur = () => {
|
|
7782
|
+
parseAndValidateDateInput(dateInputValue);
|
|
6858
7783
|
};
|
|
6859
|
-
const
|
|
6860
|
-
// Parse and validate when input loses focus
|
|
6861
|
-
parseAndValidateInput(inputValue);
|
|
6862
|
-
};
|
|
6863
|
-
const handleKeyDown = (e) => {
|
|
6864
|
-
// Parse and validate when Enter is pressed
|
|
7784
|
+
const handleDateInputKeyDown = (e) => {
|
|
6865
7785
|
if (e.key === 'Enter') {
|
|
6866
7786
|
e.preventDefault();
|
|
6867
|
-
|
|
7787
|
+
parseAndValidateDateInput(dateInputValue);
|
|
6868
7788
|
}
|
|
6869
7789
|
};
|
|
6870
|
-
|
|
6871
|
-
|
|
6872
|
-
|
|
6873
|
-
|
|
6874
|
-
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
|
|
6879
|
-
dayjs.
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
6883
|
-
onChange = (_newValue) => { }, startTime, selectedDate, timezone = 'Asia/Hong_Kong', portalled = true, labels = {
|
|
6884
|
-
placeholder: 'HH:mm:ss',
|
|
6885
|
-
emptyMessage: 'No time found',
|
|
6886
|
-
}, }) {
|
|
6887
|
-
// Generate time options (every 15 minutes, seconds always 0)
|
|
6888
|
-
const timeOptions = useMemo(() => {
|
|
6889
|
-
const options = [];
|
|
6890
|
-
// Get start time for comparison if provided
|
|
6891
|
-
let startDateTime = null;
|
|
6892
|
-
let shouldFilterByDate = false;
|
|
6893
|
-
if (startTime && selectedDate) {
|
|
6894
|
-
const startDateObj = dayjs(startTime).tz(timezone);
|
|
6895
|
-
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6896
|
-
if (startDateObj.isValid() && selectedDateObj.isValid()) {
|
|
6897
|
-
startDateTime = startDateObj;
|
|
6898
|
-
// Only filter if dates are the same
|
|
6899
|
-
shouldFilterByDate =
|
|
6900
|
-
startDateObj.format('YYYY-MM-DD') ===
|
|
6901
|
-
selectedDateObj.format('YYYY-MM-DD');
|
|
6902
|
-
}
|
|
6903
|
-
}
|
|
6904
|
-
for (let h = 0; h < 24; h++) {
|
|
6905
|
-
for (let m = 0; m < 60; m += 15) {
|
|
6906
|
-
const timeDisplay = `${h.toString().padStart(2, '0')}:${m
|
|
6907
|
-
.toString()
|
|
6908
|
-
.padStart(2, '0')}:00`;
|
|
6909
|
-
// Filter out times that would result in negative duration (only when dates are the same)
|
|
6910
|
-
if (startDateTime && selectedDate && shouldFilterByDate) {
|
|
6911
|
-
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6912
|
-
const optionDateTime = selectedDateObj
|
|
6913
|
-
.hour(h)
|
|
6914
|
-
.minute(m)
|
|
6915
|
-
.second(0)
|
|
6916
|
-
.millisecond(0);
|
|
6917
|
-
if (optionDateTime.isBefore(startDateTime)) {
|
|
6918
|
-
continue; // Skip this option as it would result in negative duration
|
|
6919
|
-
}
|
|
6920
|
-
}
|
|
6921
|
-
// Calculate duration if startTime is provided
|
|
6922
|
-
let durationText;
|
|
6923
|
-
if (startDateTime && selectedDate) {
|
|
6924
|
-
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6925
|
-
const optionDateTime = selectedDateObj
|
|
6926
|
-
.hour(h)
|
|
6927
|
-
.minute(m)
|
|
6928
|
-
.second(0)
|
|
6929
|
-
.millisecond(0);
|
|
6930
|
-
if (optionDateTime.isValid() &&
|
|
6931
|
-
optionDateTime.isAfter(startDateTime)) {
|
|
6932
|
-
const diffMs = optionDateTime.diff(startDateTime);
|
|
6933
|
-
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
6934
|
-
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
6935
|
-
const diffSeconds = Math.floor((diffMs % (1000 * 60)) / 1000);
|
|
6936
|
-
if (diffHours > 0 || diffMinutes > 0 || diffSeconds > 0) {
|
|
6937
|
-
let diffText = '';
|
|
6938
|
-
if (diffHours > 0) {
|
|
6939
|
-
diffText = `${diffHours}h ${diffMinutes}m`;
|
|
6940
|
-
}
|
|
6941
|
-
else if (diffMinutes > 0) {
|
|
6942
|
-
diffText = `${diffMinutes}m ${diffSeconds}s`;
|
|
6943
|
-
}
|
|
6944
|
-
else {
|
|
6945
|
-
diffText = `${diffSeconds}s`;
|
|
6946
|
-
}
|
|
6947
|
-
durationText = `+${diffText}`;
|
|
6948
|
-
}
|
|
6949
|
-
}
|
|
6950
|
-
}
|
|
6951
|
-
options.push({
|
|
6952
|
-
label: timeDisplay,
|
|
6953
|
-
value: `${h}:${m}:0`,
|
|
6954
|
-
hour: h,
|
|
6955
|
-
minute: m,
|
|
6956
|
-
second: 0,
|
|
6957
|
-
searchText: timeDisplay, // Use base time without duration for searching
|
|
6958
|
-
durationText,
|
|
6959
|
-
});
|
|
6960
|
-
}
|
|
6961
|
-
}
|
|
6962
|
-
return options;
|
|
6963
|
-
}, [startTime, selectedDate, timezone]);
|
|
6964
|
-
const { contains } = useFilter({ sensitivity: 'base' });
|
|
6965
|
-
const { collection, filter } = useListCollection({
|
|
6966
|
-
initialItems: timeOptions,
|
|
6967
|
-
itemToString: (item) => item.searchText, // Use searchText (without duration) for filtering
|
|
6968
|
-
itemToValue: (item) => item.value,
|
|
6969
|
-
filter: contains,
|
|
6970
|
-
});
|
|
6971
|
-
// Get current value string for combobox
|
|
6972
|
-
const currentValue = useMemo(() => {
|
|
6973
|
-
if (hour === null || minute === null || second === null) {
|
|
6974
|
-
return '';
|
|
6975
|
-
}
|
|
6976
|
-
return `${hour}:${minute}:${second}`;
|
|
6977
|
-
}, [hour, minute, second]);
|
|
6978
|
-
// Calculate duration difference
|
|
6979
|
-
const durationDiff = useMemo(() => {
|
|
6980
|
-
if (!startTime ||
|
|
6981
|
-
!selectedDate ||
|
|
6982
|
-
hour === null ||
|
|
6983
|
-
minute === null ||
|
|
6984
|
-
second === null) {
|
|
6985
|
-
return null;
|
|
6986
|
-
}
|
|
6987
|
-
const startDateObj = dayjs(startTime).tz(timezone);
|
|
6988
|
-
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6989
|
-
const currentDateTime = selectedDateObj
|
|
6990
|
-
.hour(hour)
|
|
6991
|
-
.minute(minute)
|
|
6992
|
-
.second(second ?? 0)
|
|
6993
|
-
.millisecond(0);
|
|
6994
|
-
if (!startDateObj.isValid() || !currentDateTime.isValid()) {
|
|
6995
|
-
return null;
|
|
6996
|
-
}
|
|
6997
|
-
const diffMs = currentDateTime.diff(startDateObj);
|
|
6998
|
-
if (diffMs < 0) {
|
|
6999
|
-
return null;
|
|
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;
|
|
7000
7802
|
}
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
if (diffHours > 0) {
|
|
7007
|
-
diffText = `${diffHours}h ${diffMinutes}m`;
|
|
7008
|
-
}
|
|
7009
|
-
else if (diffMinutes > 0) {
|
|
7010
|
-
diffText = `${diffMinutes}m ${diffSeconds}s`;
|
|
7011
|
-
}
|
|
7012
|
-
else {
|
|
7013
|
-
diffText = `${diffSeconds}s`;
|
|
7014
|
-
}
|
|
7015
|
-
return `+${diffText}`;
|
|
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;
|
|
7016
7808
|
}
|
|
7017
|
-
return
|
|
7018
|
-
}, [hour, minute, second, startTime, selectedDate, timezone]);
|
|
7019
|
-
const handleClear = () => {
|
|
7020
|
-
setHour(null);
|
|
7021
|
-
setMinute(null);
|
|
7022
|
-
setSecond(null);
|
|
7023
|
-
filter(''); // Reset filter to show all options
|
|
7024
|
-
onChange({ hour: null, minute: null, second: null });
|
|
7809
|
+
return true;
|
|
7025
7810
|
};
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
if (selectedOption) {
|
|
7034
|
-
setHour(selectedOption.hour);
|
|
7035
|
-
setMinute(selectedOption.minute);
|
|
7036
|
-
setSecond(selectedOption.second);
|
|
7037
|
-
filter(''); // Reset filter after selection
|
|
7038
|
-
onChange({
|
|
7039
|
-
hour: selectedOption.hour,
|
|
7040
|
-
minute: selectedOption.minute,
|
|
7041
|
-
second: selectedOption.second,
|
|
7042
|
-
});
|
|
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);
|
|
7043
7818
|
}
|
|
7044
7819
|
};
|
|
7045
|
-
//
|
|
7046
|
-
const
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
const
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
// Validate ranges
|
|
7061
|
-
if (parsedHour >= 0 &&
|
|
7062
|
-
parsedHour <= 23 &&
|
|
7063
|
-
parsedMinute >= 0 &&
|
|
7064
|
-
parsedMinute <= 59 &&
|
|
7065
|
-
parsedSecond >= 0 &&
|
|
7066
|
-
parsedSecond <= 59) {
|
|
7067
|
-
setHour(parsedHour);
|
|
7068
|
-
setMinute(parsedMinute);
|
|
7069
|
-
setSecond(parsedSecond);
|
|
7070
|
-
onChange({
|
|
7071
|
-
hour: parsedHour,
|
|
7072
|
-
minute: parsedMinute,
|
|
7073
|
-
second: parsedSecond,
|
|
7074
|
-
});
|
|
7075
|
-
return;
|
|
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')}`;
|
|
7076
7835
|
}
|
|
7836
|
+
return `${hour24.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
|
|
7077
7837
|
}
|
|
7078
7838
|
else {
|
|
7079
|
-
//
|
|
7080
|
-
const
|
|
7081
|
-
if (
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
parsedSecond >= 0 &&
|
|
7091
|
-
parsedSecond <= 59) {
|
|
7092
|
-
setHour(parsedHour);
|
|
7093
|
-
setMinute(parsedMinute);
|
|
7094
|
-
setSecond(parsedSecond);
|
|
7095
|
-
onChange({
|
|
7096
|
-
hour: parsedHour,
|
|
7097
|
-
minute: parsedMinute,
|
|
7098
|
-
second: parsedSecond,
|
|
7099
|
-
});
|
|
7100
|
-
return;
|
|
7101
|
-
}
|
|
7102
|
-
}
|
|
7103
|
-
}
|
|
7104
|
-
// Parse failed, select first result
|
|
7105
|
-
selectFirstResult();
|
|
7106
|
-
};
|
|
7107
|
-
// Select first result from filtered collection
|
|
7108
|
-
const selectFirstResult = () => {
|
|
7109
|
-
if (collection.items.length > 0) {
|
|
7110
|
-
const firstItem = collection.items[0];
|
|
7111
|
-
setHour(firstItem.hour);
|
|
7112
|
-
setMinute(firstItem.minute);
|
|
7113
|
-
setSecond(firstItem.second);
|
|
7114
|
-
filter(''); // Reset filter after selection
|
|
7115
|
-
onChange({
|
|
7116
|
-
hour: firstItem.hour,
|
|
7117
|
-
minute: firstItem.minute,
|
|
7118
|
-
second: firstItem.second,
|
|
7119
|
-
});
|
|
7120
|
-
}
|
|
7121
|
-
};
|
|
7122
|
-
const handleInputValueChange = (details) => {
|
|
7123
|
-
// Filter the collection based on input, but don't parse yet
|
|
7124
|
-
filter(details.inputValue);
|
|
7125
|
-
};
|
|
7126
|
-
const handleFocus = (e) => {
|
|
7127
|
-
// Select all text when focusing
|
|
7128
|
-
e.target.select();
|
|
7129
|
-
};
|
|
7130
|
-
const handleBlur = (e) => {
|
|
7131
|
-
// Parse and commit the input value when losing focus
|
|
7132
|
-
const inputValue = e.target.value;
|
|
7133
|
-
if (inputValue) {
|
|
7134
|
-
parseAndCommitInput(inputValue);
|
|
7135
|
-
}
|
|
7136
|
-
};
|
|
7137
|
-
const handleKeyDown = (e) => {
|
|
7138
|
-
// Commit input on Enter key
|
|
7139
|
-
if (e.key === 'Enter') {
|
|
7140
|
-
e.preventDefault();
|
|
7141
|
-
const inputValue = e.currentTarget.value;
|
|
7142
|
-
if (inputValue) {
|
|
7143
|
-
parseAndCommitInput(inputValue);
|
|
7144
|
-
}
|
|
7145
|
-
// Blur the input
|
|
7146
|
-
e.currentTarget?.blur();
|
|
7147
|
-
}
|
|
7148
|
-
};
|
|
7149
|
-
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", openOnClick: true, flex: 1, children: [jsxs(Combobox.Control, { children: [jsx(InputGroup$1, { startElement: jsx(BsClock, {}), children: jsx(Combobox.Input, { placeholder: labels.placeholder, 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 }), 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 }) })), jsx(Button$1, { onClick: handleClear, size: "sm", variant: "ghost", children: jsx(Icon, { children: jsx(MdCancel, {}) }) })] }) }));
|
|
7150
|
-
}
|
|
7151
|
-
|
|
7152
|
-
dayjs.extend(utc);
|
|
7153
|
-
dayjs.extend(timezone);
|
|
7154
|
-
function DateTimePicker$1({ value, onChange, format = 'date-time', showSeconds = false, labels = {
|
|
7155
|
-
monthNamesShort: [
|
|
7156
|
-
'Jan',
|
|
7157
|
-
'Feb',
|
|
7158
|
-
'Mar',
|
|
7159
|
-
'Apr',
|
|
7160
|
-
'May',
|
|
7161
|
-
'Jun',
|
|
7162
|
-
'Jul',
|
|
7163
|
-
'Aug',
|
|
7164
|
-
'Sep',
|
|
7165
|
-
'Oct',
|
|
7166
|
-
'Nov',
|
|
7167
|
-
'Dec',
|
|
7168
|
-
],
|
|
7169
|
-
weekdayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
|
7170
|
-
backButtonLabel: 'Back',
|
|
7171
|
-
forwardButtonLabel: 'Next',
|
|
7172
|
-
}, timePickerLabels, timezone = 'Asia/Hong_Kong', startTime, minDate, maxDate, portalled = false, }) {
|
|
7173
|
-
console.log('[DateTimePicker] Component initialized with props:', {
|
|
7174
|
-
value,
|
|
7175
|
-
format,
|
|
7176
|
-
showSeconds,
|
|
7177
|
-
timezone,
|
|
7178
|
-
startTime,
|
|
7179
|
-
minDate,
|
|
7180
|
-
maxDate,
|
|
7181
|
-
});
|
|
7182
|
-
// Initialize selectedDate from value prop, converting ISO to YYYY-MM-DD format
|
|
7183
|
-
const getDateString = useCallback((val) => {
|
|
7184
|
-
if (!val)
|
|
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)
|
|
7185
7850
|
return '';
|
|
7186
|
-
|
|
7187
|
-
return
|
|
7188
|
-
}, [
|
|
7189
|
-
|
|
7190
|
-
// Helper to get time values from value prop with timezone
|
|
7191
|
-
const getTimeFromValue = useCallback((val) => {
|
|
7192
|
-
console.log('[DateTimePicker] getTimeFromValue called:', {
|
|
7193
|
-
val,
|
|
7194
|
-
timezone,
|
|
7195
|
-
showSeconds,
|
|
7196
|
-
});
|
|
7197
|
-
if (!val) {
|
|
7198
|
-
console.log('[DateTimePicker] No value provided, returning nulls');
|
|
7199
|
-
return {
|
|
7200
|
-
hour12: null,
|
|
7201
|
-
minute: null,
|
|
7202
|
-
meridiem: null,
|
|
7203
|
-
hour24: null,
|
|
7204
|
-
second: null,
|
|
7205
|
-
};
|
|
7206
|
-
}
|
|
7207
|
-
const dateObj = dayjs(val).tz(timezone);
|
|
7208
|
-
console.log('[DateTimePicker] Parsed date object:', {
|
|
7209
|
-
original: val,
|
|
7210
|
-
timezone,
|
|
7211
|
-
isValid: dateObj.isValid(),
|
|
7212
|
-
formatted: dateObj.format('YYYY-MM-DD HH:mm:ss Z'),
|
|
7213
|
-
hour24: dateObj.hour(),
|
|
7214
|
-
minute: dateObj.minute(),
|
|
7215
|
-
second: dateObj.second(),
|
|
7216
|
-
});
|
|
7217
|
-
if (!dateObj.isValid()) {
|
|
7218
|
-
console.log('[DateTimePicker] Invalid date object, returning nulls');
|
|
7219
|
-
return {
|
|
7220
|
-
hour12: null,
|
|
7221
|
-
minute: null,
|
|
7222
|
-
meridiem: null,
|
|
7223
|
-
hour24: null,
|
|
7224
|
-
second: null,
|
|
7225
|
-
};
|
|
7226
|
-
}
|
|
7227
|
-
const hour24Value = dateObj.hour();
|
|
7228
|
-
const hour12Value = hour24Value % 12 || 12;
|
|
7229
|
-
const minuteValue = dateObj.minute();
|
|
7230
|
-
const meridiemValue = hour24Value >= 12 ? 'pm' : 'am';
|
|
7231
|
-
const secondValue = showSeconds ? dateObj.second() : null;
|
|
7232
|
-
const result = {
|
|
7233
|
-
hour12: hour12Value,
|
|
7234
|
-
minute: minuteValue,
|
|
7235
|
-
meridiem: meridiemValue,
|
|
7236
|
-
hour24: hour24Value,
|
|
7237
|
-
second: secondValue,
|
|
7238
|
-
};
|
|
7239
|
-
console.log('[DateTimePicker] Extracted time values:', result);
|
|
7240
|
-
return result;
|
|
7241
|
-
}, [timezone, showSeconds]);
|
|
7242
|
-
const initialTime = getTimeFromValue(value);
|
|
7243
|
-
console.log('[DateTimePicker] Initial time from value:', {
|
|
7244
|
-
value,
|
|
7245
|
-
initialTime,
|
|
7246
|
-
});
|
|
7247
|
-
// Time state for 12-hour format
|
|
7248
|
-
const [hour12, setHour12] = useState(initialTime.hour12);
|
|
7249
|
-
const [minute, setMinute] = useState(initialTime.minute);
|
|
7250
|
-
const [meridiem, setMeridiem] = useState(initialTime.meridiem);
|
|
7251
|
-
// Time state for 24-hour format
|
|
7252
|
-
const [hour24, setHour24] = useState(initialTime.hour24);
|
|
7253
|
-
const [second, setSecond] = useState(initialTime.second);
|
|
7254
|
-
// Sync selectedDate and time states when value prop changes
|
|
7851
|
+
// Show offset as is (e.g., "+08:00")
|
|
7852
|
+
return timezoneOffset;
|
|
7853
|
+
}, [timezoneOffset, showTimezoneSelector]);
|
|
7854
|
+
// Update selectedDate when value changes externally
|
|
7255
7855
|
useEffect(() => {
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
setHour12(null);
|
|
7266
|
-
setMinute(null);
|
|
7267
|
-
setMeridiem(null);
|
|
7268
|
-
setHour24(null);
|
|
7269
|
-
setSecond(null);
|
|
7270
|
-
return;
|
|
7271
|
-
}
|
|
7272
|
-
// Check if value is valid
|
|
7273
|
-
const dateObj = dayjs(value).tz(timezone);
|
|
7274
|
-
if (!dateObj.isValid()) {
|
|
7275
|
-
console.log('[DateTimePicker] Invalid value, clearing all fields');
|
|
7276
|
-
setSelectedDate('');
|
|
7277
|
-
setHour12(null);
|
|
7278
|
-
setMinute(null);
|
|
7279
|
-
setMeridiem(null);
|
|
7280
|
-
setHour24(null);
|
|
7281
|
-
setSecond(null);
|
|
7282
|
-
return;
|
|
7283
|
-
}
|
|
7284
|
-
const dateString = getDateString(value);
|
|
7285
|
-
console.log('[DateTimePicker] Setting selectedDate:', dateString);
|
|
7286
|
-
setSelectedDate(dateString);
|
|
7287
|
-
const timeData = getTimeFromValue(value);
|
|
7288
|
-
console.log('[DateTimePicker] Updating time states:', {
|
|
7289
|
-
timeData,
|
|
7290
|
-
});
|
|
7291
|
-
setHour12(timeData.hour12);
|
|
7292
|
-
setMinute(timeData.minute);
|
|
7293
|
-
setMeridiem(timeData.meridiem);
|
|
7294
|
-
setHour24(timeData.hour24);
|
|
7295
|
-
setSecond(timeData.second);
|
|
7296
|
-
}, [value, getTimeFromValue, getDateString, timezone]);
|
|
7297
|
-
const handleDateChange = (date) => {
|
|
7298
|
-
console.log('[DateTimePicker] handleDateChange called:', {
|
|
7299
|
-
date,
|
|
7300
|
-
timezone,
|
|
7301
|
-
showSeconds,
|
|
7302
|
-
currentTimeStates: { hour12, minute, meridiem, hour24, second },
|
|
7303
|
-
});
|
|
7304
|
-
// If date is empty or invalid, clear all fields
|
|
7305
|
-
if (!date || date === '') {
|
|
7306
|
-
console.log('[DateTimePicker] Empty date, clearing all fields');
|
|
7307
|
-
setSelectedDate('');
|
|
7308
|
-
setHour12(null);
|
|
7309
|
-
setMinute(null);
|
|
7310
|
-
setMeridiem(null);
|
|
7311
|
-
setHour24(null);
|
|
7312
|
-
setSecond(null);
|
|
7313
|
-
onChange?.(undefined);
|
|
7314
|
-
return;
|
|
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
|
+
}
|
|
7315
7865
|
}
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
date,
|
|
7321
|
-
timezone,
|
|
7322
|
-
isValid: dateObj.isValid(),
|
|
7323
|
-
isoString: dateObj.toISOString(),
|
|
7324
|
-
formatted: dateObj.format('YYYY-MM-DD HH:mm:ss Z'),
|
|
7325
|
-
});
|
|
7326
|
-
if (!dateObj.isValid()) {
|
|
7327
|
-
console.warn('[DateTimePicker] Invalid date object in handleDateChange, clearing fields');
|
|
7328
|
-
setSelectedDate('');
|
|
7329
|
-
setHour12(null);
|
|
7330
|
-
setMinute(null);
|
|
7331
|
-
setMeridiem(null);
|
|
7332
|
-
setHour24(null);
|
|
7333
|
-
setSecond(null);
|
|
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) {
|
|
7334
7870
|
onChange?.(undefined);
|
|
7335
7871
|
return;
|
|
7336
7872
|
}
|
|
7337
|
-
//
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
format
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
setMinute(data.minute);
|
|
7361
|
-
if (showSeconds) {
|
|
7362
|
-
setSecond(data.second ?? null);
|
|
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
|
+
}
|
|
7363
7896
|
}
|
|
7364
7897
|
else {
|
|
7365
|
-
//
|
|
7366
|
-
|
|
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
|
+
}
|
|
7367
7905
|
}
|
|
7368
7906
|
}
|
|
7369
|
-
else {
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
//
|
|
7377
|
-
if (
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
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()) {
|
|
7385
7936
|
onChange?.(undefined);
|
|
7386
7937
|
return;
|
|
7387
7938
|
}
|
|
7388
|
-
|
|
7389
|
-
if (
|
|
7390
|
-
|
|
7939
|
+
// Format based on format prop
|
|
7940
|
+
if (format === 'iso-date-time') {
|
|
7941
|
+
onChange?.(dateTime.format('YYYY-MM-DDTHH:mm:ss'));
|
|
7391
7942
|
}
|
|
7392
7943
|
else {
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
setHour12(null);
|
|
7396
|
-
setMinute(null);
|
|
7397
|
-
setMeridiem(null);
|
|
7398
|
-
setHour24(null);
|
|
7399
|
-
setSecond(null);
|
|
7400
|
-
onChange?.(undefined);
|
|
7944
|
+
// date-time format with timezone
|
|
7945
|
+
onChange?.(dateTime.format('YYYY-MM-DDTHH:mm:ssZ'));
|
|
7401
7946
|
}
|
|
7402
7947
|
};
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
setMinute(null);
|
|
7415
|
-
setMeridiem(null);
|
|
7416
|
-
setHour24(null);
|
|
7417
|
-
setSecond(null);
|
|
7418
|
-
onChange?.(undefined);
|
|
7419
|
-
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);
|
|
7420
7959
|
}
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
if (!dateObj.isValid()) {
|
|
7424
|
-
console.warn('[DateTimePicker] Invalid date object in updateDateTime, clearing fields:', date);
|
|
7425
|
-
setSelectedDate('');
|
|
7426
|
-
setHour12(null);
|
|
7427
|
-
setMinute(null);
|
|
7428
|
-
setMeridiem(null);
|
|
7429
|
-
setHour24(null);
|
|
7430
|
-
setSecond(null);
|
|
7431
|
-
onChange?.(undefined);
|
|
7432
|
-
return;
|
|
7960
|
+
else {
|
|
7961
|
+
setMeridiem(newMeridiem);
|
|
7433
7962
|
}
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
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
|
+
}
|
|
7452
8083
|
}
|
|
7453
|
-
console.log('[DateTimePicker] ISO format - setting time on date:', {
|
|
7454
|
-
h,
|
|
7455
|
-
m,
|
|
7456
|
-
s,
|
|
7457
|
-
showSeconds,
|
|
7458
|
-
});
|
|
7459
|
-
if (h !== null)
|
|
7460
|
-
newDate.setHours(h);
|
|
7461
|
-
if (m !== null)
|
|
7462
|
-
newDate.setMinutes(m);
|
|
7463
|
-
newDate.setSeconds(s ?? 0);
|
|
7464
8084
|
}
|
|
7465
8085
|
else {
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
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
|
+
}
|
|
7486
8152
|
}
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
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;
|
|
7491
8171
|
});
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
|
|
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;
|
|
7504
8189
|
}
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
7509
|
-
});
|
|
8190
|
+
const lowerItemText = itemText.toLowerCase();
|
|
8191
|
+
const lowerFilterText = filterText.toLowerCase();
|
|
8192
|
+
if (lowerItemText.includes(lowerFilterText)) {
|
|
8193
|
+
return true;
|
|
7510
8194
|
}
|
|
7511
|
-
|
|
7512
|
-
|
|
8195
|
+
const item = timeOptions.find((opt) => opt.searchText.toLowerCase() === lowerItemText);
|
|
8196
|
+
if (!item || !('meridiem' in item)) {
|
|
8197
|
+
return false;
|
|
7513
8198
|
}
|
|
7514
|
-
|
|
7515
|
-
|
|
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 '';
|
|
7516
8228
|
}
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
const finalISO = dayjs(newDate).tz(timezone).toISOString();
|
|
7520
|
-
console.log('[DateTimePicker] Final ISO string to emit:', {
|
|
7521
|
-
newDate: newDate.toISOString(),
|
|
7522
|
-
timezone,
|
|
7523
|
-
finalISO,
|
|
7524
|
-
});
|
|
7525
|
-
onChange?.(finalISO);
|
|
7526
|
-
};
|
|
7527
|
-
const handleClear = () => {
|
|
7528
|
-
setSelectedDate('');
|
|
7529
|
-
setHour12(null);
|
|
7530
|
-
setHour24(null);
|
|
7531
|
-
setMinute(null);
|
|
7532
|
-
setSecond(null);
|
|
7533
|
-
setMeridiem(null);
|
|
7534
|
-
onChange?.(undefined);
|
|
7535
|
-
};
|
|
7536
|
-
const isISO = format === 'iso-date-time';
|
|
7537
|
-
// Normalize startTime to ignore milliseconds
|
|
7538
|
-
const normalizedStartTime = startTime
|
|
7539
|
-
? dayjs(startTime).tz(timezone).millisecond(0).toISOString()
|
|
7540
|
-
: undefined;
|
|
7541
|
-
// Determine minDate: prioritize explicit minDate prop, then fall back to startTime
|
|
7542
|
-
const effectiveMinDate = minDate
|
|
7543
|
-
? minDate
|
|
7544
|
-
: normalizedStartTime && dayjs(normalizedStartTime).tz(timezone).isValid()
|
|
7545
|
-
? dayjs(normalizedStartTime).tz(timezone).startOf('day').toDate()
|
|
7546
|
-
: undefined;
|
|
7547
|
-
// Log current state before render
|
|
7548
|
-
useEffect(() => {
|
|
7549
|
-
console.log('[DateTimePicker] Current state before render:', {
|
|
7550
|
-
isISO,
|
|
7551
|
-
hour12,
|
|
7552
|
-
minute,
|
|
7553
|
-
meridiem,
|
|
7554
|
-
hour24,
|
|
7555
|
-
second,
|
|
7556
|
-
selectedDate,
|
|
7557
|
-
normalizedStartTime,
|
|
7558
|
-
timezone,
|
|
7559
|
-
});
|
|
7560
|
-
}, [
|
|
7561
|
-
isISO,
|
|
7562
|
-
hour12,
|
|
7563
|
-
minute,
|
|
7564
|
-
meridiem,
|
|
7565
|
-
hour24,
|
|
7566
|
-
second,
|
|
7567
|
-
selectedDate,
|
|
7568
|
-
normalizedStartTime,
|
|
7569
|
-
timezone,
|
|
7570
|
-
]);
|
|
7571
|
-
// Compute display text from current state
|
|
7572
|
-
const displayText = useMemo(() => {
|
|
7573
|
-
if (!selectedDate)
|
|
7574
|
-
return null;
|
|
7575
|
-
const dateObj = dayjs.tz(selectedDate, timezone);
|
|
7576
|
-
if (!dateObj.isValid())
|
|
7577
|
-
return null;
|
|
7578
|
-
if (isISO) {
|
|
7579
|
-
// For ISO format, use hour24, minute, second
|
|
7580
|
-
if (hour24 === null || minute === null)
|
|
7581
|
-
return null;
|
|
7582
|
-
const dateTimeObj = dateObj
|
|
7583
|
-
.hour(hour24)
|
|
7584
|
-
.minute(minute)
|
|
7585
|
-
.second(second ?? 0);
|
|
7586
|
-
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}`;
|
|
7587
8231
|
}
|
|
7588
8232
|
else {
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
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
|
+
}
|
|
7600
8278
|
}
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7618
|
-
|
|
7619
|
-
|
|
7620
|
-
|
|
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';
|
|
7621
8327
|
}
|
|
7622
8328
|
else {
|
|
7623
|
-
|
|
7624
|
-
|
|
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);
|
|
7625
8393
|
}
|
|
7626
|
-
}
|
|
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))) }) })] }) }) }) }) }))] }))] }));
|
|
7627
8451
|
}
|
|
7628
8452
|
|
|
7629
8453
|
dayjs.extend(utc);
|
|
@@ -7634,14 +8458,15 @@ const DateTimePicker = ({ column, schema, prefix, }) => {
|
|
|
7634
8458
|
const formI18n = useFormI18n(column, prefix, schema);
|
|
7635
8459
|
const { required, gridColumn = 'span 12', gridRow = 'span 1', displayDateFormat = 'YYYY-MM-DD HH:mm:ss',
|
|
7636
8460
|
// with timezone
|
|
7637
|
-
dateFormat = 'YYYY-MM-DD[T]HH:mm:ssZ', } = schema;
|
|
8461
|
+
dateFormat = 'YYYY-MM-DD[T]HH:mm:ssZ', dateTimePicker, } = schema;
|
|
7638
8462
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
7639
8463
|
const colLabel = formI18n.colLabel;
|
|
7640
|
-
|
|
8464
|
+
useState(false);
|
|
7641
8465
|
const selectedDate = watch(colLabel);
|
|
7642
|
-
|
|
8466
|
+
selectedDate && dayjs(selectedDate).tz(timezone).isValid()
|
|
7643
8467
|
? dayjs(selectedDate).tz(timezone).format(displayDateFormat)
|
|
7644
8468
|
: '';
|
|
8469
|
+
// Set default date on mount if no value exists
|
|
7645
8470
|
const dateTimePickerLabelsConfig = {
|
|
7646
8471
|
monthNamesShort: dateTimePickerLabels?.monthNamesShort ?? [
|
|
7647
8472
|
'January',
|
|
@@ -7681,11 +8506,9 @@ const DateTimePicker = ({ column, schema, prefix, }) => {
|
|
|
7681
8506
|
else {
|
|
7682
8507
|
setValue(colLabel, undefined);
|
|
7683
8508
|
}
|
|
7684
|
-
}, timezone: timezone, labels: dateTimePickerLabelsConfig, timePickerLabels: timePickerLabels }));
|
|
8509
|
+
}, timezone: timezone, labels: dateTimePickerLabelsConfig, timePickerLabels: timePickerLabels, showQuickActions: dateTimePicker?.showQuickActions ?? false, quickActionLabels: dateTimePicker?.quickActionLabels, showTimezoneSelector: dateTimePicker?.showTimezoneSelector ?? false }));
|
|
7685
8510
|
return (jsx(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
7686
|
-
gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children:
|
|
7687
|
-
setOpen(true);
|
|
7688
|
-
}, 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 }));
|
|
7689
8512
|
};
|
|
7690
8513
|
|
|
7691
8514
|
const SchemaRenderer = ({ schema, prefix, column, }) => {
|
|
@@ -7806,7 +8629,7 @@ const BooleanViewer = ({ schema, column, prefix, }) => {
|
|
|
7806
8629
|
const value = watch(colLabel);
|
|
7807
8630
|
const formI18n = useFormI18n(column, prefix, schema);
|
|
7808
8631
|
return (jsxs(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
7809
|
-
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() }))] }));
|
|
7810
8633
|
};
|
|
7811
8634
|
|
|
7812
8635
|
const CustomViewer = ({ column, schema, prefix }) => {
|
|
@@ -7838,23 +8661,22 @@ const DateViewer = ({ column, schema, prefix }) => {
|
|
|
7838
8661
|
|
|
7839
8662
|
const EnumViewer = ({ column, isMultiple = false, schema, prefix, }) => {
|
|
7840
8663
|
const { watch, formState: { errors }, } = useFormContext();
|
|
7841
|
-
const formI18n = useFormI18n(column, prefix);
|
|
8664
|
+
const formI18n = useFormI18n(column, prefix, schema);
|
|
7842
8665
|
const { required } = schema;
|
|
7843
8666
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
7844
|
-
const { gridColumn =
|
|
8667
|
+
const { gridColumn = 'span 12', gridRow = 'span 1', renderDisplay } = schema;
|
|
7845
8668
|
const colLabel = formI18n.colLabel;
|
|
7846
8669
|
const watchEnum = watch(colLabel);
|
|
7847
8670
|
const watchEnums = (watch(colLabel) ?? []);
|
|
7848
|
-
|
|
7849
|
-
|
|
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) => {
|
|
7850
8674
|
const item = enumValue;
|
|
7851
8675
|
if (item === undefined) {
|
|
7852
8676
|
return jsx(Fragment, { children: "undefined" });
|
|
7853
8677
|
}
|
|
7854
|
-
return (jsx(Tag, { size: "lg", children:
|
|
7855
|
-
|
|
7856
|
-
: formI18n.t(item) }, item));
|
|
7857
|
-
}) })), !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() }))] }));
|
|
7858
8680
|
};
|
|
7859
8681
|
|
|
7860
8682
|
const FileViewer = ({ column, schema, prefix }) => {
|
|
@@ -7974,31 +8796,35 @@ const StringViewer = ({ column, schema, prefix, }) => {
|
|
|
7974
8796
|
|
|
7975
8797
|
const TagViewer = ({ column, schema, prefix }) => {
|
|
7976
8798
|
const { watch, formState: { errors }, setValue, } = useFormContext();
|
|
7977
|
-
const { serverUrl } = useSchemaContext();
|
|
7978
8799
|
if (schema.properties == undefined) {
|
|
7979
|
-
throw new Error(
|
|
8800
|
+
throw new Error('schema properties undefined when using DatePicker');
|
|
7980
8801
|
}
|
|
7981
|
-
const { gridColumn, gridRow, in_table, object_id_column } = schema;
|
|
8802
|
+
const { gridColumn, gridRow, in_table, object_id_column, tagPicker } = schema;
|
|
7982
8803
|
if (in_table === undefined) {
|
|
7983
|
-
throw new Error(
|
|
8804
|
+
throw new Error('in_table is undefined when using TagPicker');
|
|
7984
8805
|
}
|
|
7985
8806
|
if (object_id_column === undefined) {
|
|
7986
|
-
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.');
|
|
7987
8811
|
}
|
|
7988
8812
|
const query = useQuery({
|
|
7989
8813
|
queryKey: [`tagpicker`, in_table],
|
|
7990
8814
|
queryFn: async () => {
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
in_table: "tables_tags_view",
|
|
8815
|
+
const result = await tagPicker.queryFn({
|
|
8816
|
+
in_table: 'tables_tags_view',
|
|
7994
8817
|
where: [
|
|
7995
8818
|
{
|
|
7996
|
-
id:
|
|
8819
|
+
id: 'table_name',
|
|
7997
8820
|
value: [in_table],
|
|
7998
8821
|
},
|
|
7999
8822
|
],
|
|
8000
8823
|
limit: 100,
|
|
8824
|
+
offset: 0,
|
|
8825
|
+
searching: '',
|
|
8001
8826
|
});
|
|
8827
|
+
return result.data || { data: [] };
|
|
8002
8828
|
},
|
|
8003
8829
|
staleTime: 10000,
|
|
8004
8830
|
});
|
|
@@ -8006,17 +8832,19 @@ const TagViewer = ({ column, schema, prefix }) => {
|
|
|
8006
8832
|
const existingTagsQuery = useQuery({
|
|
8007
8833
|
queryKey: [`existing`, { in_table, object_id_column }, object_id],
|
|
8008
8834
|
queryFn: async () => {
|
|
8009
|
-
|
|
8010
|
-
serverUrl,
|
|
8835
|
+
const result = await tagPicker.queryFn({
|
|
8011
8836
|
in_table: in_table,
|
|
8012
8837
|
where: [
|
|
8013
8838
|
{
|
|
8014
8839
|
id: object_id_column,
|
|
8015
|
-
value: object_id[0],
|
|
8840
|
+
value: [object_id[0]],
|
|
8016
8841
|
},
|
|
8017
8842
|
],
|
|
8018
8843
|
limit: 100,
|
|
8844
|
+
offset: 0,
|
|
8845
|
+
searching: '',
|
|
8019
8846
|
});
|
|
8847
|
+
return result.data || { data: [] };
|
|
8020
8848
|
},
|
|
8021
8849
|
enabled: object_id != undefined,
|
|
8022
8850
|
staleTime: 10000,
|
|
@@ -8027,9 +8855,9 @@ const TagViewer = ({ column, schema, prefix }) => {
|
|
|
8027
8855
|
if (!!object_id === false) {
|
|
8028
8856
|
return jsx(Fragment, {});
|
|
8029
8857
|
}
|
|
8030
|
-
return (jsxs(Flex, { flexFlow:
|
|
8858
|
+
return (jsxs(Flex, { flexFlow: 'column', gap: 4, gridColumn,
|
|
8031
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 }) => {
|
|
8032
|
-
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) => {
|
|
8033
8861
|
const existedTags = Object.values(all_tags)
|
|
8034
8862
|
.filter(({ id }) => {
|
|
8035
8863
|
return existingTagList.some(({ tag_id }) => tag_id === id);
|
|
@@ -8041,20 +8869,20 @@ const TagViewer = ({ column, schema, prefix }) => {
|
|
|
8041
8869
|
tagIds.value,
|
|
8042
8870
|
]);
|
|
8043
8871
|
setValue(`${column}.${parent_tag_name}.old`, existedTags);
|
|
8044
|
-
}, children: jsx(Flex, { flexFlow:
|
|
8872
|
+
}, children: jsx(Flex, { flexFlow: 'wrap', gap: 2, children: Object.entries(all_tags).map(([tagName, { id }]) => {
|
|
8045
8873
|
if (existingTagList.some(({ tag_id }) => tag_id === id)) {
|
|
8046
|
-
return (jsx(RadioCardItem, { label: tagName, value: id, flex:
|
|
8874
|
+
return (jsx(RadioCardItem, { label: tagName, value: id, flex: '0 0 0%', disabled: true }, `${tagName}-${id}`));
|
|
8047
8875
|
}
|
|
8048
|
-
return (jsx(RadioCardItem, { label: tagName, value: id, flex:
|
|
8876
|
+
return (jsx(RadioCardItem, { label: tagName, value: id, flex: '0 0 0%', colorPalette: 'blue' }, `${tagName}-${id}`));
|
|
8049
8877
|
}) }) })), !is_mutually_exclusive && (jsx(CheckboxGroup, { onValueChange: (tagIds) => {
|
|
8050
8878
|
setValue(`${column}.${parent_tag_name}.current`, tagIds);
|
|
8051
|
-
}, children: jsx(Flex, { flexFlow:
|
|
8879
|
+
}, children: jsx(Flex, { flexFlow: 'wrap', gap: 2, children: Object.entries(all_tags).map(([tagName, { id }]) => {
|
|
8052
8880
|
if (existingTagList.some(({ tag_id }) => tag_id === id)) {
|
|
8053
|
-
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}`));
|
|
8054
8882
|
}
|
|
8055
|
-
return (jsx(CheckboxCard, { label: tagName, value: id, flex:
|
|
8883
|
+
return (jsx(CheckboxCard, { label: tagName, value: id, flex: '0 0 0%' }, `${tagName}-${id}`));
|
|
8056
8884
|
}) }) }))] }, `tag-${parent_tag_name}`));
|
|
8057
|
-
}), errors[`${column}`] && (jsx(Text, { color:
|
|
8885
|
+
}), errors[`${column}`] && (jsx(Text, { color: 'red.400', children: (errors[`${column}`]?.message ?? 'No error message') }))] }));
|
|
8058
8886
|
};
|
|
8059
8887
|
|
|
8060
8888
|
const TextAreaViewer = ({ column, schema, prefix, }) => {
|
|
@@ -8261,6 +9089,17 @@ const FormBody = () => {
|
|
|
8261
9089
|
|
|
8262
9090
|
const FormTitle = () => {
|
|
8263
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
|
+
}
|
|
8264
9103
|
return jsx(Heading, { children: schema.title ?? 'Form' });
|
|
8265
9104
|
};
|
|
8266
9105
|
|
|
@@ -9424,4 +10263,4 @@ function DataTableServer({ columns, enableRowSelection = true, enableMultiRowSel
|
|
|
9424
10263
|
}, children: jsx(DataTableServerContext.Provider, { value: { url: url ?? '', query }, children: children }) }));
|
|
9425
10264
|
}
|
|
9426
10265
|
|
|
9427
|
-
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 };
|