@bsol-oss/react-datatable5 13.0.1-beta.1 → 13.0.1-beta.11
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 +77 -21
- package/dist/index.js +2113 -1270
- package/dist/index.mjs +2116 -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 +49 -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.js
CHANGED
|
@@ -29,10 +29,10 @@ var reactHookForm = require('react-hook-form');
|
|
|
29
29
|
var Ajv = require('ajv');
|
|
30
30
|
var addFormats = require('ajv-formats');
|
|
31
31
|
var dayjs = require('dayjs');
|
|
32
|
-
var
|
|
32
|
+
var customParseFormat = require('dayjs/plugin/customParseFormat');
|
|
33
33
|
var timezone = require('dayjs/plugin/timezone');
|
|
34
|
+
var utc = require('dayjs/plugin/utc');
|
|
34
35
|
var ti = require('react-icons/ti');
|
|
35
|
-
var customParseFormat = require('dayjs/plugin/customParseFormat');
|
|
36
36
|
var matchSorterUtils = require('@tanstack/match-sorter-utils');
|
|
37
37
|
|
|
38
38
|
function _interopNamespaceDefault(e) {
|
|
@@ -3714,7 +3714,7 @@ const TextWithCopy = ({ text, globalFilter, highlightedText, }) => {
|
|
|
3714
3714
|
const displayText = highlightedText !== undefined
|
|
3715
3715
|
? highlightedText
|
|
3716
3716
|
: highlightText$1(textValue, globalFilter);
|
|
3717
|
-
return (jsxRuntime.jsxs(react.HStack, { gap: 2, alignItems: "center", children: [jsxRuntime.jsx(react.Text, { as: "span", children: displayText }), jsxRuntime.jsx(react.Clipboard.Root, { value: textValue, children: jsxRuntime.jsx(react.Clipboard.Trigger, { asChild: true, children: jsxRuntime.jsx(react.IconButton, { size: "
|
|
3717
|
+
return (jsxRuntime.jsxs(react.HStack, { gap: 2, alignItems: "center", children: [jsxRuntime.jsx(react.Text, { as: "span", children: displayText }), jsxRuntime.jsx(react.Clipboard.Root, { value: textValue, children: jsxRuntime.jsx(react.Clipboard.Trigger, { asChild: true, children: jsxRuntime.jsx(react.IconButton, { size: "2xs", variant: "ghost", "aria-label": "Copy", fontSize: "1em", children: jsxRuntime.jsx(react.Clipboard.Indicator, { copied: jsxRuntime.jsx(lu.LuCheck, {}), children: jsxRuntime.jsx(lu.LuCopy, {}) }) }) }) })] }));
|
|
3718
3718
|
};
|
|
3719
3719
|
|
|
3720
3720
|
// Helper function to highlight matching text
|
|
@@ -4036,7 +4036,6 @@ const getColumns = ({ schema, include = [], ignore = [], width = [], meta = {},
|
|
|
4036
4036
|
//@ts-expect-error TODO: find appropriate type
|
|
4037
4037
|
const SchemaFormContext = React.createContext({
|
|
4038
4038
|
schema: {},
|
|
4039
|
-
serverUrl: '',
|
|
4040
4039
|
requestUrl: '',
|
|
4041
4040
|
order: [],
|
|
4042
4041
|
ignore: [],
|
|
@@ -4147,6 +4146,22 @@ const convertAjvErrorsToFieldErrors = (errors, schema) => {
|
|
|
4147
4146
|
// Get the schema node for this field to check for custom error messages
|
|
4148
4147
|
const fieldSchema = getSchemaNodeForField(schema, fieldName);
|
|
4149
4148
|
const customMessage = fieldSchema?.errorMessages?.[error.keyword];
|
|
4149
|
+
// Debug log when error message is missing
|
|
4150
|
+
if (!customMessage) {
|
|
4151
|
+
console.debug(`[Form Validation] Missing error message for field '${fieldName}' with keyword '${error.keyword}'. Add errorMessages.${error.keyword} to schema for field '${fieldName}'`, {
|
|
4152
|
+
fieldName,
|
|
4153
|
+
keyword: error.keyword,
|
|
4154
|
+
instancePath: error.instancePath,
|
|
4155
|
+
schemaPath: error.schemaPath,
|
|
4156
|
+
params: error.params,
|
|
4157
|
+
fieldSchema: fieldSchema
|
|
4158
|
+
? {
|
|
4159
|
+
type: fieldSchema.type,
|
|
4160
|
+
errorMessages: fieldSchema.errorMessages,
|
|
4161
|
+
}
|
|
4162
|
+
: undefined,
|
|
4163
|
+
});
|
|
4164
|
+
}
|
|
4150
4165
|
// Provide helpful fallback message if no custom message is provided
|
|
4151
4166
|
const fallbackMessage = customMessage ||
|
|
4152
4167
|
`Missing error message for ${error.keyword}. Add errorMessages.${error.keyword} to schema for field '${fieldName}'`;
|
|
@@ -4276,7 +4291,7 @@ const idPickerSanityCheck = (column, foreign_key) => {
|
|
|
4276
4291
|
throw new Error(`The key column does not exist in properties of column ${column} when using id-picker.`);
|
|
4277
4292
|
}
|
|
4278
4293
|
};
|
|
4279
|
-
const FormRoot = ({ schema, idMap, setIdMap, form,
|
|
4294
|
+
const FormRoot = ({ schema, idMap, setIdMap, form, translate, children, order = [], ignore = [], include = [], onSubmit = undefined, rowNumber = undefined, requestOptions = {}, getUpdatedData = () => { }, customErrorRenderer, customSuccessRenderer, displayConfig = {
|
|
4280
4295
|
showSubmitButton: true,
|
|
4281
4296
|
showResetButton: true,
|
|
4282
4297
|
showTitle: true,
|
|
@@ -4317,9 +4332,11 @@ const FormRoot = ({ schema, idMap, setIdMap, form, serverUrl, translate, childre
|
|
|
4317
4332
|
}
|
|
4318
4333
|
};
|
|
4319
4334
|
const defaultSubmitPromise = (data) => {
|
|
4335
|
+
if (!requestOptions.url) {
|
|
4336
|
+
throw new Error('requestOptions.url is required when onSubmit is not provided');
|
|
4337
|
+
}
|
|
4320
4338
|
const options = {
|
|
4321
4339
|
method: 'POST',
|
|
4322
|
-
url: `${serverUrl}`,
|
|
4323
4340
|
data: clearEmptyString(data),
|
|
4324
4341
|
...requestOptions,
|
|
4325
4342
|
};
|
|
@@ -4337,7 +4354,6 @@ const FormRoot = ({ schema, idMap, setIdMap, form, serverUrl, translate, childre
|
|
|
4337
4354
|
};
|
|
4338
4355
|
return (jsxRuntime.jsx(SchemaFormContext.Provider, { value: {
|
|
4339
4356
|
schema,
|
|
4340
|
-
serverUrl,
|
|
4341
4357
|
order,
|
|
4342
4358
|
ignore,
|
|
4343
4359
|
include,
|
|
@@ -4377,39 +4393,31 @@ const FormRoot = ({ schema, idMap, setIdMap, form, serverUrl, translate, childre
|
|
|
4377
4393
|
}, children: jsxRuntime.jsx(reactHookForm.FormProvider, { ...form, children: children }) }));
|
|
4378
4394
|
};
|
|
4379
4395
|
|
|
4380
|
-
function removeIndex(str) {
|
|
4381
|
-
return str.replace(/\.\d+\./g, ".");
|
|
4382
|
-
}
|
|
4383
|
-
|
|
4384
4396
|
/**
|
|
4385
|
-
* Custom hook for form field labels
|
|
4386
|
-
* Automatically handles colLabel construction
|
|
4387
|
-
* Uses schema.title
|
|
4397
|
+
* Custom hook for form field labels.
|
|
4398
|
+
* Automatically handles colLabel construction.
|
|
4399
|
+
* Uses schema.title for labels and schema.errorMessages for error messages.
|
|
4388
4400
|
*
|
|
4389
4401
|
* @param column - The column name
|
|
4390
4402
|
* @param prefix - The prefix for the field (usually empty string or parent path)
|
|
4391
|
-
* @param schema -
|
|
4403
|
+
* @param schema - Required schema object with title and errorMessages properties
|
|
4392
4404
|
* @returns Object with label helper functions
|
|
4393
4405
|
*
|
|
4394
4406
|
* @example
|
|
4395
4407
|
* ```tsx
|
|
4396
4408
|
* const formI18n = useFormI18n(column, prefix, schema);
|
|
4397
4409
|
*
|
|
4398
|
-
* // Get field label (
|
|
4410
|
+
* // Get field label (from schema.title)
|
|
4399
4411
|
* <Field label={formI18n.label()} />
|
|
4400
4412
|
*
|
|
4401
|
-
* // Get required error message
|
|
4413
|
+
* // Get required error message (from schema.errorMessages?.required)
|
|
4402
4414
|
* <Text>{formI18n.required()}</Text>
|
|
4403
4415
|
*
|
|
4404
|
-
* // Get custom text
|
|
4405
|
-
* <Text>{formI18n.t('add_more')}</Text>
|
|
4406
|
-
*
|
|
4407
4416
|
* // Access the raw colLabel
|
|
4408
4417
|
* const colLabel = formI18n.colLabel;
|
|
4409
4418
|
* ```
|
|
4410
4419
|
*/
|
|
4411
4420
|
const useFormI18n = (column, prefix = '', schema) => {
|
|
4412
|
-
const { translate } = useSchemaContext();
|
|
4413
4421
|
const colLabel = `${prefix}${column}`;
|
|
4414
4422
|
return {
|
|
4415
4423
|
/**
|
|
@@ -4417,36 +4425,55 @@ const useFormI18n = (column, prefix = '', schema) => {
|
|
|
4417
4425
|
*/
|
|
4418
4426
|
colLabel,
|
|
4419
4427
|
/**
|
|
4420
|
-
* Get the field label from schema title
|
|
4421
|
-
*
|
|
4428
|
+
* Get the field label from schema title property.
|
|
4429
|
+
* Logs a debug message if title is missing.
|
|
4422
4430
|
*/
|
|
4423
|
-
label: (
|
|
4424
|
-
if (schema
|
|
4431
|
+
label: () => {
|
|
4432
|
+
if (schema.title) {
|
|
4425
4433
|
return schema.title;
|
|
4426
4434
|
}
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
+
// Debug log when field title is missing
|
|
4436
|
+
console.debug(`[Form Field Label] Missing title for field '${colLabel}'. Add title property to schema for field '${colLabel}'.`, {
|
|
4437
|
+
fieldName: column,
|
|
4438
|
+
colLabel,
|
|
4439
|
+
prefix,
|
|
4440
|
+
schema: {
|
|
4441
|
+
type: schema.type,
|
|
4442
|
+
errorMessages: schema.errorMessages
|
|
4443
|
+
? Object.keys(schema.errorMessages)
|
|
4444
|
+
: undefined,
|
|
4445
|
+
},
|
|
4446
|
+
});
|
|
4447
|
+
// Return column name as fallback
|
|
4448
|
+
return column;
|
|
4435
4449
|
},
|
|
4436
4450
|
/**
|
|
4437
|
-
* Get
|
|
4438
|
-
*
|
|
4439
|
-
*
|
|
4440
|
-
* @param key - The key suffix (e.g., 'add_more', 'total', etc.)
|
|
4441
|
-
* @param options - Optional options (e.g., defaultValue, interpolation variables)
|
|
4451
|
+
* Get the required error message from schema.errorMessages?.required.
|
|
4452
|
+
* Returns a helpful fallback message if not provided.
|
|
4442
4453
|
*/
|
|
4443
|
-
|
|
4444
|
-
|
|
4454
|
+
required: () => {
|
|
4455
|
+
const errorMessage = schema.errorMessages?.required;
|
|
4456
|
+
if (errorMessage) {
|
|
4457
|
+
return errorMessage;
|
|
4458
|
+
}
|
|
4459
|
+
// Debug log when error message is missing
|
|
4460
|
+
console.debug(`[Form Field Required] Missing error message for required field '${colLabel}'. Add errorMessages.required to schema for field '${colLabel}'.`, {
|
|
4461
|
+
fieldName: column,
|
|
4462
|
+
colLabel,
|
|
4463
|
+
prefix,
|
|
4464
|
+
schema: {
|
|
4465
|
+
type: schema.type,
|
|
4466
|
+
title: schema.title,
|
|
4467
|
+
required: schema.required,
|
|
4468
|
+
hasErrorMessages: !!schema.errorMessages,
|
|
4469
|
+
errorMessageKeys: schema.errorMessages
|
|
4470
|
+
? Object.keys(schema.errorMessages)
|
|
4471
|
+
: undefined,
|
|
4472
|
+
},
|
|
4473
|
+
});
|
|
4474
|
+
// Return helpful fallback message
|
|
4475
|
+
return `Missing error message for required. Add errorMessages.required to schema for field '${colLabel}'`;
|
|
4445
4476
|
},
|
|
4446
|
-
/**
|
|
4447
|
-
* Access to the original translate object for edge cases
|
|
4448
|
-
*/
|
|
4449
|
-
translate,
|
|
4450
4477
|
};
|
|
4451
4478
|
};
|
|
4452
4479
|
|
|
@@ -4521,52 +4548,56 @@ const Calendar = ({ calendars, getBackProps, getForwardProps, getDateProps, firs
|
|
|
4521
4548
|
const { labels } = React.useContext(DatePickerContext);
|
|
4522
4549
|
const { monthNamesShort, weekdayNamesShort, backButtonLabel, forwardButtonLabel, } = labels;
|
|
4523
4550
|
if (calendars.length) {
|
|
4524
|
-
return (jsxRuntime.jsxs(react.Grid, { children: [jsxRuntime.jsxs(react.Grid, { templateColumns: 'repeat(
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4551
|
+
return (jsxRuntime.jsx(react.Grid, { children: jsxRuntime.jsx(react.Grid, { templateColumns: 'repeat(2, auto)', justifyContent: 'center', children: calendars.map((calendar) => (jsxRuntime.jsxs(react.Grid, { gap: 2, children: [jsxRuntime.jsxs(react.Grid, { templateColumns: 'repeat(6, auto)', justifyContent: 'center', alignItems: 'center', gap: 2, children: [jsxRuntime.jsx(react.Button, { variant: 'ghost', size: 'sm', colorPalette: 'gray', ...getBackProps({ calendars }), children: '<' }), jsxRuntime.jsx(react.Text, { textAlign: 'center', children: monthNamesShort[calendar.month] }), jsxRuntime.jsx(react.Button, { variant: 'ghost', size: 'sm', colorPalette: 'gray', ...getForwardProps({ calendars }), children: '>' }), jsxRuntime.jsx(react.Button, { variant: 'ghost', size: 'sm', colorPalette: 'gray', ...getBackProps({
|
|
4552
|
+
calendars,
|
|
4553
|
+
offset: 12,
|
|
4554
|
+
}), children: '<' }), jsxRuntime.jsx(react.Text, { textAlign: 'center', children: calendar.year }), jsxRuntime.jsx(react.Button, { variant: 'ghost', size: 'sm', colorPalette: 'gray', ...getForwardProps({
|
|
4555
|
+
calendars,
|
|
4556
|
+
offset: 12,
|
|
4557
|
+
}), children: '>' })] }), jsxRuntime.jsxs(react.Grid, { templateColumns: 'repeat(7, auto)', justifyContent: 'center', children: [[0, 1, 2, 3, 4, 5, 6].map((weekdayNum) => {
|
|
4558
|
+
const weekday = (weekdayNum + firstDayOfWeek) % 7;
|
|
4559
|
+
return (jsxRuntime.jsx(react.Text, { textAlign: 'center', children: weekdayNamesShort[weekday] }, `${calendar.month}${calendar.year}${weekday}`));
|
|
4560
|
+
}), calendar.weeks.map((week, weekIndex) => week.map((dateObj, index) => {
|
|
4561
|
+
const key = `${calendar.month}${calendar.year}${weekIndex}${index}`;
|
|
4562
|
+
if (!dateObj) {
|
|
4563
|
+
return jsxRuntime.jsx(react.Grid, {}, key);
|
|
4564
|
+
}
|
|
4565
|
+
const { date, selected, selectable, today, isCurrentMonth, } = dateObj;
|
|
4566
|
+
const getDateColor = ({ today, selected, selectable, }) => {
|
|
4567
|
+
if (!selectable) {
|
|
4568
|
+
return 'gray';
|
|
4537
4569
|
}
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
};
|
|
4563
|
-
const color = getDateColor({ today, selected, selectable });
|
|
4564
|
-
const variant = getVariant({ today, selected, selectable });
|
|
4565
|
-
return (jsxRuntime.jsx(react.Button, { variant: variant, colorPalette: color, opacity: isCurrentMonth ? 1 : 0.4, ...getDateProps({ dateObj }), children: selectable ? date.getDate() : 'X' }, key));
|
|
4566
|
-
}))] })] }, `${calendar.month}${calendar.year}`))) })] }));
|
|
4570
|
+
if (selected) {
|
|
4571
|
+
return 'blue';
|
|
4572
|
+
}
|
|
4573
|
+
if (today) {
|
|
4574
|
+
return 'green';
|
|
4575
|
+
}
|
|
4576
|
+
return '';
|
|
4577
|
+
};
|
|
4578
|
+
const getVariant = ({ today, selected, selectable, }) => {
|
|
4579
|
+
if (!selectable) {
|
|
4580
|
+
return 'surface';
|
|
4581
|
+
}
|
|
4582
|
+
if (selected) {
|
|
4583
|
+
return 'surface';
|
|
4584
|
+
}
|
|
4585
|
+
if (today) {
|
|
4586
|
+
return 'outline';
|
|
4587
|
+
}
|
|
4588
|
+
return 'ghost';
|
|
4589
|
+
};
|
|
4590
|
+
const color = getDateColor({ today, selected, selectable });
|
|
4591
|
+
const variant = getVariant({ today, selected, selectable });
|
|
4592
|
+
return (jsxRuntime.jsx(react.Button, { variant: variant, colorPalette: color, size: 'xs', opacity: isCurrentMonth ? 1 : 0.4, ...getDateProps({ dateObj }), children: selectable ? date.getDate() : 'X' }, key));
|
|
4593
|
+
}))] })] }, `${calendar.month}${calendar.year}`))) }) }));
|
|
4567
4594
|
}
|
|
4568
4595
|
return null;
|
|
4569
4596
|
};
|
|
4597
|
+
|
|
4598
|
+
dayjs.extend(utc);
|
|
4599
|
+
dayjs.extend(timezone);
|
|
4600
|
+
dayjs.extend(customParseFormat);
|
|
4570
4601
|
const DatePickerContext = React.createContext({
|
|
4571
4602
|
labels: {
|
|
4572
4603
|
monthNamesShort: [
|
|
@@ -4586,6 +4617,9 @@ const DatePickerContext = React.createContext({
|
|
|
4586
4617
|
weekdayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
|
4587
4618
|
backButtonLabel: 'Back',
|
|
4588
4619
|
forwardButtonLabel: 'Next',
|
|
4620
|
+
todayLabel: 'Today',
|
|
4621
|
+
yesterdayLabel: 'Yesterday',
|
|
4622
|
+
tomorrowLabel: 'Tomorrow',
|
|
4589
4623
|
},
|
|
4590
4624
|
});
|
|
4591
4625
|
const DatePicker$1 = ({ labels = {
|
|
@@ -4606,6 +4640,9 @@ const DatePicker$1 = ({ labels = {
|
|
|
4606
4640
|
weekdayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
|
4607
4641
|
backButtonLabel: 'Back',
|
|
4608
4642
|
forwardButtonLabel: 'Next',
|
|
4643
|
+
todayLabel: 'Today',
|
|
4644
|
+
yesterdayLabel: 'Yesterday',
|
|
4645
|
+
tomorrowLabel: 'Tomorrow',
|
|
4609
4646
|
}, onDateSelected, selected, firstDayOfWeek, showOutsideDays, date, minDate, maxDate, monthsToDisplay, render, }) => {
|
|
4610
4647
|
const calendarData = useCalendar({
|
|
4611
4648
|
onDateSelected,
|
|
@@ -4620,9 +4657,171 @@ const DatePicker$1 = ({ labels = {
|
|
|
4620
4657
|
return (jsxRuntime.jsx(DatePickerContext.Provider, { value: { labels }, children: render ? (render(calendarData)) : (jsxRuntime.jsx(Calendar, { ...calendarData,
|
|
4621
4658
|
firstDayOfWeek })) }));
|
|
4622
4659
|
};
|
|
4660
|
+
function DatePickerInput({ value, onChange, placeholder = 'Select a date', dateFormat = 'YYYY-MM-DD', displayFormat = 'YYYY-MM-DD', labels = {
|
|
4661
|
+
monthNamesShort: [
|
|
4662
|
+
'Jan',
|
|
4663
|
+
'Feb',
|
|
4664
|
+
'Mar',
|
|
4665
|
+
'Apr',
|
|
4666
|
+
'May',
|
|
4667
|
+
'Jun',
|
|
4668
|
+
'Jul',
|
|
4669
|
+
'Aug',
|
|
4670
|
+
'Sep',
|
|
4671
|
+
'Oct',
|
|
4672
|
+
'Nov',
|
|
4673
|
+
'Dec',
|
|
4674
|
+
],
|
|
4675
|
+
weekdayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
|
4676
|
+
backButtonLabel: 'Back',
|
|
4677
|
+
forwardButtonLabel: 'Next',
|
|
4678
|
+
todayLabel: 'Today',
|
|
4679
|
+
yesterdayLabel: 'Yesterday',
|
|
4680
|
+
tomorrowLabel: 'Tomorrow',
|
|
4681
|
+
}, timezone = 'Asia/Hong_Kong', minDate, maxDate, firstDayOfWeek, showOutsideDays, monthsToDisplay = 1, insideDialog = false, readOnly = false, showHelperButtons = true, }) {
|
|
4682
|
+
const [open, setOpen] = React.useState(false);
|
|
4683
|
+
const [inputValue, setInputValue] = React.useState('');
|
|
4684
|
+
// Sync inputValue with value prop changes
|
|
4685
|
+
React.useEffect(() => {
|
|
4686
|
+
if (value) {
|
|
4687
|
+
const formatted = typeof value === 'string'
|
|
4688
|
+
? dayjs(value).tz(timezone).isValid()
|
|
4689
|
+
? dayjs(value).tz(timezone).format(displayFormat)
|
|
4690
|
+
: ''
|
|
4691
|
+
: dayjs(value).tz(timezone).format(displayFormat);
|
|
4692
|
+
setInputValue(formatted);
|
|
4693
|
+
}
|
|
4694
|
+
else {
|
|
4695
|
+
setInputValue('');
|
|
4696
|
+
}
|
|
4697
|
+
}, [value, timezone, displayFormat]);
|
|
4698
|
+
// Convert value to Date object for DatePicker
|
|
4699
|
+
const selectedDate = value
|
|
4700
|
+
? typeof value === 'string'
|
|
4701
|
+
? dayjs(value).tz(timezone).isValid()
|
|
4702
|
+
? dayjs(value).tz(timezone).toDate()
|
|
4703
|
+
: new Date()
|
|
4704
|
+
: value
|
|
4705
|
+
: new Date();
|
|
4706
|
+
// Shared function to parse and validate input value
|
|
4707
|
+
const parseAndValidateInput = (inputVal) => {
|
|
4708
|
+
// If empty, clear the value
|
|
4709
|
+
if (!inputVal.trim()) {
|
|
4710
|
+
onChange?.(undefined);
|
|
4711
|
+
setInputValue('');
|
|
4712
|
+
return;
|
|
4713
|
+
}
|
|
4714
|
+
// Try parsing with displayFormat first
|
|
4715
|
+
let parsedDate = dayjs(inputVal, displayFormat, true);
|
|
4716
|
+
// If that fails, try common date formats
|
|
4717
|
+
if (!parsedDate.isValid()) {
|
|
4718
|
+
parsedDate = dayjs(inputVal);
|
|
4719
|
+
}
|
|
4720
|
+
// If still invalid, try parsing with dateFormat
|
|
4721
|
+
if (!parsedDate.isValid()) {
|
|
4722
|
+
parsedDate = dayjs(inputVal, dateFormat, true);
|
|
4723
|
+
}
|
|
4724
|
+
// If valid, check constraints and update
|
|
4725
|
+
if (parsedDate.isValid()) {
|
|
4726
|
+
const dateObj = parsedDate.tz(timezone).toDate();
|
|
4727
|
+
// Check min/max constraints
|
|
4728
|
+
if (minDate && dateObj < minDate) {
|
|
4729
|
+
// Invalid: before minDate, reset to prop value
|
|
4730
|
+
resetToPropValue();
|
|
4731
|
+
return;
|
|
4732
|
+
}
|
|
4733
|
+
if (maxDate && dateObj > maxDate) {
|
|
4734
|
+
// Invalid: after maxDate, reset to prop value
|
|
4735
|
+
resetToPropValue();
|
|
4736
|
+
return;
|
|
4737
|
+
}
|
|
4738
|
+
// Valid date - format and update
|
|
4739
|
+
const formattedDate = parsedDate.tz(timezone).format(dateFormat);
|
|
4740
|
+
const formattedDisplay = parsedDate.tz(timezone).format(displayFormat);
|
|
4741
|
+
onChange?.(formattedDate);
|
|
4742
|
+
setInputValue(formattedDisplay);
|
|
4743
|
+
}
|
|
4744
|
+
else {
|
|
4745
|
+
// Invalid date - reset to prop value
|
|
4746
|
+
resetToPropValue();
|
|
4747
|
+
}
|
|
4748
|
+
};
|
|
4749
|
+
// Helper function to reset input to prop value
|
|
4750
|
+
const resetToPropValue = () => {
|
|
4751
|
+
if (value) {
|
|
4752
|
+
const formatted = typeof value === 'string'
|
|
4753
|
+
? dayjs(value).tz(timezone).isValid()
|
|
4754
|
+
? dayjs(value).tz(timezone).format(displayFormat)
|
|
4755
|
+
: ''
|
|
4756
|
+
: dayjs(value).tz(timezone).format(displayFormat);
|
|
4757
|
+
setInputValue(formatted);
|
|
4758
|
+
}
|
|
4759
|
+
else {
|
|
4760
|
+
setInputValue('');
|
|
4761
|
+
}
|
|
4762
|
+
};
|
|
4763
|
+
const handleInputChange = (e) => {
|
|
4764
|
+
// Only update the input value, don't parse yet
|
|
4765
|
+
setInputValue(e.target.value);
|
|
4766
|
+
};
|
|
4767
|
+
const handleInputBlur = () => {
|
|
4768
|
+
// Parse and validate when input loses focus
|
|
4769
|
+
parseAndValidateInput(inputValue);
|
|
4770
|
+
};
|
|
4771
|
+
const handleKeyDown = (e) => {
|
|
4772
|
+
// Parse and validate when Enter is pressed
|
|
4773
|
+
if (e.key === 'Enter') {
|
|
4774
|
+
e.preventDefault();
|
|
4775
|
+
parseAndValidateInput(inputValue);
|
|
4776
|
+
}
|
|
4777
|
+
};
|
|
4778
|
+
const handleDateSelected = ({ date }) => {
|
|
4779
|
+
console.debug('[DatePickerInput] handleDateSelected called:', {
|
|
4780
|
+
date: date.toISOString(),
|
|
4781
|
+
timezone,
|
|
4782
|
+
dateFormat,
|
|
4783
|
+
formattedDate: dayjs(date).tz(timezone).format(dateFormat),
|
|
4784
|
+
});
|
|
4785
|
+
const formattedDate = dayjs(date).tz(timezone).format(dateFormat);
|
|
4786
|
+
console.debug('[DatePickerInput] Calling onChange with formatted date:', formattedDate);
|
|
4787
|
+
onChange?.(formattedDate);
|
|
4788
|
+
setOpen(false);
|
|
4789
|
+
};
|
|
4790
|
+
// Helper function to get dates in the correct timezone
|
|
4791
|
+
const getToday = () => dayjs().tz(timezone).startOf('day').toDate();
|
|
4792
|
+
const getYesterday = () => dayjs().tz(timezone).subtract(1, 'day').startOf('day').toDate();
|
|
4793
|
+
const getTomorrow = () => dayjs().tz(timezone).add(1, 'day').startOf('day').toDate();
|
|
4794
|
+
// Check if a date is within min/max constraints
|
|
4795
|
+
const isDateValid = (date) => {
|
|
4796
|
+
if (minDate) {
|
|
4797
|
+
const minDateStart = dayjs(minDate).tz(timezone).startOf('day').toDate();
|
|
4798
|
+
const dateStart = dayjs(date).tz(timezone).startOf('day').toDate();
|
|
4799
|
+
if (dateStart < minDateStart)
|
|
4800
|
+
return false;
|
|
4801
|
+
}
|
|
4802
|
+
if (maxDate) {
|
|
4803
|
+
const maxDateStart = dayjs(maxDate).tz(timezone).startOf('day').toDate();
|
|
4804
|
+
const dateStart = dayjs(date).tz(timezone).startOf('day').toDate();
|
|
4805
|
+
if (dateStart > maxDateStart)
|
|
4806
|
+
return false;
|
|
4807
|
+
}
|
|
4808
|
+
return true;
|
|
4809
|
+
};
|
|
4810
|
+
const handleHelperButtonClick = (date) => {
|
|
4811
|
+
if (isDateValid(date)) {
|
|
4812
|
+
handleDateSelected({ date });
|
|
4813
|
+
}
|
|
4814
|
+
};
|
|
4815
|
+
const today = getToday();
|
|
4816
|
+
const yesterday = getYesterday();
|
|
4817
|
+
const tomorrow = getTomorrow();
|
|
4818
|
+
const datePickerContent = (jsxRuntime.jsxs(react.Grid, { gap: 2, children: [showHelperButtons && (jsxRuntime.jsxs(react.Grid, { templateColumns: "repeat(3, 1fr)", gap: 2, children: [jsxRuntime.jsx(react.Button, { size: "sm", variant: "outline", onClick: () => handleHelperButtonClick(yesterday), disabled: !isDateValid(yesterday), children: labels.yesterdayLabel ?? 'Yesterday' }), jsxRuntime.jsx(react.Button, { size: "sm", variant: "outline", onClick: () => handleHelperButtonClick(today), disabled: !isDateValid(today), children: labels.todayLabel ?? 'Today' }), jsxRuntime.jsx(react.Button, { size: "sm", variant: "outline", onClick: () => handleHelperButtonClick(tomorrow), disabled: !isDateValid(tomorrow), children: labels.tomorrowLabel ?? 'Tomorrow' })] })), jsxRuntime.jsx(DatePicker$1, { selected: selectedDate, onDateSelected: handleDateSelected, labels: labels, minDate: minDate, maxDate: maxDate, firstDayOfWeek: firstDayOfWeek, showOutsideDays: showOutsideDays, monthsToDisplay: monthsToDisplay })] }));
|
|
4819
|
+
return (jsxRuntime.jsxs(react.Popover.Root, { open: open, onOpenChange: (e) => setOpen(e.open), closeOnInteractOutside: true, autoFocus: false, children: [jsxRuntime.jsx(InputGroup, { endElement: jsxRuntime.jsx(react.Popover.Trigger, { asChild: true, children: jsxRuntime.jsx(react.IconButton, { variant: "ghost", size: "2xs", "aria-label": "Open calendar", onClick: () => setOpen(true), children: jsxRuntime.jsx(react.Icon, { children: jsxRuntime.jsx(md.MdDateRange, {}) }) }) }), children: jsxRuntime.jsx(react.Input, { value: inputValue, onChange: handleInputChange, onBlur: handleInputBlur, onKeyDown: handleKeyDown, placeholder: placeholder, readOnly: readOnly }) }), insideDialog ? (jsxRuntime.jsx(react.Popover.Positioner, { children: jsxRuntime.jsx(react.Popover.Content, { width: "fit-content", minH: "25rem", children: jsxRuntime.jsx(react.Popover.Body, { children: datePickerContent }) }) })) : (jsxRuntime.jsx(react.Portal, { children: jsxRuntime.jsx(react.Popover.Positioner, { children: jsxRuntime.jsx(react.Popover.Content, { width: "fit-content", minH: "25rem", children: jsxRuntime.jsx(react.Popover.Body, { children: datePickerContent }) }) }) }))] }));
|
|
4820
|
+
}
|
|
4623
4821
|
|
|
4624
4822
|
dayjs.extend(utc);
|
|
4625
4823
|
dayjs.extend(timezone);
|
|
4824
|
+
dayjs.extend(customParseFormat);
|
|
4626
4825
|
const DatePicker = ({ column, schema, prefix }) => {
|
|
4627
4826
|
const { watch, formState: { errors }, setValue, } = reactHookForm.useFormContext();
|
|
4628
4827
|
const { timezone, dateTimePickerLabels, insideDialog } = useSchemaContext();
|
|
@@ -4631,15 +4830,29 @@ const DatePicker = ({ column, schema, prefix }) => {
|
|
|
4631
4830
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
4632
4831
|
const colLabel = formI18n.colLabel;
|
|
4633
4832
|
const [open, setOpen] = React.useState(false);
|
|
4833
|
+
const [inputValue, setInputValue] = React.useState('');
|
|
4634
4834
|
const selectedDate = watch(colLabel);
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4835
|
+
// Update input value when form value changes
|
|
4836
|
+
React.useEffect(() => {
|
|
4837
|
+
if (selectedDate) {
|
|
4838
|
+
const parsedDate = dayjs(selectedDate).tz(timezone);
|
|
4839
|
+
if (parsedDate.isValid()) {
|
|
4840
|
+
const formatted = parsedDate.format(displayDateFormat);
|
|
4841
|
+
setInputValue(formatted);
|
|
4842
|
+
}
|
|
4843
|
+
else {
|
|
4844
|
+
setInputValue('');
|
|
4845
|
+
}
|
|
4846
|
+
}
|
|
4847
|
+
else {
|
|
4848
|
+
setInputValue('');
|
|
4849
|
+
}
|
|
4850
|
+
}, [selectedDate, displayDateFormat, timezone]);
|
|
4851
|
+
// Format and validate existing value
|
|
4638
4852
|
React.useEffect(() => {
|
|
4639
4853
|
try {
|
|
4640
4854
|
if (selectedDate) {
|
|
4641
4855
|
// Parse the selectedDate as UTC or in a specific timezone to avoid +8 hour shift
|
|
4642
|
-
// For example, parse as UTC:
|
|
4643
4856
|
const parsedDate = dayjs(selectedDate).tz(timezone);
|
|
4644
4857
|
if (!parsedDate.isValid())
|
|
4645
4858
|
return;
|
|
@@ -4657,7 +4870,7 @@ const DatePicker = ({ column, schema, prefix }) => {
|
|
|
4657
4870
|
catch (e) {
|
|
4658
4871
|
console.error(e);
|
|
4659
4872
|
}
|
|
4660
|
-
}, [selectedDate, dateFormat, colLabel, setValue]);
|
|
4873
|
+
}, [selectedDate, dateFormat, colLabel, setValue, timezone]);
|
|
4661
4874
|
const datePickerLabels = {
|
|
4662
4875
|
monthNamesShort: dateTimePickerLabels?.monthNamesShort ?? [
|
|
4663
4876
|
'January',
|
|
@@ -4685,14 +4898,92 @@ const DatePicker = ({ column, schema, prefix }) => {
|
|
|
4685
4898
|
backButtonLabel: dateTimePickerLabels?.backButtonLabel ?? 'Back',
|
|
4686
4899
|
forwardButtonLabel: dateTimePickerLabels?.forwardButtonLabel ?? 'Forward',
|
|
4687
4900
|
};
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4901
|
+
// Convert value to Date object for DatePicker
|
|
4902
|
+
const selectedDateObj = selectedDate
|
|
4903
|
+
? dayjs(selectedDate).tz(timezone).isValid()
|
|
4904
|
+
? dayjs(selectedDate).tz(timezone).toDate()
|
|
4905
|
+
: new Date()
|
|
4906
|
+
: new Date();
|
|
4907
|
+
// Shared function to parse and validate input value
|
|
4908
|
+
const parseAndValidateInput = (inputVal) => {
|
|
4909
|
+
// If empty, clear the value
|
|
4910
|
+
if (!inputVal.trim()) {
|
|
4911
|
+
setValue(colLabel, undefined, {
|
|
4912
|
+
shouldValidate: true,
|
|
4913
|
+
shouldDirty: true,
|
|
4914
|
+
});
|
|
4915
|
+
setInputValue('');
|
|
4916
|
+
return;
|
|
4917
|
+
}
|
|
4918
|
+
// Try parsing with displayDateFormat first
|
|
4919
|
+
let parsedDate = dayjs(inputVal, displayDateFormat, true);
|
|
4920
|
+
// If that fails, try common date formats
|
|
4921
|
+
if (!parsedDate.isValid()) {
|
|
4922
|
+
parsedDate = dayjs(inputVal);
|
|
4923
|
+
}
|
|
4924
|
+
// If still invalid, try parsing with dateFormat
|
|
4925
|
+
if (!parsedDate.isValid()) {
|
|
4926
|
+
parsedDate = dayjs(inputVal, dateFormat, true);
|
|
4927
|
+
}
|
|
4928
|
+
// If valid, format and update
|
|
4929
|
+
if (parsedDate.isValid()) {
|
|
4930
|
+
const formattedDate = parsedDate.tz(timezone).format(dateFormat);
|
|
4931
|
+
const formattedDisplay = parsedDate
|
|
4932
|
+
.tz(timezone)
|
|
4933
|
+
.format(displayDateFormat);
|
|
4934
|
+
setValue(colLabel, formattedDate, {
|
|
4935
|
+
shouldValidate: true,
|
|
4936
|
+
shouldDirty: true,
|
|
4937
|
+
});
|
|
4938
|
+
setInputValue(formattedDisplay);
|
|
4939
|
+
}
|
|
4940
|
+
else {
|
|
4941
|
+
// Invalid date - reset to prop value
|
|
4942
|
+
resetToPropValue();
|
|
4943
|
+
}
|
|
4944
|
+
};
|
|
4945
|
+
// Helper function to reset input to prop value
|
|
4946
|
+
const resetToPropValue = () => {
|
|
4947
|
+
if (selectedDate) {
|
|
4948
|
+
const parsedDate = dayjs(selectedDate).tz(timezone);
|
|
4949
|
+
if (parsedDate.isValid()) {
|
|
4950
|
+
const formatted = parsedDate.format(displayDateFormat);
|
|
4951
|
+
setInputValue(formatted);
|
|
4952
|
+
}
|
|
4953
|
+
else {
|
|
4954
|
+
setInputValue('');
|
|
4955
|
+
}
|
|
4956
|
+
}
|
|
4957
|
+
else {
|
|
4958
|
+
setInputValue('');
|
|
4959
|
+
}
|
|
4960
|
+
};
|
|
4961
|
+
const handleInputChange = (e) => {
|
|
4962
|
+
// Only update the input value, don't parse yet
|
|
4963
|
+
setInputValue(e.target.value);
|
|
4964
|
+
};
|
|
4965
|
+
const handleInputBlur = () => {
|
|
4966
|
+
// Parse and validate when input loses focus
|
|
4967
|
+
parseAndValidateInput(inputValue);
|
|
4968
|
+
};
|
|
4969
|
+
const handleKeyDown = (e) => {
|
|
4970
|
+
// Parse and validate when Enter is pressed
|
|
4971
|
+
if (e.key === 'Enter') {
|
|
4972
|
+
e.preventDefault();
|
|
4973
|
+
parseAndValidateInput(inputValue);
|
|
4974
|
+
}
|
|
4975
|
+
};
|
|
4976
|
+
const handleDateSelected = ({ date }) => {
|
|
4977
|
+
const formattedDate = dayjs(date).tz(timezone).format(dateFormat);
|
|
4978
|
+
setValue(colLabel, formattedDate, {
|
|
4979
|
+
shouldValidate: true,
|
|
4980
|
+
shouldDirty: true,
|
|
4981
|
+
});
|
|
4982
|
+
setOpen(false);
|
|
4983
|
+
};
|
|
4984
|
+
const datePickerContent = (jsxRuntime.jsx(DatePicker$1, { selected: selectedDateObj, onDateSelected: handleDateSelected, labels: datePickerLabels }));
|
|
4692
4985
|
return (jsxRuntime.jsx(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
4693
|
-
gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: jsxRuntime.jsxs(react.Popover.Root, { open: open, onOpenChange: (e) => setOpen(e.open), closeOnInteractOutside: true, children: [jsxRuntime.jsx(react.Popover.Trigger, { asChild: true, children: jsxRuntime.
|
|
4694
|
-
setOpen(true);
|
|
4695
|
-
}, justifyContent: 'start', children: [jsxRuntime.jsx(md.MdDateRange, {}), selectedDate !== undefined ? `${displayDate}` : ''] }) }), insideDialog ? (jsxRuntime.jsx(react.Popover.Positioner, { children: jsxRuntime.jsx(react.Popover.Content, { width: "fit-content", minH: "25rem", children: jsxRuntime.jsx(react.Popover.Body, { children: datePickerContent }) }) })) : (jsxRuntime.jsx(react.Portal, { children: jsxRuntime.jsx(react.Popover.Positioner, { children: jsxRuntime.jsx(react.Popover.Content, { width: "fit-content", minH: "25rem", children: jsxRuntime.jsx(react.Popover.Body, { children: datePickerContent }) }) }) }))] }) }));
|
|
4986
|
+
gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: jsxRuntime.jsxs(react.Popover.Root, { open: open, onOpenChange: (e) => setOpen(e.open), closeOnInteractOutside: true, autoFocus: false, children: [jsxRuntime.jsx(InputGroup, { endElement: jsxRuntime.jsx(react.Popover.Trigger, { asChild: true, children: jsxRuntime.jsx(react.IconButton, { variant: "ghost", size: "2xs", "aria-label": "Open calendar", onClick: () => setOpen(true), children: jsxRuntime.jsx(react.Icon, { children: jsxRuntime.jsx(md.MdDateRange, {}) }) }) }), children: jsxRuntime.jsx(react.Input, { value: inputValue, onChange: handleInputChange, onBlur: handleInputBlur, onKeyDown: handleKeyDown, placeholder: formI18n.label(), size: "sm" }) }), insideDialog ? (jsxRuntime.jsx(react.Popover.Positioner, { children: jsxRuntime.jsx(react.Popover.Content, { width: "fit-content", minH: "25rem", children: jsxRuntime.jsx(react.Popover.Body, { children: datePickerContent }) }) })) : (jsxRuntime.jsx(react.Portal, { children: jsxRuntime.jsx(react.Popover.Positioner, { children: jsxRuntime.jsx(react.Popover.Content, { width: "fit-content", minH: "25rem", children: jsxRuntime.jsx(react.Popover.Body, { children: datePickerContent }) }) }) }))] }) }));
|
|
4696
4987
|
};
|
|
4697
4988
|
|
|
4698
4989
|
dayjs.extend(utc);
|
|
@@ -4700,7 +4991,7 @@ dayjs.extend(timezone);
|
|
|
4700
4991
|
const DateRangePicker = ({ column, schema, prefix, }) => {
|
|
4701
4992
|
const { watch, formState: { errors }, setValue, } = reactHookForm.useFormContext();
|
|
4702
4993
|
const { timezone, insideDialog } = useSchemaContext();
|
|
4703
|
-
const formI18n = useFormI18n(column, prefix);
|
|
4994
|
+
const formI18n = useFormI18n(column, prefix, schema);
|
|
4704
4995
|
const { required, gridColumn = 'span 12', gridRow = 'span 1', displayDateFormat = 'YYYY-MM-DD', dateFormat = 'YYYY-MM-DD', } = schema;
|
|
4705
4996
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
4706
4997
|
const colLabel = formI18n.colLabel;
|
|
@@ -4806,27 +5097,82 @@ const EnumPicker = ({ column, isMultiple = false, schema, prefix, showTotalAndLi
|
|
|
4806
5097
|
const watchEnum = watch(colLabel);
|
|
4807
5098
|
const watchEnums = (watch(colLabel) ?? []);
|
|
4808
5099
|
const dataList = schema.enum ?? [];
|
|
5100
|
+
// Helper function to render enum value (returns ReactNode)
|
|
5101
|
+
// If renderDisplay is provided, use it; otherwise show the enum string value directly
|
|
5102
|
+
const renderEnumValue = (value) => {
|
|
5103
|
+
if (renderDisplay) {
|
|
5104
|
+
return renderDisplay(value);
|
|
5105
|
+
}
|
|
5106
|
+
// If no renderDisplay provided, show the enum string value directly
|
|
5107
|
+
return value;
|
|
5108
|
+
};
|
|
5109
|
+
// Helper function to get string representation for input display
|
|
5110
|
+
// Converts ReactNode to string for combobox input display
|
|
5111
|
+
const getDisplayString = (value) => {
|
|
5112
|
+
if (renderDisplay) {
|
|
5113
|
+
const rendered = renderDisplay(value);
|
|
5114
|
+
// If renderDisplay returns a string, use it directly
|
|
5115
|
+
if (typeof rendered === 'string') {
|
|
5116
|
+
return rendered;
|
|
5117
|
+
}
|
|
5118
|
+
// If it's a React element, try to extract text content
|
|
5119
|
+
// For now, fallback to the raw value if we can't extract text
|
|
5120
|
+
// In most cases, renderDisplay should return a string or simple element
|
|
5121
|
+
if (typeof rendered === 'object' &&
|
|
5122
|
+
rendered !== null &&
|
|
5123
|
+
'props' in rendered) {
|
|
5124
|
+
const props = rendered.props;
|
|
5125
|
+
// Try to extract text from React element props
|
|
5126
|
+
if (props?.children) {
|
|
5127
|
+
const children = props.children;
|
|
5128
|
+
if (typeof children === 'string') {
|
|
5129
|
+
return children;
|
|
5130
|
+
}
|
|
5131
|
+
}
|
|
5132
|
+
}
|
|
5133
|
+
// Fallback: use raw value if we can't extract string
|
|
5134
|
+
return value;
|
|
5135
|
+
}
|
|
5136
|
+
return value;
|
|
5137
|
+
};
|
|
5138
|
+
// Debug log when renderDisplay is missing
|
|
5139
|
+
if (!renderDisplay) {
|
|
5140
|
+
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.`, {
|
|
5141
|
+
fieldName: column,
|
|
5142
|
+
colLabel,
|
|
5143
|
+
prefix,
|
|
5144
|
+
enumValues: dataList,
|
|
5145
|
+
});
|
|
5146
|
+
}
|
|
4809
5147
|
// Current value for combobox (array format)
|
|
4810
5148
|
const currentValue = isMultiple
|
|
4811
5149
|
? watchEnums.filter((val) => val != null && val !== '')
|
|
4812
5150
|
: watchEnum
|
|
4813
5151
|
? [watchEnum]
|
|
4814
5152
|
: [];
|
|
4815
|
-
//
|
|
5153
|
+
// Track input focus state for single selection
|
|
5154
|
+
const [isInputFocused, setIsInputFocused] = React.useState(false);
|
|
5155
|
+
// Get the selected value for single selection display
|
|
5156
|
+
const selectedSingleValue = !isMultiple && watchEnum ? watchEnum : null;
|
|
5157
|
+
const selectedSingleRendered = selectedSingleValue
|
|
5158
|
+
? renderEnumValue(selectedSingleValue)
|
|
5159
|
+
: null;
|
|
5160
|
+
const isSelectedSingleValueString = typeof selectedSingleRendered === 'string';
|
|
4816
5161
|
const comboboxItems = React.useMemo(() => {
|
|
4817
5162
|
return dataList.map((item) => ({
|
|
4818
|
-
label:
|
|
4819
|
-
? String(renderDisplay(item))
|
|
4820
|
-
: formI18n.t(item),
|
|
5163
|
+
label: item, // Internal: used for search/filtering only
|
|
4821
5164
|
value: item,
|
|
5165
|
+
raw: item, // Passed to renderEnumValue for UI rendering
|
|
5166
|
+
displayLabel: getDisplayString(item), // Used for input display when selected
|
|
4822
5167
|
}));
|
|
4823
|
-
}, [dataList, renderDisplay
|
|
5168
|
+
}, [dataList, renderDisplay]);
|
|
4824
5169
|
// Use filter hook for combobox
|
|
4825
5170
|
const { contains } = react.useFilter({ sensitivity: 'base' });
|
|
4826
5171
|
// Create collection for combobox
|
|
5172
|
+
// itemToString uses displayLabel to show rendered display in input when selected
|
|
4827
5173
|
const { collection, filter } = react.useListCollection({
|
|
4828
5174
|
initialItems: comboboxItems,
|
|
4829
|
-
itemToString: (item) => item.
|
|
5175
|
+
itemToString: (item) => item.displayLabel, // Use displayLabel for selected value display
|
|
4830
5176
|
itemToValue: (item) => item.value,
|
|
4831
5177
|
filter: contains,
|
|
4832
5178
|
});
|
|
@@ -4850,9 +5196,7 @@ const EnumPicker = ({ column, isMultiple = false, schema, prefix, showTotalAndLi
|
|
|
4850
5196
|
setValue(colLabel, details.value);
|
|
4851
5197
|
}
|
|
4852
5198
|
}, children: jsxRuntime.jsx(react.HStack, { gap: "6", children: dataList.map((item) => {
|
|
4853
|
-
return (jsxRuntime.jsxs(react.RadioGroup.Item, { value: item, children: [jsxRuntime.jsx(react.RadioGroup.ItemHiddenInput, {}), jsxRuntime.jsx(react.RadioGroup.ItemIndicator, {}), jsxRuntime.jsx(react.RadioGroup.ItemText, { children:
|
|
4854
|
-
? renderDisplay(item)
|
|
4855
|
-
: formI18n.t(item) })] }, `${colLabel}-${item}`));
|
|
5199
|
+
return (jsxRuntime.jsxs(react.RadioGroup.Item, { value: item, children: [jsxRuntime.jsx(react.RadioGroup.ItemHiddenInput, {}), jsxRuntime.jsx(react.RadioGroup.ItemIndicator, {}), jsxRuntime.jsx(react.RadioGroup.ItemText, { children: renderEnumValue(item) })] }, `${colLabel}-${item}`));
|
|
4856
5200
|
}) }) }) }));
|
|
4857
5201
|
}
|
|
4858
5202
|
return (jsxRuntime.jsxs(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
@@ -4863,16 +5207,31 @@ const EnumPicker = ({ column, isMultiple = false, schema, prefix, showTotalAndLi
|
|
|
4863
5207
|
return (jsxRuntime.jsx(Tag, { size: "lg", closable: true, onClick: () => {
|
|
4864
5208
|
const newValue = currentValue.filter((val) => val !== enumValue);
|
|
4865
5209
|
setValue(colLabel, newValue);
|
|
4866
|
-
}, children:
|
|
4867
|
-
? renderDisplay(enumValue)
|
|
4868
|
-
: formI18n.t(enumValue) }, enumValue));
|
|
5210
|
+
}, children: renderEnumValue(enumValue) }, enumValue));
|
|
4869
5211
|
}) })), jsxRuntime.jsxs(react.Combobox.Root, { collection: collection, value: currentValue, onValueChange: handleValueChange, onInputValueChange: handleInputValueChange, multiple: isMultiple, closeOnSelect: !isMultiple, openOnClick: true, invalid: !!errors[colLabel], width: "100%", positioning: insideDialog
|
|
4870
5212
|
? { strategy: 'fixed', hideWhenDetached: true }
|
|
4871
|
-
: undefined, children: [jsxRuntime.jsxs(react.Combobox.Control, {
|
|
5213
|
+
: undefined, children: [jsxRuntime.jsxs(react.Combobox.Control, { position: "relative", children: [!isMultiple &&
|
|
5214
|
+
selectedSingleValue &&
|
|
5215
|
+
!isInputFocused &&
|
|
5216
|
+
!isSelectedSingleValueString &&
|
|
5217
|
+
selectedSingleRendered && (jsxRuntime.jsx(react.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 })), jsxRuntime.jsx(react.Combobox.Input, { placeholder: !isMultiple && selectedSingleValue && !isInputFocused
|
|
5218
|
+
? undefined
|
|
5219
|
+
: enumPickerLabels?.typeToSearch ?? 'Type to search', onFocus: () => setIsInputFocused(true), onBlur: () => setIsInputFocused(false), style: {
|
|
5220
|
+
color: !isMultiple &&
|
|
5221
|
+
selectedSingleValue &&
|
|
5222
|
+
!isInputFocused &&
|
|
5223
|
+
!isSelectedSingleValueString
|
|
5224
|
+
? 'transparent'
|
|
5225
|
+
: undefined,
|
|
5226
|
+
caretColor: !isMultiple &&
|
|
5227
|
+
selectedSingleValue &&
|
|
5228
|
+
!isInputFocused &&
|
|
5229
|
+
!isSelectedSingleValueString
|
|
5230
|
+
? 'transparent'
|
|
5231
|
+
: undefined,
|
|
5232
|
+
} }), jsxRuntime.jsxs(react.Combobox.IndicatorGroup, { children: [!isMultiple && currentValue.length > 0 && (jsxRuntime.jsx(react.Combobox.ClearTrigger, { onClick: () => {
|
|
4872
5233
|
setValue(colLabel, '');
|
|
4873
|
-
} })), jsxRuntime.jsx(react.Combobox.Trigger, {})] })] }), insideDialog ? (jsxRuntime.jsx(react.Combobox.Positioner, { children: jsxRuntime.jsxs(react.Combobox.Content, { children: [showTotalAndLimit && (jsxRuntime.jsx(react.Text, { p: 2, fontSize: "sm", color: "fg.muted", children: `${enumPickerLabels?.total ??
|
|
4874
|
-
formI18n.t('empty_search_result') })) : (jsxRuntime.jsx(jsxRuntime.Fragment, { children: collection.items.map((item, index) => (jsxRuntime.jsxs(react.Combobox.Item, { item: item, children: [jsxRuntime.jsx(react.Combobox.ItemText, { children: item.label }), jsxRuntime.jsx(react.Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) }))] }) })) : (jsxRuntime.jsx(react.Portal, { children: jsxRuntime.jsx(react.Combobox.Positioner, { children: jsxRuntime.jsxs(react.Combobox.Content, { children: [showTotalAndLimit && (jsxRuntime.jsx(react.Text, { p: 2, fontSize: "sm", color: "fg.muted", children: `${enumPickerLabels?.total ?? formI18n.t('total')}: ${collection.items.length}` })), collection.items.length === 0 ? (jsxRuntime.jsx(react.Combobox.Empty, { children: enumPickerLabels?.emptySearchResult ??
|
|
4875
|
-
formI18n.t('empty_search_result') })) : (jsxRuntime.jsx(jsxRuntime.Fragment, { children: collection.items.map((item, index) => (jsxRuntime.jsxs(react.Combobox.Item, { item: item, children: [jsxRuntime.jsx(react.Combobox.ItemText, { children: item.label }), jsxRuntime.jsx(react.Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) }))] }) }) }))] })] }));
|
|
5234
|
+
} })), jsxRuntime.jsx(react.Combobox.Trigger, {})] })] }), insideDialog ? (jsxRuntime.jsx(react.Combobox.Positioner, { children: jsxRuntime.jsxs(react.Combobox.Content, { children: [showTotalAndLimit && (jsxRuntime.jsx(react.Text, { p: 2, fontSize: "sm", color: "fg.muted", children: `${enumPickerLabels?.total ?? 'Total'}: ${collection.items.length}` })), collection.items.length === 0 ? (jsxRuntime.jsx(react.Combobox.Empty, { children: enumPickerLabels?.emptySearchResult ?? 'No results found' })) : (jsxRuntime.jsx(jsxRuntime.Fragment, { children: collection.items.map((item, index) => (jsxRuntime.jsxs(react.Combobox.Item, { item: item, children: [jsxRuntime.jsx(react.Combobox.ItemText, { children: renderEnumValue(item.raw) }), jsxRuntime.jsx(react.Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) }))] }) })) : (jsxRuntime.jsx(react.Portal, { children: jsxRuntime.jsx(react.Combobox.Positioner, { children: jsxRuntime.jsxs(react.Combobox.Content, { children: [showTotalAndLimit && (jsxRuntime.jsx(react.Text, { p: 2, fontSize: "sm", color: "fg.muted", children: `${enumPickerLabels?.total ?? 'Total'}: ${collection.items.length}` })), collection.items.length === 0 ? (jsxRuntime.jsx(react.Combobox.Empty, { children: enumPickerLabels?.emptySearchResult ?? 'No results found' })) : (jsxRuntime.jsx(jsxRuntime.Fragment, { children: collection.items.map((item, index) => (jsxRuntime.jsxs(react.Combobox.Item, { item: item, children: [jsxRuntime.jsx(react.Combobox.ItemText, { children: renderEnumValue(item.raw) }), jsxRuntime.jsx(react.Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) }))] }) }) }))] })] }));
|
|
4876
5235
|
};
|
|
4877
5236
|
|
|
4878
5237
|
function isEnteringWindow(_ref) {
|
|
@@ -5345,7 +5704,7 @@ const MediaLibraryBrowser = ({ onFetchFiles, filterImageOnly = false, labels, en
|
|
|
5345
5704
|
}) })) }))] }));
|
|
5346
5705
|
};
|
|
5347
5706
|
|
|
5348
|
-
function MediaBrowserDialog({ open, onClose, onSelect, title, filterImageOnly = false, onFetchFiles, onUploadFile, enableUpload = false, labels,
|
|
5707
|
+
function MediaBrowserDialog({ open, onClose, onSelect, title, filterImageOnly = false, onFetchFiles, onUploadFile, enableUpload = false, labels, }) {
|
|
5349
5708
|
const [selectedFile, setSelectedFile] = React.useState(undefined);
|
|
5350
5709
|
const [activeTab, setActiveTab] = React.useState('browse');
|
|
5351
5710
|
const [uploadingFiles, setUploadingFiles] = React.useState(new Set());
|
|
@@ -5429,7 +5788,7 @@ function MediaBrowserDialog({ open, onClose, onSelect, title, filterImageOnly =
|
|
|
5429
5788
|
const FilePicker = ({ column, schema, prefix }) => {
|
|
5430
5789
|
const { setValue, formState: { errors }, watch, } = reactHookForm.useFormContext();
|
|
5431
5790
|
const { filePickerLabels } = useSchemaContext();
|
|
5432
|
-
const formI18n = useFormI18n(column, prefix);
|
|
5791
|
+
const formI18n = useFormI18n(column, prefix, schema);
|
|
5433
5792
|
const { required, gridColumn = 'span 12', gridRow = 'span 1', type, } = schema;
|
|
5434
5793
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
5435
5794
|
const isSingleSelect = type === 'string';
|
|
@@ -5487,7 +5846,7 @@ const FilePicker = ({ column, schema, prefix }) => {
|
|
|
5487
5846
|
const newFiles = files.filter(({ name }) => !currentFiles.some((cur) => cur.name === name));
|
|
5488
5847
|
setValue(colLabel, [...currentFiles, ...newFiles]);
|
|
5489
5848
|
}
|
|
5490
|
-
}, placeholder: filePickerLabels?.fileDropzone ??
|
|
5849
|
+
}, placeholder: filePickerLabels?.fileDropzone ?? 'Drop files here' }) }), jsxRuntime.jsx(react.Flex, { flexFlow: 'column', gap: 1, children: currentFiles.map((file, index) => {
|
|
5491
5850
|
const fileIdentifier = getFileIdentifier(file, index);
|
|
5492
5851
|
const fileName = getFileName(file);
|
|
5493
5852
|
const fileSize = getFileSize(file);
|
|
@@ -5505,7 +5864,7 @@ const FilePicker = ({ column, schema, prefix }) => {
|
|
|
5505
5864
|
const FormMediaLibraryBrowser = ({ column, schema, prefix, }) => {
|
|
5506
5865
|
const { setValue, formState: { errors }, watch, } = reactHookForm.useFormContext();
|
|
5507
5866
|
const { filePickerLabels } = useSchemaContext();
|
|
5508
|
-
const formI18n = useFormI18n(column, prefix);
|
|
5867
|
+
const formI18n = useFormI18n(column, prefix, schema);
|
|
5509
5868
|
const { required, gridColumn = 'span 12', gridRow = 'span 1', filePicker, type, } = schema;
|
|
5510
5869
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
5511
5870
|
const isSingleSelect = type === 'string';
|
|
@@ -5598,11 +5957,7 @@ const FormMediaLibraryBrowser = ({ column, schema, prefix, }) => {
|
|
|
5598
5957
|
}
|
|
5599
5958
|
};
|
|
5600
5959
|
return (jsxRuntime.jsxs(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
5601
|
-
gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: [jsxRuntime.jsx(react.VStack, { align: "stretch", gap: 2, children: jsxRuntime.jsx(react.Button, { variant: "outline", onClick: () => setDialogOpen(true), borderColor: "border.default", bg: "bg.panel", _hover: { bg: 'bg.muted' }, children: filePickerLabels?.browseLibrary ??
|
|
5602
|
-
formI18n.t('browse_library') ??
|
|
5603
|
-
'Browse from Library' }) }), jsxRuntime.jsx(MediaBrowserDialog, { open: dialogOpen, onClose: () => setDialogOpen(false), onSelect: handleMediaLibrarySelect, title: filePickerLabels?.dialogTitle ??
|
|
5604
|
-
filePickerLabels?.dialogTitle ??
|
|
5605
|
-
'Select File', filterImageOnly: filterImageOnly, onFetchFiles: onFetchFiles, onUploadFile: onUploadFile, enableUpload: enableUpload, labels: filePickerLabels, colLabel: colLabel }), jsxRuntime.jsx(react.Flex, { flexFlow: 'column', gap: 1, children: currentFileIds.map((fileId, index) => {
|
|
5960
|
+
gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: [jsxRuntime.jsx(react.VStack, { align: "stretch", gap: 2, children: jsxRuntime.jsx(react.Button, { variant: "outline", onClick: () => setDialogOpen(true), borderColor: "border.default", bg: "bg.panel", _hover: { bg: 'bg.muted' }, children: filePickerLabels?.browseLibrary ?? 'Browse from Library' }) }), jsxRuntime.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 }), jsxRuntime.jsx(react.Flex, { flexFlow: 'column', gap: 1, children: currentFileIds.map((fileId, index) => {
|
|
5606
5961
|
const file = fileMap.get(fileId);
|
|
5607
5962
|
const isImage = file
|
|
5608
5963
|
? /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(file.name)
|
|
@@ -5618,106 +5973,51 @@ const FormMediaLibraryBrowser = ({ column, schema, prefix, }) => {
|
|
|
5618
5973
|
}) })] }));
|
|
5619
5974
|
};
|
|
5620
5975
|
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
}
|
|
5625
|
-
if (in_table === undefined || in_table.length == 0) {
|
|
5626
|
-
throw new Error("The in_table is missing");
|
|
5627
|
-
}
|
|
5628
|
-
const options = {
|
|
5629
|
-
method: "GET",
|
|
5630
|
-
url: `${serverUrl}/api/g/${in_table}`,
|
|
5631
|
-
params: {
|
|
5632
|
-
searching,
|
|
5633
|
-
where,
|
|
5634
|
-
limit,
|
|
5635
|
-
offset
|
|
5636
|
-
},
|
|
5637
|
-
};
|
|
5638
|
-
try {
|
|
5639
|
-
const { data } = await axios.request(options);
|
|
5640
|
-
console.log(data);
|
|
5641
|
-
return data;
|
|
5642
|
-
}
|
|
5643
|
-
catch (error) {
|
|
5644
|
-
console.error(error);
|
|
5645
|
-
throw error;
|
|
5646
|
-
}
|
|
5647
|
-
};
|
|
5648
|
-
|
|
5649
|
-
// Default renderDisplay function that stringifies JSON
|
|
5976
|
+
// Default renderDisplay function that intelligently displays items
|
|
5977
|
+
// If item is an object, tries to find common display fields (name, title, label, etc.)
|
|
5978
|
+
// Otherwise falls back to JSON.stringify
|
|
5650
5979
|
const defaultRenderDisplay = (item) => {
|
|
5980
|
+
// Check if item is an object (not null, not array, not primitive)
|
|
5981
|
+
if (item !== null &&
|
|
5982
|
+
typeof item === 'object' &&
|
|
5983
|
+
!Array.isArray(item) &&
|
|
5984
|
+
!(item instanceof Date)) {
|
|
5985
|
+
const obj = item;
|
|
5986
|
+
// Try common display fields in order of preference
|
|
5987
|
+
const displayFields = [
|
|
5988
|
+
'name',
|
|
5989
|
+
'title',
|
|
5990
|
+
'label',
|
|
5991
|
+
'displayName',
|
|
5992
|
+
'display_name',
|
|
5993
|
+
'text',
|
|
5994
|
+
'value',
|
|
5995
|
+
];
|
|
5996
|
+
for (const field of displayFields) {
|
|
5997
|
+
if (obj[field] !== undefined && obj[field] !== null) {
|
|
5998
|
+
const value = obj[field];
|
|
5999
|
+
// Return the value if it's a string or number, otherwise stringify it
|
|
6000
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
6001
|
+
return String(value);
|
|
6002
|
+
}
|
|
6003
|
+
}
|
|
6004
|
+
}
|
|
6005
|
+
// If no display field found, fall back to JSON.stringify
|
|
6006
|
+
return JSON.stringify(item);
|
|
6007
|
+
}
|
|
6008
|
+
// For non-objects (primitives, arrays, dates), use JSON.stringify
|
|
5651
6009
|
return JSON.stringify(item);
|
|
5652
6010
|
};
|
|
5653
6011
|
|
|
5654
|
-
/**
|
|
5655
|
-
* Load initial values for IdPicker fields into idMap
|
|
5656
|
-
* Uses customQueryFn if available, otherwise falls back to getTableData
|
|
5657
|
-
*
|
|
5658
|
-
* @param params - Configuration for loading initial values
|
|
5659
|
-
* @returns Promise with fetched data and idMap
|
|
5660
|
-
*/
|
|
5661
|
-
const loadInitialValues = async ({ ids, foreign_key, serverUrl, setIdMap, }) => {
|
|
5662
|
-
if (!ids || ids.length === 0) {
|
|
5663
|
-
return { data: { data: [], count: 0 }, idMap: {} };
|
|
5664
|
-
}
|
|
5665
|
-
const { table, column: column_ref, customQueryFn } = foreign_key;
|
|
5666
|
-
// Filter out IDs that are already in idMap (optional optimization)
|
|
5667
|
-
// For now, we'll fetch all requested IDs to ensure consistency
|
|
5668
|
-
if (customQueryFn) {
|
|
5669
|
-
const { data, idMap: returnedIdMap } = await customQueryFn({
|
|
5670
|
-
searching: '',
|
|
5671
|
-
limit: ids.length,
|
|
5672
|
-
offset: 0,
|
|
5673
|
-
where: [
|
|
5674
|
-
{
|
|
5675
|
-
id: column_ref,
|
|
5676
|
-
value: ids.length === 1 ? ids[0] : ids, // CustomQueryFn accepts string | string[]
|
|
5677
|
-
},
|
|
5678
|
-
],
|
|
5679
|
-
});
|
|
5680
|
-
// Update idMap with returned values
|
|
5681
|
-
if (returnedIdMap && Object.keys(returnedIdMap).length > 0) {
|
|
5682
|
-
setIdMap((state) => {
|
|
5683
|
-
return { ...state, ...returnedIdMap };
|
|
5684
|
-
});
|
|
5685
|
-
}
|
|
5686
|
-
return { data, idMap: returnedIdMap || {} };
|
|
5687
|
-
}
|
|
5688
|
-
// Fallback to default getTableData
|
|
5689
|
-
const data = await getTableData({
|
|
5690
|
-
serverUrl,
|
|
5691
|
-
searching: '',
|
|
5692
|
-
in_table: table,
|
|
5693
|
-
limit: ids.length,
|
|
5694
|
-
offset: 0,
|
|
5695
|
-
where: [
|
|
5696
|
-
{
|
|
5697
|
-
id: column_ref,
|
|
5698
|
-
value: ids, // Always pass as array
|
|
5699
|
-
},
|
|
5700
|
-
],
|
|
5701
|
-
});
|
|
5702
|
-
// Build idMap from fetched data
|
|
5703
|
-
const newMap = Object.fromEntries((data ?? { data: [] }).data.map((item) => {
|
|
5704
|
-
return [
|
|
5705
|
-
item[column_ref],
|
|
5706
|
-
{
|
|
5707
|
-
...item,
|
|
5708
|
-
},
|
|
5709
|
-
];
|
|
5710
|
-
}));
|
|
5711
|
-
// Update idMap state
|
|
5712
|
-
setIdMap((state) => {
|
|
5713
|
-
return { ...state, ...newMap };
|
|
5714
|
-
});
|
|
5715
|
-
return { data: data, idMap: newMap };
|
|
5716
|
-
};
|
|
5717
6012
|
const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
|
|
5718
6013
|
const { watch, getValues, formState: { errors }, setValue, } = reactHookForm.useFormContext();
|
|
5719
|
-
const {
|
|
5720
|
-
const { renderDisplay, foreign_key } = schema;
|
|
6014
|
+
const { idMap, setIdMap, idPickerLabels, insideDialog } = useSchemaContext();
|
|
6015
|
+
const { renderDisplay, itemToValue: schemaItemToValue, loadInitialValues, foreign_key, variant, } = schema;
|
|
6016
|
+
// loadInitialValues should be provided in schema for id-picker fields
|
|
6017
|
+
// It's used to load the record of the id so the display is human-readable
|
|
6018
|
+
if (variant === 'id-picker' && !loadInitialValues) {
|
|
6019
|
+
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.`);
|
|
6020
|
+
}
|
|
5721
6021
|
const { table, column: column_ref, customQueryFn, } = foreign_key;
|
|
5722
6022
|
const [searchText, setSearchText] = React.useState('');
|
|
5723
6023
|
const [debouncedSearchText, setDebouncedSearchText] = React.useState('');
|
|
@@ -5762,60 +6062,58 @@ const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
|
|
|
5762
6062
|
const missingIdsKey = React.useMemo(() => {
|
|
5763
6063
|
return JSON.stringify([...missingIds].sort());
|
|
5764
6064
|
}, [missingIds]);
|
|
6065
|
+
// Include idMap state in query key to force refetch when idMap is reset (e.g., on remount from another page)
|
|
6066
|
+
// This ensures the query runs even if React Query has cached data for the same missing IDs
|
|
6067
|
+
const idMapStateKey = React.useMemo(() => {
|
|
6068
|
+
// Create a key based on whether the required IDs are in idMap
|
|
6069
|
+
const hasRequiredIds = currentValue.every((id) => idMap[id]);
|
|
6070
|
+
return hasRequiredIds ? 'complete' : 'incomplete';
|
|
6071
|
+
}, [currentValue, idMap]);
|
|
5765
6072
|
// Query to fetch initial values that are missing from idMap
|
|
5766
6073
|
// This query runs automatically when missingIds.length > 0 and updates idMap
|
|
5767
6074
|
const initialValuesQuery = reactQuery.useQuery({
|
|
5768
|
-
queryKey: [`idpicker-initial`, column, missingIdsKey],
|
|
6075
|
+
queryKey: [`idpicker-initial`, column, missingIdsKey, idMapStateKey],
|
|
5769
6076
|
queryFn: async () => {
|
|
5770
6077
|
if (missingIds.length === 0) {
|
|
5771
6078
|
return { data: [], count: 0 };
|
|
5772
6079
|
}
|
|
5773
|
-
// Use
|
|
6080
|
+
// Use schema's loadInitialValues (required for id-picker)
|
|
6081
|
+
if (!loadInitialValues) {
|
|
6082
|
+
console.warn(`loadInitialValues is required in schema for IdPicker field '${column}'. Returning empty idMap.`);
|
|
6083
|
+
return { data: [], count: 0 };
|
|
6084
|
+
}
|
|
5774
6085
|
const result = await loadInitialValues({
|
|
5775
6086
|
ids: missingIds,
|
|
5776
6087
|
foreign_key: foreign_key,
|
|
5777
|
-
serverUrl,
|
|
5778
6088
|
setIdMap,
|
|
5779
6089
|
});
|
|
5780
6090
|
return result.data;
|
|
5781
6091
|
},
|
|
5782
6092
|
enabled: missingIds.length > 0, // Only fetch if there are missing IDs
|
|
5783
|
-
staleTime:
|
|
6093
|
+
staleTime: 0, // Always consider data stale to refetch on remount
|
|
6094
|
+
refetchOnMount: true, // Always refetch when component remounts (e.g., from another page)
|
|
6095
|
+
refetchOnWindowFocus: false, // Don't refetch on window focus
|
|
5784
6096
|
});
|
|
5785
6097
|
const { isLoading: isLoadingInitialValues, isFetching: isFetchingInitialValues, } = initialValuesQuery;
|
|
5786
6098
|
// Query for search results (async loading)
|
|
5787
6099
|
const query = reactQuery.useQuery({
|
|
5788
6100
|
queryKey: [`idpicker`, { column, searchText: debouncedSearchText, limit }],
|
|
5789
6101
|
queryFn: async () => {
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
limit: limit,
|
|
5794
|
-
offset: 0,
|
|
5795
|
-
});
|
|
5796
|
-
setIdMap((state) => {
|
|
5797
|
-
return { ...state, ...idMap };
|
|
5798
|
-
});
|
|
5799
|
-
return data;
|
|
6102
|
+
// customQueryFn is required when serverUrl is not available
|
|
6103
|
+
if (!customQueryFn) {
|
|
6104
|
+
throw new Error(`customQueryFn is required in foreign_key for table ${table}. serverUrl has been removed.`);
|
|
5800
6105
|
}
|
|
5801
|
-
const data = await
|
|
5802
|
-
serverUrl,
|
|
6106
|
+
const { data, idMap } = await customQueryFn({
|
|
5803
6107
|
searching: debouncedSearchText ?? '',
|
|
5804
|
-
in_table: table,
|
|
5805
6108
|
limit: limit,
|
|
5806
6109
|
offset: 0,
|
|
5807
6110
|
});
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
{
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
];
|
|
5815
|
-
}));
|
|
5816
|
-
setIdMap((state) => {
|
|
5817
|
-
return { ...state, ...newMap };
|
|
5818
|
-
});
|
|
6111
|
+
// Update idMap with returned values
|
|
6112
|
+
if (idMap && Object.keys(idMap).length > 0) {
|
|
6113
|
+
setIdMap((state) => {
|
|
6114
|
+
return { ...state, ...idMap };
|
|
6115
|
+
});
|
|
6116
|
+
}
|
|
5819
6117
|
return data;
|
|
5820
6118
|
},
|
|
5821
6119
|
enabled: true, // Always enabled for combobox
|
|
@@ -5847,17 +6145,51 @@ const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
|
|
|
5847
6145
|
// Depend on idMapKey which only changes when items we care about change
|
|
5848
6146
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5849
6147
|
}, [currentValueKey, idMapKey]);
|
|
6148
|
+
// Default itemToValue function: extract value from item using column_ref
|
|
6149
|
+
const defaultItemToValue = (item) => String(item[column_ref]);
|
|
6150
|
+
// Use schema's itemToValue if provided, otherwise use default
|
|
6151
|
+
const itemToValueFn = schemaItemToValue
|
|
6152
|
+
? (item) => schemaItemToValue(item)
|
|
6153
|
+
: defaultItemToValue;
|
|
6154
|
+
// itemToString function: convert item to readable string using renderDisplay
|
|
6155
|
+
// This ensures items can always be displayed as readable strings in the combobox
|
|
6156
|
+
const renderFn = renderDisplay || defaultRenderDisplay;
|
|
6157
|
+
const itemToStringFn = (item) => {
|
|
6158
|
+
const rendered = renderFn(item);
|
|
6159
|
+
// If already a string or number, return it
|
|
6160
|
+
if (typeof rendered === 'string')
|
|
6161
|
+
return rendered;
|
|
6162
|
+
if (typeof rendered === 'number')
|
|
6163
|
+
return String(rendered);
|
|
6164
|
+
// For ReactNode, fall back to defaultRenderDisplay which converts to string
|
|
6165
|
+
return String(defaultRenderDisplay(item));
|
|
6166
|
+
};
|
|
5850
6167
|
// Transform data for combobox collection
|
|
5851
6168
|
// label is used for filtering/searching (must be a string)
|
|
6169
|
+
// displayLabel is used for input display when selected (string representation of rendered display)
|
|
5852
6170
|
// raw item is stored for custom rendering
|
|
5853
6171
|
// Also include items from idMap that match currentValue (for initial values display)
|
|
5854
6172
|
const comboboxItems = React.useMemo(() => {
|
|
5855
6173
|
const renderFn = renderDisplay || defaultRenderDisplay;
|
|
6174
|
+
// Helper to convert rendered display to string for displayLabel
|
|
6175
|
+
// For ReactNodes (non-string/number), we can't safely stringify due to circular refs
|
|
6176
|
+
// So we use the label (which is already a string) as fallback
|
|
6177
|
+
const getDisplayString = (rendered, fallbackLabel) => {
|
|
6178
|
+
if (typeof rendered === 'string')
|
|
6179
|
+
return rendered;
|
|
6180
|
+
if (typeof rendered === 'number')
|
|
6181
|
+
return String(rendered);
|
|
6182
|
+
// For ReactNode, use the fallback label (which is already a string representation)
|
|
6183
|
+
// The actual ReactNode will be rendered in the overlay, not in the input
|
|
6184
|
+
return fallbackLabel;
|
|
6185
|
+
};
|
|
5856
6186
|
const itemsFromDataList = dataList.map((item) => {
|
|
5857
6187
|
const rendered = renderFn(item);
|
|
6188
|
+
const label = typeof rendered === 'string' ? rendered : JSON.stringify(item); // Use string for filtering
|
|
5858
6189
|
return {
|
|
5859
|
-
label
|
|
5860
|
-
|
|
6190
|
+
label, // Use string for filtering
|
|
6191
|
+
displayLabel: getDisplayString(rendered, label), // String representation for input display
|
|
6192
|
+
value: itemToValueFn(item),
|
|
5861
6193
|
raw: item,
|
|
5862
6194
|
};
|
|
5863
6195
|
});
|
|
@@ -5866,25 +6198,28 @@ const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
|
|
|
5866
6198
|
const itemsFromIdMap = idMapItems
|
|
5867
6199
|
.map((item) => {
|
|
5868
6200
|
// Check if this item is already in itemsFromDataList
|
|
5869
|
-
const alreadyIncluded = itemsFromDataList.some((i) => i.value ===
|
|
6201
|
+
const alreadyIncluded = itemsFromDataList.some((i) => i.value === itemToValueFn(item));
|
|
5870
6202
|
if (alreadyIncluded)
|
|
5871
6203
|
return null;
|
|
5872
6204
|
const rendered = renderFn(item);
|
|
6205
|
+
const label = typeof rendered === 'string' ? rendered : JSON.stringify(item);
|
|
5873
6206
|
return {
|
|
5874
|
-
label
|
|
5875
|
-
|
|
6207
|
+
label,
|
|
6208
|
+
displayLabel: getDisplayString(rendered, label), // String representation for input display
|
|
6209
|
+
value: itemToValueFn(item),
|
|
5876
6210
|
raw: item,
|
|
5877
6211
|
};
|
|
5878
6212
|
})
|
|
5879
6213
|
.filter((item) => item !== null);
|
|
5880
6214
|
return [...itemsFromIdMap, ...itemsFromDataList];
|
|
5881
|
-
}, [dataList, column_ref, renderDisplay, idMapItems]);
|
|
6215
|
+
}, [dataList, column_ref, renderDisplay, idMapItems, itemToValueFn]);
|
|
5882
6216
|
// Use filter hook for combobox
|
|
5883
6217
|
const { contains } = react.useFilter({ sensitivity: 'base' });
|
|
5884
6218
|
// Create collection for combobox
|
|
6219
|
+
// itemToString uses displayLabel to show rendered display in input when selected
|
|
5885
6220
|
const { collection, filter, set } = react.useListCollection({
|
|
5886
6221
|
initialItems: comboboxItems,
|
|
5887
|
-
itemToString: (item) => item.
|
|
6222
|
+
itemToString: (item) => item.displayLabel, // Use displayLabel for selected value display
|
|
5888
6223
|
itemToValue: (item) => item.value,
|
|
5889
6224
|
filter: contains,
|
|
5890
6225
|
});
|
|
@@ -5939,6 +6274,10 @@ const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
|
|
|
5939
6274
|
idPickerLabels,
|
|
5940
6275
|
insideDialog: insideDialog ?? false,
|
|
5941
6276
|
renderDisplay,
|
|
6277
|
+
itemToValue: itemToValueFn,
|
|
6278
|
+
itemToString: itemToStringFn,
|
|
6279
|
+
loadInitialValues: loadInitialValues ??
|
|
6280
|
+
(async () => ({ data: { data: [], count: 0 }, idMap: {} })), // Fallback if not provided
|
|
5942
6281
|
column_ref,
|
|
5943
6282
|
errors,
|
|
5944
6283
|
setValue,
|
|
@@ -5947,60 +6286,69 @@ const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
|
|
|
5947
6286
|
|
|
5948
6287
|
const IdPickerSingle = ({ column, schema, prefix, }) => {
|
|
5949
6288
|
const formI18n = useFormI18n(column, prefix, schema);
|
|
5950
|
-
const { required, gridColumn = 'span 12', gridRow = 'span 1'
|
|
6289
|
+
const { required, gridColumn = 'span 12', gridRow = 'span 1' } = schema;
|
|
5951
6290
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
5952
|
-
const { colLabel, currentValue, searchText, setSearchText, isLoading, isFetching, isPending, isError, isSearching,
|
|
6291
|
+
const { colLabel, currentValue, searchText, setSearchText, isLoading, isFetching, isPending, isError, isSearching, collection, filter, idMap, idPickerLabels, insideDialog, renderDisplay: renderDisplayFn, itemToValue, itemToString, errors, setValue, } = useIdPickerData({
|
|
5953
6292
|
column,
|
|
5954
6293
|
schema,
|
|
5955
6294
|
prefix,
|
|
5956
6295
|
isMultiple: false,
|
|
5957
6296
|
});
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
const
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
6297
|
+
// Get the selected value for single selection display
|
|
6298
|
+
const selectedId = currentValue.length > 0 ? currentValue[0] : null;
|
|
6299
|
+
const selectedItem = selectedId
|
|
6300
|
+
? idMap[selectedId]
|
|
6301
|
+
: undefined;
|
|
6302
|
+
// Use itemToValue to get the combobox value from the selected item, or use the ID directly
|
|
6303
|
+
const comboboxValue = selectedItem
|
|
6304
|
+
? itemToString(selectedItem)
|
|
6305
|
+
: selectedId || '';
|
|
6306
|
+
// itemToString is available from the hook and can be used to get a readable string
|
|
6307
|
+
// representation of any item. The collection's itemToString is automatically used
|
|
6308
|
+
// by the combobox to display selected values.
|
|
6309
|
+
// Use useCombobox hook to control input value
|
|
6310
|
+
const combobox = react.useCombobox({
|
|
6311
|
+
collection,
|
|
6312
|
+
value: [comboboxValue],
|
|
6313
|
+
onInputValueChange(e) {
|
|
6314
|
+
setSearchText(e.inputValue);
|
|
6315
|
+
filter(e.inputValue);
|
|
6316
|
+
},
|
|
6317
|
+
onValueChange(e) {
|
|
6318
|
+
setValue(colLabel, e.value[0] || '');
|
|
6319
|
+
// Clear the input value after selection
|
|
6320
|
+
setSearchText('');
|
|
6321
|
+
},
|
|
6322
|
+
multiple: false,
|
|
6323
|
+
closeOnSelect: true,
|
|
6324
|
+
openOnClick: true,
|
|
6325
|
+
invalid: !!errors[colLabel],
|
|
6326
|
+
});
|
|
6327
|
+
// Use renderDisplay from hook (which comes from schema) or fallback to default
|
|
6328
|
+
const renderDisplayFunction = renderDisplayFn || defaultRenderDisplay;
|
|
6329
|
+
// Get the selected value for single selection display (already computed above)
|
|
6330
|
+
const selectedRendered = selectedItem
|
|
6331
|
+
? renderDisplayFunction(selectedItem)
|
|
6332
|
+
: null;
|
|
6333
|
+
return (jsxRuntime.jsx(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
6334
|
+
gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: jsxRuntime.jsxs(react.Combobox.RootProvider, { value: combobox, width: "100%", children: [jsxRuntime.jsx(react.Show, { when: selectedId && selectedRendered, children: jsxRuntime.jsxs(react.HStack, { justifyContent: 'space-between', children: [jsxRuntime.jsx(react.Box, { children: selectedRendered }), currentValue.length > 0 && (jsxRuntime.jsx(react.Button, { variant: "ghost", size: "sm", onClick: () => {
|
|
6335
|
+
setValue(colLabel, '');
|
|
6336
|
+
}, children: jsxRuntime.jsx(react.Icon, { children: jsxRuntime.jsx(bi.BiX, {}) }) }))] }) }), jsxRuntime.jsx(react.Show, { when: !selectedId || !selectedRendered, children: jsxRuntime.jsxs(react.Combobox.Control, { position: "relative", children: [jsxRuntime.jsx(react.Combobox.Input, { placeholder: idPickerLabels?.typeToSearch ?? 'Type to search' }), jsxRuntime.jsxs(react.Combobox.IndicatorGroup, { children: [(isFetching || isLoading || isPending) && jsxRuntime.jsx(react.Spinner, { size: "xs" }), isError && (jsxRuntime.jsx(react.Icon, { color: "fg.error", children: jsxRuntime.jsx(bi.BiError, {}) })), jsxRuntime.jsx(react.Combobox.Trigger, {})] })] }) }), insideDialog ? (jsxRuntime.jsx(react.Combobox.Positioner, { children: jsxRuntime.jsx(react.Combobox.Content, { children: isError ? (jsxRuntime.jsx(react.Text, { p: 2, color: "fg.error", fontSize: "sm", children: idPickerLabels?.emptySearchResult ?? 'Loading failed' })) : isFetching || isLoading || isPending || isSearching ? (
|
|
6337
|
+
// Show skeleton items to prevent UI shift
|
|
6338
|
+
jsxRuntime.jsx(jsxRuntime.Fragment, { children: Array.from({ length: 5 }).map((_, index) => (jsxRuntime.jsx(react.Flex, { p: 2, align: "center", gap: 2, children: jsxRuntime.jsx(react.Skeleton, { height: "20px", flex: "1" }) }, `skeleton-${index}`))) })) : collection.items.length === 0 ? (jsxRuntime.jsx(react.Combobox.Empty, { children: searchText
|
|
6339
|
+
? idPickerLabels?.emptySearchResult ?? 'No results found'
|
|
6340
|
+
: idPickerLabels?.initialResults ??
|
|
6341
|
+
'Start typing to search' })) : (jsxRuntime.jsx(jsxRuntime.Fragment, { children: collection.items.map((item, index) => (jsxRuntime.jsxs(react.Combobox.Item, { item: item, children: [renderDisplayFunction(item.raw), jsxRuntime.jsx(react.Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) })) }) })) : (jsxRuntime.jsx(react.Portal, { children: jsxRuntime.jsx(react.Combobox.Positioner, { children: jsxRuntime.jsx(react.Combobox.Content, { children: isError ? (jsxRuntime.jsx(react.Text, { p: 2, color: "fg.error", fontSize: "sm", children: idPickerLabels?.emptySearchResult ?? 'Loading failed' })) : isFetching || isLoading || isPending || isSearching ? (
|
|
5985
6342
|
// Show skeleton items to prevent UI shift
|
|
5986
6343
|
jsxRuntime.jsx(jsxRuntime.Fragment, { children: Array.from({ length: 5 }).map((_, index) => (jsxRuntime.jsx(react.Flex, { p: 2, align: "center", gap: 2, children: jsxRuntime.jsx(react.Skeleton, { height: "20px", flex: "1" }) }, `skeleton-${index}`))) })) : collection.items.length === 0 ? (jsxRuntime.jsx(react.Combobox.Empty, { children: searchText
|
|
5987
6344
|
? idPickerLabels?.emptySearchResult ?? 'No results found'
|
|
5988
6345
|
: idPickerLabels?.initialResults ??
|
|
5989
|
-
'Start typing to search' })) : (jsxRuntime.jsx(jsxRuntime.Fragment, { children: collection.items.map((item, index) => (jsxRuntime.
|
|
5990
|
-
? renderDisplayFunction(item.raw)
|
|
5991
|
-
: item.label }), jsxRuntime.jsx(react.Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) })) }) })) : (jsxRuntime.jsx(react.Portal, { children: jsxRuntime.jsx(react.Combobox.Positioner, { children: jsxRuntime.jsx(react.Combobox.Content, { children: isError ? (jsxRuntime.jsx(react.Text, { p: 2, color: "fg.error", fontSize: "sm", children: idPickerLabels?.emptySearchResult ?? 'Loading failed' })) : isFetching || isLoading || isPending || isSearching ? (
|
|
5992
|
-
// Show skeleton items to prevent UI shift
|
|
5993
|
-
jsxRuntime.jsx(jsxRuntime.Fragment, { children: Array.from({ length: 5 }).map((_, index) => (jsxRuntime.jsx(react.Flex, { p: 2, align: "center", gap: 2, children: jsxRuntime.jsx(react.Skeleton, { height: "20px", flex: "1" }) }, `skeleton-${index}`))) })) : collection.items.length === 0 ? (jsxRuntime.jsx(react.Combobox.Empty, { children: searchText
|
|
5994
|
-
? idPickerLabels?.emptySearchResult ?? 'No results found'
|
|
5995
|
-
: idPickerLabels?.initialResults ??
|
|
5996
|
-
'Start typing to search' })) : (jsxRuntime.jsx(jsxRuntime.Fragment, { children: collection.items.map((item, index) => (jsxRuntime.jsxs(react.Combobox.Item, { item: item, children: [jsxRuntime.jsx(react.Combobox.ItemText, { children: !!renderDisplayFunction === true
|
|
5997
|
-
? renderDisplayFunction(item.raw)
|
|
5998
|
-
: item.label }), jsxRuntime.jsx(react.Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) })) }) }) }))] })] }));
|
|
6346
|
+
'Start typing to search' })) : (jsxRuntime.jsx(jsxRuntime.Fragment, { children: collection.items.map((item, index) => (jsxRuntime.jsx(react.Combobox.Item, { item: item, children: renderDisplayFunction(item.raw) }, item.value ?? `item-${index}`))) })) }) }) }))] }) }));
|
|
5999
6347
|
};
|
|
6000
6348
|
|
|
6001
6349
|
const IdPickerMultiple = ({ column, schema, prefix, }) => {
|
|
6002
6350
|
const formI18n = useFormI18n(column, prefix, schema);
|
|
6003
|
-
const { required, gridColumn = 'span 12', gridRow = 'span 1'
|
|
6351
|
+
const { required, gridColumn = 'span 12', gridRow = 'span 1' } = schema;
|
|
6004
6352
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
6005
6353
|
const { colLabel, currentValue, searchText, setSearchText, isLoading, isFetching, isPending, isError, isSearching, isLoadingInitialValues, isFetchingInitialValues, missingIds, collection, idMap, idPickerLabels, insideDialog, renderDisplay: renderDisplayFn, errors, setValue, } = useIdPickerData({
|
|
6006
6354
|
column,
|
|
@@ -6014,7 +6362,8 @@ const IdPickerMultiple = ({ column, schema, prefix, }) => {
|
|
|
6014
6362
|
const handleValueChange = (details) => {
|
|
6015
6363
|
setValue(colLabel, details.value);
|
|
6016
6364
|
};
|
|
6017
|
-
|
|
6365
|
+
// Use renderDisplay from hook (which comes from schema) or fallback to default
|
|
6366
|
+
const renderDisplayFunction = renderDisplayFn || defaultRenderDisplay;
|
|
6018
6367
|
return (jsxRuntime.jsxs(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
6019
6368
|
gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: [currentValue.length > 0 && (jsxRuntime.jsx(react.Flex, { flexFlow: 'wrap', gap: 1, mb: 2, children: currentValue.map((id) => {
|
|
6020
6369
|
const item = idMap[id];
|
|
@@ -6039,16 +6388,12 @@ const IdPickerMultiple = ({ column, schema, prefix, }) => {
|
|
|
6039
6388
|
jsxRuntime.jsx(jsxRuntime.Fragment, { children: Array.from({ length: 5 }).map((_, index) => (jsxRuntime.jsx(react.Flex, { p: 2, align: "center", gap: 2, children: jsxRuntime.jsx(react.Skeleton, { height: "20px", flex: "1" }) }, `skeleton-${index}`))) })) : collection.items.length === 0 ? (jsxRuntime.jsx(react.Combobox.Empty, { children: searchText
|
|
6040
6389
|
? idPickerLabels?.emptySearchResult ?? 'No results found'
|
|
6041
6390
|
: idPickerLabels?.initialResults ??
|
|
6042
|
-
'Start typing to search' })) : (jsxRuntime.jsx(jsxRuntime.Fragment, { children: collection.items.map((item, index) => (jsxRuntime.jsxs(react.Combobox.Item, { item: item, children: [jsxRuntime.jsx(react.Combobox.
|
|
6043
|
-
? renderDisplayFunction(item.raw)
|
|
6044
|
-
: item.label }), jsxRuntime.jsx(react.Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) })) }) })) : (jsxRuntime.jsx(react.Portal, { children: jsxRuntime.jsx(react.Combobox.Positioner, { children: jsxRuntime.jsx(react.Combobox.Content, { children: isError ? (jsxRuntime.jsx(react.Text, { p: 2, color: "fg.error", fontSize: "sm", children: idPickerLabels?.emptySearchResult ?? 'Loading failed' })) : isFetching || isLoading || isPending || isSearching ? (
|
|
6391
|
+
'Start typing to search' })) : (jsxRuntime.jsx(jsxRuntime.Fragment, { children: collection.items.map((item, index) => (jsxRuntime.jsxs(react.Combobox.Item, { item: item, children: [renderDisplayFunction(item.raw), jsxRuntime.jsx(react.Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) })) }) })) : (jsxRuntime.jsx(react.Portal, { children: jsxRuntime.jsx(react.Combobox.Positioner, { children: jsxRuntime.jsx(react.Combobox.Content, { children: isError ? (jsxRuntime.jsx(react.Text, { p: 2, color: "fg.error", fontSize: "sm", children: idPickerLabels?.emptySearchResult ?? 'Loading failed' })) : isFetching || isLoading || isPending || isSearching ? (
|
|
6045
6392
|
// Show skeleton items to prevent UI shift
|
|
6046
6393
|
jsxRuntime.jsx(jsxRuntime.Fragment, { children: Array.from({ length: 5 }).map((_, index) => (jsxRuntime.jsx(react.Flex, { p: 2, align: "center", gap: 2, children: jsxRuntime.jsx(react.Skeleton, { height: "20px", flex: "1" }) }, `skeleton-${index}`))) })) : collection.items.length === 0 ? (jsxRuntime.jsx(react.Combobox.Empty, { children: searchText
|
|
6047
6394
|
? idPickerLabels?.emptySearchResult ?? 'No results found'
|
|
6048
6395
|
: idPickerLabels?.initialResults ??
|
|
6049
|
-
'Start typing to search' })) : (jsxRuntime.jsx(jsxRuntime.Fragment, { children: collection.items.map((item, index) => (jsxRuntime.jsxs(react.Combobox.Item, { item: item, children: [jsxRuntime.jsx(react.Combobox.
|
|
6050
|
-
? renderDisplayFunction(item.raw)
|
|
6051
|
-
: item.label }), jsxRuntime.jsx(react.Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) })) }) }) }))] })] }));
|
|
6396
|
+
'Start typing to search' })) : (jsxRuntime.jsx(jsxRuntime.Fragment, { children: collection.items.map((item, index) => (jsxRuntime.jsxs(react.Combobox.Item, { item: item, children: [renderDisplayFunction(item.raw), jsxRuntime.jsx(react.Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) })) }) }) }))] })] }));
|
|
6052
6397
|
};
|
|
6053
6398
|
|
|
6054
6399
|
const NumberInputRoot = React__namespace.forwardRef(function NumberInput(props, ref) {
|
|
@@ -6232,31 +6577,35 @@ react.RadioCard.ItemIndicator;
|
|
|
6232
6577
|
|
|
6233
6578
|
const TagPicker = ({ column, schema, prefix }) => {
|
|
6234
6579
|
const { watch, formState: { errors }, setValue, } = reactHookForm.useFormContext();
|
|
6235
|
-
const { serverUrl } = useSchemaContext();
|
|
6236
6580
|
if (schema.properties == undefined) {
|
|
6237
|
-
throw new Error(
|
|
6581
|
+
throw new Error('schema properties undefined when using DatePicker');
|
|
6238
6582
|
}
|
|
6239
|
-
const { gridColumn, gridRow, in_table, object_id_column } = schema;
|
|
6583
|
+
const { gridColumn, gridRow, in_table, object_id_column, tagPicker } = schema;
|
|
6240
6584
|
if (in_table === undefined) {
|
|
6241
|
-
throw new Error(
|
|
6585
|
+
throw new Error('in_table is undefined when using TagPicker');
|
|
6242
6586
|
}
|
|
6243
6587
|
if (object_id_column === undefined) {
|
|
6244
|
-
throw new Error(
|
|
6588
|
+
throw new Error('object_id_column is undefined when using TagPicker');
|
|
6589
|
+
}
|
|
6590
|
+
if (!tagPicker?.queryFn) {
|
|
6591
|
+
throw new Error('tagPicker.queryFn is required in schema. serverUrl has been removed.');
|
|
6245
6592
|
}
|
|
6246
6593
|
const query = reactQuery.useQuery({
|
|
6247
6594
|
queryKey: [`tagpicker`, in_table],
|
|
6248
6595
|
queryFn: async () => {
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
in_table: "tables_tags_view",
|
|
6596
|
+
const result = await tagPicker.queryFn({
|
|
6597
|
+
in_table: 'tables_tags_view',
|
|
6252
6598
|
where: [
|
|
6253
6599
|
{
|
|
6254
|
-
id:
|
|
6600
|
+
id: 'table_name',
|
|
6255
6601
|
value: [in_table],
|
|
6256
6602
|
},
|
|
6257
6603
|
],
|
|
6258
6604
|
limit: 100,
|
|
6605
|
+
offset: 0,
|
|
6606
|
+
searching: '',
|
|
6259
6607
|
});
|
|
6608
|
+
return result.data || { data: [] };
|
|
6260
6609
|
},
|
|
6261
6610
|
staleTime: 10000,
|
|
6262
6611
|
});
|
|
@@ -6264,17 +6613,19 @@ const TagPicker = ({ column, schema, prefix }) => {
|
|
|
6264
6613
|
const existingTagsQuery = reactQuery.useQuery({
|
|
6265
6614
|
queryKey: [`existing`, { in_table, object_id_column }, object_id],
|
|
6266
6615
|
queryFn: async () => {
|
|
6267
|
-
|
|
6268
|
-
serverUrl,
|
|
6616
|
+
const result = await tagPicker.queryFn({
|
|
6269
6617
|
in_table: in_table,
|
|
6270
6618
|
where: [
|
|
6271
6619
|
{
|
|
6272
6620
|
id: object_id_column,
|
|
6273
|
-
value: object_id[0],
|
|
6621
|
+
value: [object_id[0]],
|
|
6274
6622
|
},
|
|
6275
6623
|
],
|
|
6276
6624
|
limit: 100,
|
|
6625
|
+
offset: 0,
|
|
6626
|
+
searching: '',
|
|
6277
6627
|
});
|
|
6628
|
+
return result.data || { data: [] };
|
|
6278
6629
|
},
|
|
6279
6630
|
enabled: object_id != undefined,
|
|
6280
6631
|
staleTime: 10000,
|
|
@@ -6285,9 +6636,9 @@ const TagPicker = ({ column, schema, prefix }) => {
|
|
|
6285
6636
|
if (!!object_id === false) {
|
|
6286
6637
|
return jsxRuntime.jsx(jsxRuntime.Fragment, {});
|
|
6287
6638
|
}
|
|
6288
|
-
return (jsxRuntime.jsxs(react.Flex, { flexFlow:
|
|
6639
|
+
return (jsxRuntime.jsxs(react.Flex, { flexFlow: 'column', gap: 4, gridColumn,
|
|
6289
6640
|
gridRow, children: [isFetching && jsxRuntime.jsx(jsxRuntime.Fragment, { children: "isFetching" }), isLoading && jsxRuntime.jsx(jsxRuntime.Fragment, { children: "isLoading" }), isPending && jsxRuntime.jsx(jsxRuntime.Fragment, { children: "isPending" }), isError && jsxRuntime.jsx(jsxRuntime.Fragment, { children: "isError" }), dataList.map(({ parent_tag_name, all_tags, is_mutually_exclusive }) => {
|
|
6290
|
-
return (jsxRuntime.jsxs(react.Flex, { flexFlow:
|
|
6641
|
+
return (jsxRuntime.jsxs(react.Flex, { flexFlow: 'column', gap: 2, children: [jsxRuntime.jsx(react.Text, { children: parent_tag_name }), is_mutually_exclusive && (jsxRuntime.jsx(RadioCardRoot, { defaultValue: "next", variant: 'surface', onValueChange: (tagIds) => {
|
|
6291
6642
|
const existedTags = Object.values(all_tags)
|
|
6292
6643
|
.filter(({ id }) => {
|
|
6293
6644
|
return existingTagList.some(({ tag_id }) => tag_id === id);
|
|
@@ -6299,20 +6650,20 @@ const TagPicker = ({ column, schema, prefix }) => {
|
|
|
6299
6650
|
tagIds.value,
|
|
6300
6651
|
]);
|
|
6301
6652
|
setValue(`${column}.${parent_tag_name}.old`, existedTags);
|
|
6302
|
-
}, children: jsxRuntime.jsx(react.Flex, { flexFlow:
|
|
6653
|
+
}, children: jsxRuntime.jsx(react.Flex, { flexFlow: 'wrap', gap: 2, children: Object.entries(all_tags).map(([tagName, { id }]) => {
|
|
6303
6654
|
if (existingTagList.some(({ tag_id }) => tag_id === id)) {
|
|
6304
|
-
return (jsxRuntime.jsx(RadioCardItem, { label: tagName, value: id, flex:
|
|
6655
|
+
return (jsxRuntime.jsx(RadioCardItem, { label: tagName, value: id, flex: '0 0 0%', disabled: true }, `${tagName}-${id}`));
|
|
6305
6656
|
}
|
|
6306
|
-
return (jsxRuntime.jsx(RadioCardItem, { label: tagName, value: id, flex:
|
|
6657
|
+
return (jsxRuntime.jsx(RadioCardItem, { label: tagName, value: id, flex: '0 0 0%', colorPalette: 'blue' }, `${tagName}-${id}`));
|
|
6307
6658
|
}) }) })), !is_mutually_exclusive && (jsxRuntime.jsx(react.CheckboxGroup, { onValueChange: (tagIds) => {
|
|
6308
6659
|
setValue(`${column}.${parent_tag_name}.current`, tagIds);
|
|
6309
|
-
}, children: jsxRuntime.jsx(react.Flex, { flexFlow:
|
|
6660
|
+
}, children: jsxRuntime.jsx(react.Flex, { flexFlow: 'wrap', gap: 2, children: Object.entries(all_tags).map(([tagName, { id }]) => {
|
|
6310
6661
|
if (existingTagList.some(({ tag_id }) => tag_id === id)) {
|
|
6311
|
-
return (jsxRuntime.jsx(CheckboxCard, { label: tagName, value: id, flex:
|
|
6662
|
+
return (jsxRuntime.jsx(CheckboxCard, { label: tagName, value: id, flex: '0 0 0%', disabled: true, colorPalette: 'blue' }, `${tagName}-${id}`));
|
|
6312
6663
|
}
|
|
6313
|
-
return (jsxRuntime.jsx(CheckboxCard, { label: tagName, value: id, flex:
|
|
6664
|
+
return (jsxRuntime.jsx(CheckboxCard, { label: tagName, value: id, flex: '0 0 0%' }, `${tagName}-${id}`));
|
|
6314
6665
|
}) }) }))] }, `tag-${parent_tag_name}`));
|
|
6315
|
-
}), errors[`${column}`] && (jsxRuntime.jsx(react.Text, { color:
|
|
6666
|
+
}), errors[`${column}`] && (jsxRuntime.jsx(react.Text, { color: 'red.400', children: (errors[`${column}`]?.message ?? 'No error message') }))] }));
|
|
6316
6667
|
};
|
|
6317
6668
|
|
|
6318
6669
|
const Textarea = React.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) => {
|
|
@@ -6419,11 +6770,193 @@ const TextAreaInput = ({ column, schema, prefix, }) => {
|
|
|
6419
6770
|
|
|
6420
6771
|
dayjs.extend(utc);
|
|
6421
6772
|
dayjs.extend(timezone);
|
|
6422
|
-
const TimePicker$1 = (
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6773
|
+
const TimePicker$1 = (props) => {
|
|
6774
|
+
const { format = '12h', value: controlledValue, onChange: controlledOnChange, hour: uncontrolledHour, setHour: uncontrolledSetHour, minute: uncontrolledMinute, setMinute: uncontrolledSetMinute, startTime, selectedDate, timezone = 'Asia/Hong_Kong', portalled = true, labels = {
|
|
6775
|
+
placeholder: format === '24h' ? 'HH:mm:ss' : 'hh:mm AM/PM',
|
|
6776
|
+
emptyMessage: 'No time found',
|
|
6777
|
+
}, onTimeChange, } = props;
|
|
6778
|
+
const is24Hour = format === '24h';
|
|
6779
|
+
const uncontrolledMeridiem = is24Hour ? undefined : props.meridiem;
|
|
6780
|
+
const uncontrolledSetMeridiem = is24Hour ? undefined : props.setMeridiem;
|
|
6781
|
+
const uncontrolledSecond = is24Hour ? props.second : undefined;
|
|
6782
|
+
const uncontrolledSetSecond = is24Hour ? props.setSecond : undefined;
|
|
6783
|
+
// Determine if we're in controlled mode
|
|
6784
|
+
const isControlled = controlledValue !== undefined;
|
|
6785
|
+
// Parse time string to extract hour, minute, second, meridiem
|
|
6786
|
+
const parseTimeString = (timeStr) => {
|
|
6787
|
+
if (!timeStr || !timeStr.trim()) {
|
|
6788
|
+
return { hour: null, minute: null, second: null, meridiem: null };
|
|
6789
|
+
}
|
|
6790
|
+
// Remove timezone suffix if present (e.g., "14:30:00Z" -> "14:30:00")
|
|
6791
|
+
const timeWithoutTz = timeStr.replace(/[Z+-]\d{2}:?\d{2}$/, '').trim();
|
|
6792
|
+
// Try parsing 24-hour format: "HH:mm:ss" or "HH:mm"
|
|
6793
|
+
const time24Pattern = /^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?$/;
|
|
6794
|
+
const match24 = timeWithoutTz.match(time24Pattern);
|
|
6795
|
+
if (match24) {
|
|
6796
|
+
const hour24 = parseInt(match24[1], 10);
|
|
6797
|
+
const minute = parseInt(match24[2], 10);
|
|
6798
|
+
const second = match24[3] ? parseInt(match24[3], 10) : 0;
|
|
6799
|
+
if (hour24 >= 0 &&
|
|
6800
|
+
hour24 <= 23 &&
|
|
6801
|
+
minute >= 0 &&
|
|
6802
|
+
minute <= 59 &&
|
|
6803
|
+
second >= 0 &&
|
|
6804
|
+
second <= 59) {
|
|
6805
|
+
if (is24Hour) {
|
|
6806
|
+
return { hour: hour24, minute, second, meridiem: null };
|
|
6807
|
+
}
|
|
6808
|
+
else {
|
|
6809
|
+
// Convert to 12-hour format
|
|
6810
|
+
let hour12 = hour24;
|
|
6811
|
+
let meridiem;
|
|
6812
|
+
if (hour24 === 0) {
|
|
6813
|
+
hour12 = 12;
|
|
6814
|
+
meridiem = 'am';
|
|
6815
|
+
}
|
|
6816
|
+
else if (hour24 === 12) {
|
|
6817
|
+
hour12 = 12;
|
|
6818
|
+
meridiem = 'pm';
|
|
6819
|
+
}
|
|
6820
|
+
else if (hour24 > 12) {
|
|
6821
|
+
hour12 = hour24 - 12;
|
|
6822
|
+
meridiem = 'pm';
|
|
6823
|
+
}
|
|
6824
|
+
else {
|
|
6825
|
+
hour12 = hour24;
|
|
6826
|
+
meridiem = 'am';
|
|
6827
|
+
}
|
|
6828
|
+
return { hour: hour12, minute, second: null, meridiem };
|
|
6829
|
+
}
|
|
6830
|
+
}
|
|
6831
|
+
}
|
|
6832
|
+
// Try parsing 12-hour format: "hh:mm AM/PM" or "hh:mm:ss AM/PM"
|
|
6833
|
+
const time12Pattern = /^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?\s*(am|pm|AM|PM)$/i;
|
|
6834
|
+
const match12 = timeWithoutTz.match(time12Pattern);
|
|
6835
|
+
if (match12 && !is24Hour) {
|
|
6836
|
+
const hour12 = parseInt(match12[1], 10);
|
|
6837
|
+
const minute = parseInt(match12[2], 10);
|
|
6838
|
+
const second = match12[3] ? parseInt(match12[3], 10) : null;
|
|
6839
|
+
const meridiem = match12[4].toLowerCase();
|
|
6840
|
+
if (hour12 >= 1 &&
|
|
6841
|
+
hour12 <= 12 &&
|
|
6842
|
+
minute >= 0 &&
|
|
6843
|
+
minute <= 59 &&
|
|
6844
|
+
(second === null || (second >= 0 && second <= 59))) {
|
|
6845
|
+
return { hour: hour12, minute, second, meridiem };
|
|
6846
|
+
}
|
|
6847
|
+
}
|
|
6848
|
+
return { hour: null, minute: null, second: null, meridiem: null };
|
|
6849
|
+
};
|
|
6850
|
+
// Format time values to time string
|
|
6851
|
+
const formatTimeString = (hour, minute, second, meridiem) => {
|
|
6852
|
+
if (hour === null || minute === null) {
|
|
6853
|
+
return undefined;
|
|
6854
|
+
}
|
|
6855
|
+
if (is24Hour) {
|
|
6856
|
+
const h = hour.toString().padStart(2, '0');
|
|
6857
|
+
const m = minute.toString().padStart(2, '0');
|
|
6858
|
+
const s = (second ?? 0).toString().padStart(2, '0');
|
|
6859
|
+
return `${h}:${m}:${s}`;
|
|
6860
|
+
}
|
|
6861
|
+
else {
|
|
6862
|
+
if (meridiem === null) {
|
|
6863
|
+
return undefined;
|
|
6864
|
+
}
|
|
6865
|
+
const h = hour.toString();
|
|
6866
|
+
const m = minute.toString().padStart(2, '0');
|
|
6867
|
+
return `${h}:${m} ${meridiem.toUpperCase()}`;
|
|
6868
|
+
}
|
|
6869
|
+
};
|
|
6870
|
+
// Internal state for controlled mode
|
|
6871
|
+
const [internalHour, setInternalHour] = React.useState(null);
|
|
6872
|
+
const [internalMinute, setInternalMinute] = React.useState(null);
|
|
6873
|
+
const [internalSecond, setInternalSecond] = React.useState(null);
|
|
6874
|
+
const [internalMeridiem, setInternalMeridiem] = React.useState(null);
|
|
6875
|
+
// Use controlled or uncontrolled values
|
|
6876
|
+
const hour = isControlled ? internalHour : uncontrolledHour ?? null;
|
|
6877
|
+
const minute = isControlled ? internalMinute : uncontrolledMinute ?? null;
|
|
6878
|
+
const second = isControlled ? internalSecond : uncontrolledSecond ?? null;
|
|
6879
|
+
const meridiem = isControlled
|
|
6880
|
+
? internalMeridiem
|
|
6881
|
+
: uncontrolledMeridiem ?? null;
|
|
6882
|
+
// Setters that work for both modes
|
|
6883
|
+
const setHour = isControlled
|
|
6884
|
+
? setInternalHour
|
|
6885
|
+
: uncontrolledSetHour || (() => { });
|
|
6886
|
+
const setMinute = isControlled
|
|
6887
|
+
? setInternalMinute
|
|
6888
|
+
: uncontrolledSetMinute || (() => { });
|
|
6889
|
+
const setSecond = isControlled
|
|
6890
|
+
? setInternalSecond
|
|
6891
|
+
: uncontrolledSetSecond || (() => { });
|
|
6892
|
+
const setMeridiem = isControlled
|
|
6893
|
+
? setInternalMeridiem
|
|
6894
|
+
: uncontrolledSetMeridiem || (() => { });
|
|
6895
|
+
// Sync internal state with controlled value prop
|
|
6896
|
+
const prevValueRef = React.useRef(controlledValue);
|
|
6897
|
+
React.useEffect(() => {
|
|
6898
|
+
if (!isControlled)
|
|
6899
|
+
return;
|
|
6900
|
+
if (prevValueRef.current === controlledValue) {
|
|
6901
|
+
return;
|
|
6902
|
+
}
|
|
6903
|
+
prevValueRef.current = controlledValue;
|
|
6904
|
+
const parsed = parseTimeString(controlledValue);
|
|
6905
|
+
setInternalHour(parsed.hour);
|
|
6906
|
+
setInternalMinute(parsed.minute);
|
|
6907
|
+
if (is24Hour) {
|
|
6908
|
+
setInternalSecond(parsed.second);
|
|
6909
|
+
}
|
|
6910
|
+
else {
|
|
6911
|
+
setInternalMeridiem(parsed.meridiem);
|
|
6912
|
+
}
|
|
6913
|
+
}, [controlledValue, isControlled, is24Hour]);
|
|
6914
|
+
// Wrapper onChange that calls both controlled and uncontrolled onChange
|
|
6915
|
+
const handleTimeChange = (newHour, newMinute, newSecond, newMeridiem) => {
|
|
6916
|
+
if (isControlled) {
|
|
6917
|
+
const timeString = formatTimeString(newHour, newMinute, newSecond, newMeridiem);
|
|
6918
|
+
controlledOnChange?.(timeString);
|
|
6919
|
+
}
|
|
6920
|
+
else {
|
|
6921
|
+
// Call legacy onTimeChange if provided
|
|
6922
|
+
if (onTimeChange) {
|
|
6923
|
+
if (is24Hour) {
|
|
6924
|
+
const timeChange24h = onTimeChange;
|
|
6925
|
+
timeChange24h({
|
|
6926
|
+
hour: newHour,
|
|
6927
|
+
minute: newMinute,
|
|
6928
|
+
second: newSecond,
|
|
6929
|
+
});
|
|
6930
|
+
}
|
|
6931
|
+
else {
|
|
6932
|
+
const timeChange12h = onTimeChange;
|
|
6933
|
+
timeChange12h({
|
|
6934
|
+
hour: newHour,
|
|
6935
|
+
minute: newMinute,
|
|
6936
|
+
meridiem: newMeridiem,
|
|
6937
|
+
});
|
|
6938
|
+
}
|
|
6939
|
+
}
|
|
6940
|
+
}
|
|
6941
|
+
};
|
|
6942
|
+
const [inputValue, setInputValue] = React.useState('');
|
|
6943
|
+
// Sync inputValue with current time
|
|
6944
|
+
React.useEffect(() => {
|
|
6945
|
+
if (is24Hour && second !== undefined) {
|
|
6946
|
+
if (hour !== null && minute !== null && second !== null) {
|
|
6947
|
+
const formatted = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}:${second.toString().padStart(2, '0')}`;
|
|
6948
|
+
setInputValue(formatted);
|
|
6949
|
+
}
|
|
6950
|
+
else {
|
|
6951
|
+
setInputValue('');
|
|
6952
|
+
}
|
|
6953
|
+
}
|
|
6954
|
+
else {
|
|
6955
|
+
// 12-hour format - input is managed by combobox
|
|
6956
|
+
setInputValue('');
|
|
6957
|
+
}
|
|
6958
|
+
}, [hour, minute, second, is24Hour]);
|
|
6959
|
+
// Generate time options based on format
|
|
6427
6960
|
const timeOptions = React.useMemo(() => {
|
|
6428
6961
|
const options = [];
|
|
6429
6962
|
// Get start time for comparison if provided
|
|
@@ -6434,32 +6967,25 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
|
|
|
6434
6967
|
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6435
6968
|
if (startDateObj.isValid() && selectedDateObj.isValid()) {
|
|
6436
6969
|
startDateTime = startDateObj;
|
|
6437
|
-
// Only filter if dates are the same
|
|
6438
6970
|
shouldFilterByDate =
|
|
6439
6971
|
startDateObj.format('YYYY-MM-DD') ===
|
|
6440
6972
|
selectedDateObj.format('YYYY-MM-DD');
|
|
6441
6973
|
}
|
|
6442
6974
|
}
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
for (let
|
|
6446
|
-
for (
|
|
6447
|
-
//
|
|
6448
|
-
let hour24 = h;
|
|
6449
|
-
if (mer === 'am' && h === 12)
|
|
6450
|
-
hour24 = 0;
|
|
6451
|
-
else if (mer === 'pm' && h < 12)
|
|
6452
|
-
hour24 = h + 12;
|
|
6453
|
-
// Filter out times that would result in negative duration (only when dates are the same)
|
|
6975
|
+
if (is24Hour) {
|
|
6976
|
+
// Generate 24-hour format options (0-23 for hours)
|
|
6977
|
+
for (let h = 0; h < 24; h++) {
|
|
6978
|
+
for (let m = 0; m < 60; m += 15) {
|
|
6979
|
+
// Filter out times that would result in negative duration
|
|
6454
6980
|
if (startDateTime && selectedDate && shouldFilterByDate) {
|
|
6455
6981
|
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6456
6982
|
const optionDateTime = selectedDateObj
|
|
6457
|
-
.hour(
|
|
6983
|
+
.hour(h)
|
|
6458
6984
|
.minute(m)
|
|
6459
6985
|
.second(0)
|
|
6460
6986
|
.millisecond(0);
|
|
6461
6987
|
if (optionDateTime.isBefore(startDateTime)) {
|
|
6462
|
-
continue;
|
|
6988
|
+
continue;
|
|
6463
6989
|
}
|
|
6464
6990
|
}
|
|
6465
6991
|
// Calculate duration if startTime is provided
|
|
@@ -6467,7 +6993,7 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
|
|
|
6467
6993
|
if (startDateTime && selectedDate) {
|
|
6468
6994
|
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6469
6995
|
const optionDateTime = selectedDateObj
|
|
6470
|
-
.hour(
|
|
6996
|
+
.hour(h)
|
|
6471
6997
|
.minute(m)
|
|
6472
6998
|
.second(0)
|
|
6473
6999
|
.millisecond(0);
|
|
@@ -6492,58 +7018,204 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
|
|
|
6492
7018
|
}
|
|
6493
7019
|
}
|
|
6494
7020
|
}
|
|
6495
|
-
const
|
|
6496
|
-
const minuteDisplay = m.toString().padStart(2, '0');
|
|
6497
|
-
const timeDisplay = `${hourDisplay}:${minuteDisplay} ${mer.toUpperCase()}`;
|
|
7021
|
+
const timeDisplay = `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:00`;
|
|
6498
7022
|
options.push({
|
|
6499
7023
|
label: timeDisplay,
|
|
6500
|
-
value: `${h}:${m}
|
|
7024
|
+
value: `${h}:${m}:0`,
|
|
6501
7025
|
hour: h,
|
|
6502
7026
|
minute: m,
|
|
6503
|
-
|
|
6504
|
-
searchText: timeDisplay,
|
|
7027
|
+
second: 0,
|
|
7028
|
+
searchText: timeDisplay,
|
|
6505
7029
|
durationText,
|
|
6506
7030
|
});
|
|
6507
7031
|
}
|
|
6508
7032
|
}
|
|
6509
7033
|
}
|
|
7034
|
+
else {
|
|
7035
|
+
// Generate 12-hour format options (1-12 for hours, AM/PM)
|
|
7036
|
+
for (let h = 1; h <= 12; h++) {
|
|
7037
|
+
for (let m = 0; m < 60; m += 15) {
|
|
7038
|
+
for (const mer of ['am', 'pm']) {
|
|
7039
|
+
// Convert 12-hour to 24-hour for comparison
|
|
7040
|
+
let hour24 = h;
|
|
7041
|
+
if (mer === 'am' && h === 12)
|
|
7042
|
+
hour24 = 0;
|
|
7043
|
+
else if (mer === 'pm' && h < 12)
|
|
7044
|
+
hour24 = h + 12;
|
|
7045
|
+
// Filter out times that would result in negative duration
|
|
7046
|
+
if (startDateTime && selectedDate && shouldFilterByDate) {
|
|
7047
|
+
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
7048
|
+
const optionDateTime = selectedDateObj
|
|
7049
|
+
.hour(hour24)
|
|
7050
|
+
.minute(m)
|
|
7051
|
+
.second(0)
|
|
7052
|
+
.millisecond(0);
|
|
7053
|
+
if (optionDateTime.isBefore(startDateTime)) {
|
|
7054
|
+
continue;
|
|
7055
|
+
}
|
|
7056
|
+
}
|
|
7057
|
+
// Calculate duration if startTime is provided
|
|
7058
|
+
let durationText;
|
|
7059
|
+
if (startDateTime && selectedDate) {
|
|
7060
|
+
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
7061
|
+
const optionDateTime = selectedDateObj
|
|
7062
|
+
.hour(hour24)
|
|
7063
|
+
.minute(m)
|
|
7064
|
+
.second(0)
|
|
7065
|
+
.millisecond(0);
|
|
7066
|
+
if (optionDateTime.isValid() &&
|
|
7067
|
+
optionDateTime.isAfter(startDateTime)) {
|
|
7068
|
+
const diffMs = optionDateTime.diff(startDateTime);
|
|
7069
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
7070
|
+
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
7071
|
+
const diffSeconds = Math.floor((diffMs % (1000 * 60)) / 1000);
|
|
7072
|
+
if (diffHours > 0 || diffMinutes > 0 || diffSeconds > 0) {
|
|
7073
|
+
let diffText = '';
|
|
7074
|
+
if (diffHours > 0) {
|
|
7075
|
+
diffText = `${diffHours}h ${diffMinutes}m`;
|
|
7076
|
+
}
|
|
7077
|
+
else if (diffMinutes > 0) {
|
|
7078
|
+
diffText = `${diffMinutes}m ${diffSeconds}s`;
|
|
7079
|
+
}
|
|
7080
|
+
else {
|
|
7081
|
+
diffText = `${diffSeconds}s`;
|
|
7082
|
+
}
|
|
7083
|
+
durationText = `+${diffText}`;
|
|
7084
|
+
}
|
|
7085
|
+
}
|
|
7086
|
+
}
|
|
7087
|
+
const hourDisplay = h.toString();
|
|
7088
|
+
const minuteDisplay = m.toString().padStart(2, '0');
|
|
7089
|
+
const timeDisplay = `${hourDisplay}:${minuteDisplay} ${mer.toUpperCase()}`;
|
|
7090
|
+
options.push({
|
|
7091
|
+
label: timeDisplay,
|
|
7092
|
+
value: `${h}:${m}:${mer}`,
|
|
7093
|
+
hour: h,
|
|
7094
|
+
minute: m,
|
|
7095
|
+
meridiem: mer,
|
|
7096
|
+
searchText: timeDisplay,
|
|
7097
|
+
durationText,
|
|
7098
|
+
});
|
|
7099
|
+
}
|
|
7100
|
+
}
|
|
7101
|
+
}
|
|
7102
|
+
// Sort 12-hour options by time (convert to 24-hour for proper chronological sorting)
|
|
7103
|
+
return options.sort((a, b) => {
|
|
7104
|
+
const a12 = a;
|
|
7105
|
+
const b12 = b;
|
|
7106
|
+
let hour24A = a12.hour;
|
|
7107
|
+
if (a12.meridiem === 'am' && a12.hour === 12)
|
|
7108
|
+
hour24A = 0;
|
|
7109
|
+
else if (a12.meridiem === 'pm' && a12.hour < 12)
|
|
7110
|
+
hour24A = a12.hour + 12;
|
|
7111
|
+
let hour24B = b12.hour;
|
|
7112
|
+
if (b12.meridiem === 'am' && b12.hour === 12)
|
|
7113
|
+
hour24B = 0;
|
|
7114
|
+
else if (b12.meridiem === 'pm' && b12.hour < 12)
|
|
7115
|
+
hour24B = b12.hour + 12;
|
|
7116
|
+
if (hour24A !== hour24B) {
|
|
7117
|
+
return hour24A - hour24B;
|
|
7118
|
+
}
|
|
7119
|
+
return a12.minute - b12.minute;
|
|
7120
|
+
});
|
|
7121
|
+
}
|
|
6510
7122
|
return options;
|
|
6511
|
-
}, [startTime, selectedDate, timezone]);
|
|
7123
|
+
}, [startTime, selectedDate, timezone, is24Hour]);
|
|
7124
|
+
// itemToString returns only the clean display text (no metadata)
|
|
7125
|
+
const itemToString = React.useMemo(() => {
|
|
7126
|
+
return (item) => {
|
|
7127
|
+
return item.searchText;
|
|
7128
|
+
};
|
|
7129
|
+
}, []);
|
|
7130
|
+
// Custom filter function
|
|
6512
7131
|
const { contains } = react.useFilter({ sensitivity: 'base' });
|
|
7132
|
+
const customTimeFilter = React.useMemo(() => {
|
|
7133
|
+
if (is24Hour) {
|
|
7134
|
+
return contains; // Simple contains filter for 24-hour format
|
|
7135
|
+
}
|
|
7136
|
+
// For 12-hour format, support both 12-hour and 24-hour input
|
|
7137
|
+
return (itemText, filterText) => {
|
|
7138
|
+
if (!filterText) {
|
|
7139
|
+
return true;
|
|
7140
|
+
}
|
|
7141
|
+
const lowerItemText = itemText.toLowerCase();
|
|
7142
|
+
const lowerFilterText = filterText.toLowerCase();
|
|
7143
|
+
if (lowerItemText.includes(lowerFilterText)) {
|
|
7144
|
+
return true;
|
|
7145
|
+
}
|
|
7146
|
+
const item = timeOptions.find((opt) => opt.searchText.toLowerCase() === lowerItemText);
|
|
7147
|
+
if (!item || !('meridiem' in item)) {
|
|
7148
|
+
return false;
|
|
7149
|
+
}
|
|
7150
|
+
// Convert item to 24-hour format for matching
|
|
7151
|
+
let hour24 = item.hour;
|
|
7152
|
+
if (item.meridiem === 'am' && item.hour === 12)
|
|
7153
|
+
hour24 = 0;
|
|
7154
|
+
else if (item.meridiem === 'pm' && item.hour < 12)
|
|
7155
|
+
hour24 = item.hour + 12;
|
|
7156
|
+
const hour24Str = hour24.toString().padStart(2, '0');
|
|
7157
|
+
const minuteStr = item.minute.toString().padStart(2, '0');
|
|
7158
|
+
const formats = [
|
|
7159
|
+
`${hour24Str}:${minuteStr}`,
|
|
7160
|
+
`${hour24Str}${minuteStr}`,
|
|
7161
|
+
hour24Str,
|
|
7162
|
+
`${hour24}:${minuteStr}`,
|
|
7163
|
+
hour24.toString(),
|
|
7164
|
+
];
|
|
7165
|
+
return formats.some((format) => format.toLowerCase().includes(lowerFilterText) ||
|
|
7166
|
+
lowerFilterText.includes(format.toLowerCase()));
|
|
7167
|
+
};
|
|
7168
|
+
}, [timeOptions, is24Hour, contains]);
|
|
6513
7169
|
const { collection, filter } = react.useListCollection({
|
|
6514
7170
|
initialItems: timeOptions,
|
|
6515
|
-
itemToString:
|
|
7171
|
+
itemToString: itemToString,
|
|
6516
7172
|
itemToValue: (item) => item.value,
|
|
6517
|
-
filter:
|
|
7173
|
+
filter: customTimeFilter,
|
|
6518
7174
|
});
|
|
6519
7175
|
// Get current value string for combobox
|
|
6520
7176
|
const currentValue = React.useMemo(() => {
|
|
6521
|
-
if (
|
|
6522
|
-
|
|
7177
|
+
if (is24Hour) {
|
|
7178
|
+
if (hour === null || minute === null || second === null) {
|
|
7179
|
+
return '';
|
|
7180
|
+
}
|
|
7181
|
+
return `${hour}:${minute}:${second}`;
|
|
7182
|
+
}
|
|
7183
|
+
else {
|
|
7184
|
+
if (hour === null || minute === null || meridiem === null) {
|
|
7185
|
+
return '';
|
|
7186
|
+
}
|
|
7187
|
+
return `${hour}:${minute}:${meridiem}`;
|
|
6523
7188
|
}
|
|
6524
|
-
|
|
6525
|
-
}, [hour, minute, meridiem]);
|
|
7189
|
+
}, [hour, minute, second, meridiem, is24Hour]);
|
|
6526
7190
|
// Calculate duration difference
|
|
6527
7191
|
const durationDiff = React.useMemo(() => {
|
|
6528
|
-
if (!startTime ||
|
|
6529
|
-
!selectedDate ||
|
|
6530
|
-
hour === null ||
|
|
6531
|
-
minute === null ||
|
|
6532
|
-
meridiem === null) {
|
|
7192
|
+
if (!startTime || !selectedDate || hour === null || minute === null) {
|
|
6533
7193
|
return null;
|
|
6534
7194
|
}
|
|
7195
|
+
if (is24Hour) {
|
|
7196
|
+
if (second === null)
|
|
7197
|
+
return null;
|
|
7198
|
+
}
|
|
7199
|
+
else {
|
|
7200
|
+
if (meridiem === null)
|
|
7201
|
+
return null;
|
|
7202
|
+
}
|
|
6535
7203
|
const startDateObj = dayjs(startTime).tz(timezone);
|
|
6536
7204
|
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6537
|
-
// Convert
|
|
7205
|
+
// Convert to 24-hour format
|
|
6538
7206
|
let hour24 = hour;
|
|
6539
|
-
if (
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
7207
|
+
if (!is24Hour && meridiem) {
|
|
7208
|
+
if (meridiem === 'am' && hour === 12)
|
|
7209
|
+
hour24 = 0;
|
|
7210
|
+
else if (meridiem === 'pm' && hour < 12)
|
|
7211
|
+
hour24 = hour + 12;
|
|
7212
|
+
}
|
|
6543
7213
|
const currentDateTime = selectedDateObj
|
|
6544
7214
|
.hour(hour24)
|
|
6545
7215
|
.minute(minute)
|
|
6546
|
-
.second(
|
|
7216
|
+
.second(is24Hour && second !== null && second !== undefined
|
|
7217
|
+
? second
|
|
7218
|
+
: 0)
|
|
6547
7219
|
.millisecond(0);
|
|
6548
7220
|
if (!startDateObj.isValid() || !currentDateTime.isValid()) {
|
|
6549
7221
|
return null;
|
|
@@ -6569,13 +7241,28 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
|
|
|
6569
7241
|
return `+${diffText}`;
|
|
6570
7242
|
}
|
|
6571
7243
|
return null;
|
|
6572
|
-
}, [
|
|
7244
|
+
}, [
|
|
7245
|
+
hour,
|
|
7246
|
+
minute,
|
|
7247
|
+
second,
|
|
7248
|
+
meridiem,
|
|
7249
|
+
startTime,
|
|
7250
|
+
selectedDate,
|
|
7251
|
+
timezone,
|
|
7252
|
+
is24Hour,
|
|
7253
|
+
]);
|
|
6573
7254
|
const handleClear = () => {
|
|
6574
7255
|
setHour(null);
|
|
6575
7256
|
setMinute(null);
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
7257
|
+
if (is24Hour && setSecond) {
|
|
7258
|
+
setSecond(null);
|
|
7259
|
+
handleTimeChange(null, null, null, null);
|
|
7260
|
+
}
|
|
7261
|
+
else if (!is24Hour && setMeridiem) {
|
|
7262
|
+
setMeridiem(null);
|
|
7263
|
+
handleTimeChange(null, null, null, null);
|
|
7264
|
+
}
|
|
7265
|
+
filter('');
|
|
6579
7266
|
};
|
|
6580
7267
|
const handleValueChange = (details) => {
|
|
6581
7268
|
if (details.value.length === 0) {
|
|
@@ -6587,71 +7274,165 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
|
|
|
6587
7274
|
if (selectedOption) {
|
|
6588
7275
|
setHour(selectedOption.hour);
|
|
6589
7276
|
setMinute(selectedOption.minute);
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
}
|
|
7277
|
+
filter('');
|
|
7278
|
+
if (is24Hour) {
|
|
7279
|
+
const opt24 = selectedOption;
|
|
7280
|
+
if (setSecond)
|
|
7281
|
+
setSecond(opt24.second);
|
|
7282
|
+
handleTimeChange(opt24.hour, opt24.minute, opt24.second, null);
|
|
7283
|
+
}
|
|
7284
|
+
else {
|
|
7285
|
+
const opt12 = selectedOption;
|
|
7286
|
+
if (setMeridiem)
|
|
7287
|
+
setMeridiem(opt12.meridiem);
|
|
7288
|
+
handleTimeChange(opt12.hour, opt12.minute, null, opt12.meridiem);
|
|
7289
|
+
}
|
|
6597
7290
|
}
|
|
6598
7291
|
};
|
|
6599
7292
|
// Parse input value and update state
|
|
6600
7293
|
const parseAndCommitInput = (value) => {
|
|
6601
7294
|
const trimmedValue = value.trim();
|
|
6602
|
-
// Filter the collection based on input
|
|
6603
7295
|
filter(trimmedValue);
|
|
6604
7296
|
if (!trimmedValue) {
|
|
6605
7297
|
return;
|
|
6606
7298
|
}
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
|
|
7299
|
+
if (is24Hour) {
|
|
7300
|
+
// Parse 24-hour format: "HH:mm:ss" or "HH:mm" or "HHmmss" or "HHmm"
|
|
7301
|
+
const timePattern = /^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?$/;
|
|
7302
|
+
const match = trimmedValue.match(timePattern);
|
|
7303
|
+
if (match) {
|
|
7304
|
+
const parsedHour = parseInt(match[1], 10);
|
|
7305
|
+
const parsedMinute = parseInt(match[2], 10);
|
|
7306
|
+
const parsedSecond = match[3] ? parseInt(match[3], 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
|
+
}
|
|
7320
|
+
}
|
|
7321
|
+
// Try numbers only format: "123045" or "1230"
|
|
7322
|
+
const numbersOnly = trimmedValue.replace(/[^0-9]/g, '');
|
|
7323
|
+
if (numbersOnly.length >= 4) {
|
|
7324
|
+
const parsedHour = parseInt(numbersOnly.slice(0, 2), 10);
|
|
7325
|
+
const parsedMinute = parseInt(numbersOnly.slice(2, 4), 10);
|
|
7326
|
+
const parsedSecond = numbersOnly.length >= 6 ? parseInt(numbersOnly.slice(4, 6), 10) : 0;
|
|
7327
|
+
if (parsedHour >= 0 &&
|
|
7328
|
+
parsedHour <= 23 &&
|
|
7329
|
+
parsedMinute >= 0 &&
|
|
7330
|
+
parsedMinute <= 59 &&
|
|
7331
|
+
parsedSecond >= 0 &&
|
|
7332
|
+
parsedSecond <= 59) {
|
|
7333
|
+
setHour(parsedHour);
|
|
7334
|
+
setMinute(parsedMinute);
|
|
7335
|
+
if (setSecond)
|
|
7336
|
+
setSecond(parsedSecond);
|
|
7337
|
+
handleTimeChange(parsedHour, parsedMinute, parsedSecond, null);
|
|
7338
|
+
return;
|
|
7339
|
+
}
|
|
6628
7340
|
}
|
|
6629
7341
|
}
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
7342
|
+
else {
|
|
7343
|
+
// Parse 24-hour format first (e.g., "13:30", "14:00", "1330", "1400")
|
|
7344
|
+
const timePattern24Hour = /^(\d{1,2}):?(\d{2})$/;
|
|
7345
|
+
const match24Hour = trimmedValue.match(timePattern24Hour);
|
|
7346
|
+
if (match24Hour) {
|
|
7347
|
+
const parsedHour24 = parseInt(match24Hour[1], 10);
|
|
7348
|
+
const parsedMinute = parseInt(match24Hour[2], 10);
|
|
7349
|
+
if (parsedHour24 >= 0 &&
|
|
7350
|
+
parsedHour24 <= 23 &&
|
|
7351
|
+
parsedMinute >= 0 &&
|
|
7352
|
+
parsedMinute <= 59) {
|
|
7353
|
+
// Convert 24-hour to 12-hour format
|
|
7354
|
+
let hour12;
|
|
7355
|
+
let meridiem;
|
|
7356
|
+
if (parsedHour24 === 0) {
|
|
7357
|
+
hour12 = 12;
|
|
7358
|
+
meridiem = 'am';
|
|
7359
|
+
}
|
|
7360
|
+
else if (parsedHour24 === 12) {
|
|
7361
|
+
hour12 = 12;
|
|
7362
|
+
meridiem = 'pm';
|
|
7363
|
+
}
|
|
7364
|
+
else if (parsedHour24 > 12) {
|
|
7365
|
+
hour12 = parsedHour24 - 12;
|
|
7366
|
+
meridiem = 'pm';
|
|
7367
|
+
}
|
|
7368
|
+
else {
|
|
7369
|
+
hour12 = parsedHour24;
|
|
7370
|
+
meridiem = 'am';
|
|
7371
|
+
}
|
|
7372
|
+
setHour(hour12);
|
|
7373
|
+
setMinute(parsedMinute);
|
|
7374
|
+
if (setMeridiem)
|
|
7375
|
+
setMeridiem(meridiem);
|
|
7376
|
+
handleTimeChange(hour12, parsedMinute, null, meridiem);
|
|
7377
|
+
return;
|
|
7378
|
+
}
|
|
7379
|
+
}
|
|
7380
|
+
// Parse formats like "1:30 PM", "1:30PM", "1:30 pm", "1:30pm"
|
|
7381
|
+
const timePattern12Hour = /^(\d{1,2}):(\d{1,2})\s*(am|pm|AM|PM)$/i;
|
|
7382
|
+
const match12Hour = trimmedValue.match(timePattern12Hour);
|
|
7383
|
+
if (match12Hour) {
|
|
7384
|
+
const parsedHour = parseInt(match12Hour[1], 10);
|
|
7385
|
+
const parsedMinute = parseInt(match12Hour[2], 10);
|
|
7386
|
+
const parsedMeridiem = match12Hour[3].toLowerCase();
|
|
6640
7387
|
if (parsedHour >= 1 &&
|
|
6641
7388
|
parsedHour <= 12 &&
|
|
6642
7389
|
parsedMinute >= 0 &&
|
|
6643
7390
|
parsedMinute <= 59) {
|
|
6644
7391
|
setHour(parsedHour);
|
|
6645
7392
|
setMinute(parsedMinute);
|
|
6646
|
-
setMeridiem
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
7393
|
+
if (setMeridiem)
|
|
7394
|
+
setMeridiem(parsedMeridiem);
|
|
7395
|
+
handleTimeChange(parsedHour, parsedMinute, null, parsedMeridiem);
|
|
7396
|
+
return;
|
|
7397
|
+
}
|
|
7398
|
+
}
|
|
7399
|
+
// Parse formats like "12am" or "1pm" (hour only with meridiem, no minutes)
|
|
7400
|
+
const timePatternHourOnly = /^(\d{1,2})\s*(am|pm|AM|PM)$/i;
|
|
7401
|
+
const matchHourOnly = trimmedValue.match(timePatternHourOnly);
|
|
7402
|
+
if (matchHourOnly) {
|
|
7403
|
+
const parsedHour = parseInt(matchHourOnly[1], 10);
|
|
7404
|
+
const parsedMeridiem = matchHourOnly[2].toLowerCase();
|
|
7405
|
+
if (parsedHour >= 1 && parsedHour <= 12) {
|
|
7406
|
+
setHour(parsedHour);
|
|
7407
|
+
setMinute(0); // Default to 0 minutes when only hour is provided
|
|
7408
|
+
if (setMeridiem)
|
|
7409
|
+
setMeridiem(parsedMeridiem);
|
|
7410
|
+
handleTimeChange(parsedHour, 0, null, parsedMeridiem);
|
|
6652
7411
|
return;
|
|
6653
7412
|
}
|
|
6654
7413
|
}
|
|
7414
|
+
// Try to parse formats like "130pm" or "130 pm" (without colon, with minutes)
|
|
7415
|
+
const timePatternNoColon = /^(\d{1,4})\s*(am|pm|AM|PM)$/i;
|
|
7416
|
+
const matchNoColon = trimmedValue.match(timePatternNoColon);
|
|
7417
|
+
if (matchNoColon) {
|
|
7418
|
+
const numbersOnly = matchNoColon[1];
|
|
7419
|
+
const parsedMeridiem = matchNoColon[2].toLowerCase();
|
|
7420
|
+
if (numbersOnly.length >= 3) {
|
|
7421
|
+
const parsedHour = parseInt(numbersOnly.slice(0, -2), 10);
|
|
7422
|
+
const parsedMinute = parseInt(numbersOnly.slice(-2), 10);
|
|
7423
|
+
if (parsedHour >= 1 &&
|
|
7424
|
+
parsedHour <= 12 &&
|
|
7425
|
+
parsedMinute >= 0 &&
|
|
7426
|
+
parsedMinute <= 59) {
|
|
7427
|
+
setHour(parsedHour);
|
|
7428
|
+
setMinute(parsedMinute);
|
|
7429
|
+
if (setMeridiem)
|
|
7430
|
+
setMeridiem(parsedMeridiem);
|
|
7431
|
+
handleTimeChange(parsedHour, parsedMinute, null, parsedMeridiem);
|
|
7432
|
+
return;
|
|
7433
|
+
}
|
|
7434
|
+
}
|
|
7435
|
+
}
|
|
6655
7436
|
}
|
|
6656
7437
|
// Parse failed, select first result
|
|
6657
7438
|
selectFirstResult();
|
|
@@ -6662,58 +7443,87 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
|
|
|
6662
7443
|
const firstItem = collection.items[0];
|
|
6663
7444
|
setHour(firstItem.hour);
|
|
6664
7445
|
setMinute(firstItem.minute);
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
}
|
|
7446
|
+
filter('');
|
|
7447
|
+
if (is24Hour) {
|
|
7448
|
+
const opt24 = firstItem;
|
|
7449
|
+
if (setSecond)
|
|
7450
|
+
setSecond(opt24.second);
|
|
7451
|
+
handleTimeChange(opt24.hour, opt24.minute, opt24.second, null);
|
|
7452
|
+
}
|
|
7453
|
+
else {
|
|
7454
|
+
const opt12 = firstItem;
|
|
7455
|
+
if (setMeridiem)
|
|
7456
|
+
setMeridiem(opt12.meridiem);
|
|
7457
|
+
handleTimeChange(opt12.hour, opt12.minute, null, opt12.meridiem);
|
|
7458
|
+
}
|
|
6672
7459
|
}
|
|
6673
7460
|
};
|
|
6674
7461
|
const handleInputValueChange = (details) => {
|
|
6675
|
-
|
|
7462
|
+
if (is24Hour) {
|
|
7463
|
+
setInputValue(details.inputValue);
|
|
7464
|
+
}
|
|
6676
7465
|
filter(details.inputValue);
|
|
6677
7466
|
};
|
|
6678
7467
|
const handleFocus = (e) => {
|
|
6679
|
-
// Select all text when focusing
|
|
6680
7468
|
e.target.select();
|
|
6681
7469
|
};
|
|
6682
7470
|
const handleBlur = (e) => {
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
7471
|
+
const inputVal = e.target.value;
|
|
7472
|
+
if (is24Hour) {
|
|
7473
|
+
setInputValue(inputVal);
|
|
7474
|
+
}
|
|
7475
|
+
if (inputVal) {
|
|
7476
|
+
parseAndCommitInput(inputVal);
|
|
6687
7477
|
}
|
|
6688
7478
|
};
|
|
6689
7479
|
const handleKeyDown = (e) => {
|
|
6690
|
-
// Commit input on Enter key
|
|
6691
7480
|
if (e.key === 'Enter') {
|
|
6692
7481
|
e.preventDefault();
|
|
6693
|
-
const
|
|
6694
|
-
if (
|
|
6695
|
-
|
|
7482
|
+
const inputVal = e.currentTarget.value;
|
|
7483
|
+
if (is24Hour) {
|
|
7484
|
+
setInputValue(inputVal);
|
|
7485
|
+
}
|
|
7486
|
+
if (inputVal) {
|
|
7487
|
+
parseAndCommitInput(inputVal);
|
|
6696
7488
|
}
|
|
6697
|
-
// Blur the input
|
|
6698
7489
|
e.currentTarget?.blur();
|
|
6699
7490
|
}
|
|
6700
7491
|
};
|
|
6701
|
-
return (jsxRuntime.jsx(react.Flex, { direction: "column", gap: 3, children: jsxRuntime.jsxs(react.Flex, { alignItems: "center", gap: "2", width: "auto", minWidth: "300px", children: [jsxRuntime.jsxs(react.Combobox.Root, { collection: collection, value: currentValue ? [currentValue] : [], onValueChange: handleValueChange, onInputValueChange: handleInputValueChange, allowCustomValue: true, selectionBehavior: "replace",
|
|
7492
|
+
return (jsxRuntime.jsx(react.Flex, { direction: "column", gap: 3, children: jsxRuntime.jsxs(react.Flex, { alignItems: "center", gap: "2", width: "auto", minWidth: "300px", children: [jsxRuntime.jsxs(react.Combobox.Root, { collection: collection, value: currentValue ? [currentValue] : [], onValueChange: handleValueChange, onInputValueChange: handleInputValueChange, allowCustomValue: true, selectionBehavior: "replace", flex: 1, children: [jsxRuntime.jsxs(react.Combobox.Control, { children: [jsxRuntime.jsx(react.InputGroup, { startElement: jsxRuntime.jsx(bs.BsClock, {}), children: jsxRuntime.jsx(react.Combobox.Input, { value: is24Hour ? inputValue : undefined, placeholder: labels?.placeholder ?? (is24Hour ? 'HH:mm:ss' : 'hh:mm AM/PM'), onFocus: handleFocus, onBlur: handleBlur, onKeyDown: handleKeyDown }) }), jsxRuntime.jsx(react.Combobox.IndicatorGroup, { children: jsxRuntime.jsx(react.Combobox.Trigger, {}) })] }), jsxRuntime.jsx(react.Portal, { disabled: !portalled, children: jsxRuntime.jsx(react.Combobox.Positioner, { children: jsxRuntime.jsxs(react.Combobox.Content, { children: [jsxRuntime.jsx(react.Combobox.Empty, { children: labels?.emptyMessage ?? 'No time found' }), collection.items.map((item) => (jsxRuntime.jsxs(react.Combobox.Item, { item: item, children: [jsxRuntime.jsxs(react.Flex, { alignItems: "center", gap: 2, width: "100%", children: [jsxRuntime.jsx(react.Text, { flex: 1, children: item.label }), item.durationText && (jsxRuntime.jsx(react.Tag.Root, { size: "sm", children: jsxRuntime.jsx(react.Tag.Label, { children: item.durationText }) }))] }), jsxRuntime.jsx(react.Combobox.ItemIndicator, {})] }, item.value)))] }) }) })] }), durationDiff && (jsxRuntime.jsx(react.Tag.Root, { size: "sm", children: jsxRuntime.jsx(react.Tag.Label, { children: durationDiff }) }))] }) }));
|
|
6702
7493
|
};
|
|
6703
7494
|
|
|
6704
7495
|
dayjs.extend(timezone);
|
|
6705
7496
|
const TimePicker = ({ column, schema, prefix }) => {
|
|
6706
7497
|
const { watch, formState: { errors }, setValue, } = reactHookForm.useFormContext();
|
|
6707
7498
|
const { timezone, insideDialog, timePickerLabels } = useSchemaContext();
|
|
6708
|
-
const { required, gridColumn = 'span 12', gridRow = 'span 1', timeFormat = 'HH:mm:ssZ', displayTimeFormat = 'hh:mm A', } = schema;
|
|
7499
|
+
const { required, gridColumn = 'span 12', gridRow = 'span 1', timeFormat = 'HH:mm:ssZ', displayTimeFormat = 'hh:mm A', startTimeField, selectedDateField, } = schema;
|
|
6709
7500
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
6710
7501
|
const colLabel = `${prefix}${column}`;
|
|
6711
7502
|
const formI18n = useFormI18n(column, prefix, schema);
|
|
6712
7503
|
const [open, setOpen] = React.useState(false);
|
|
6713
7504
|
const value = watch(colLabel);
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
7505
|
+
// Watch startTime and selectedDate fields for offset calculation
|
|
7506
|
+
const startTimeValue = startTimeField
|
|
7507
|
+
? watch(`${prefix}${startTimeField}`)
|
|
7508
|
+
: undefined;
|
|
7509
|
+
const selectedDateValue = selectedDateField
|
|
7510
|
+
? watch(`${prefix}${selectedDateField}`)
|
|
7511
|
+
: undefined;
|
|
7512
|
+
// Convert to ISO string format for startTime if it's a date-time string
|
|
7513
|
+
const startTime = startTimeValue
|
|
7514
|
+
? dayjs(startTimeValue).tz(timezone).isValid()
|
|
7515
|
+
? dayjs(startTimeValue).tz(timezone).toISOString()
|
|
7516
|
+
: undefined
|
|
7517
|
+
: undefined;
|
|
7518
|
+
// Convert selectedDate to YYYY-MM-DD format
|
|
7519
|
+
const selectedDate = selectedDateValue
|
|
7520
|
+
? dayjs(selectedDateValue).tz(timezone).isValid()
|
|
7521
|
+
? dayjs(selectedDateValue).tz(timezone).format('YYYY-MM-DD')
|
|
7522
|
+
: undefined
|
|
7523
|
+
: undefined;
|
|
7524
|
+
const displayedTime = dayjs(`1970-01-01T${value}`).tz(timezone).isValid()
|
|
7525
|
+
? dayjs(`1970-01-01T${value}`).tz(timezone).format(displayTimeFormat)
|
|
7526
|
+
: '';
|
|
6717
7527
|
// Parse the initial time parts from the time string (HH:mm:ssZ)
|
|
6718
7528
|
const parseTime = (time) => {
|
|
6719
7529
|
if (!time)
|
|
@@ -6766,884 +7576,900 @@ const TimePicker = ({ column, schema, prefix }) => {
|
|
|
6766
7576
|
return (jsxRuntime.jsx(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
6767
7577
|
gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: jsxRuntime.jsxs(react.Popover.Root, { open: open, onOpenChange: (e) => setOpen(e.open), closeOnInteractOutside: true, children: [jsxRuntime.jsx(react.Popover.Trigger, { asChild: true, children: jsxRuntime.jsxs(Button, { size: "sm", variant: "outline", onClick: () => {
|
|
6768
7578
|
setOpen(true);
|
|
6769
|
-
}, justifyContent: 'start', children: [jsxRuntime.jsx(io.IoMdClock, {}), !!value ? `${displayedTime}` : ''] }) }), insideDialog ? (jsxRuntime.jsx(react.Popover.Positioner, { children: jsxRuntime.jsx(react.Popover.Content, { maxH: "70vh", overflowY: "auto", children: jsxRuntime.jsx(react.Popover.Body, { overflow: "visible", children: jsxRuntime.jsx(TimePicker$1, { hour: hour, setHour: setHour, minute: minute, setMinute: setMinute, meridiem: meridiem, setMeridiem: setMeridiem, onChange: handleTimeChange, labels: timePickerLabels }) }) }) })) : (jsxRuntime.jsx(react.Portal, { children: jsxRuntime.jsx(react.Popover.Positioner, { children: jsxRuntime.jsx(react.Popover.Content, { children: jsxRuntime.jsx(react.Popover.Body, { children: jsxRuntime.jsx(TimePicker$1, { hour: hour, setHour: setHour, minute: minute, setMinute: setMinute, meridiem: meridiem, setMeridiem: setMeridiem, onChange: handleTimeChange, labels: timePickerLabels }) }) }) }) }))] }) }));
|
|
7579
|
+
}, justifyContent: 'start', children: [jsxRuntime.jsx(io.IoMdClock, {}), !!value ? `${displayedTime}` : ''] }) }), insideDialog ? (jsxRuntime.jsx(react.Popover.Positioner, { children: jsxRuntime.jsx(react.Popover.Content, { maxH: "70vh", overflowY: "auto", children: jsxRuntime.jsx(react.Popover.Body, { overflow: "visible", children: jsxRuntime.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 }) }) }) })) : (jsxRuntime.jsx(react.Portal, { children: jsxRuntime.jsx(react.Popover.Positioner, { children: jsxRuntime.jsx(react.Popover.Content, { children: jsxRuntime.jsx(react.Popover.Body, { children: jsxRuntime.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 }) }) }) }) }))] }) }));
|
|
6770
7580
|
};
|
|
6771
7581
|
|
|
6772
7582
|
dayjs.extend(utc);
|
|
6773
7583
|
dayjs.extend(timezone);
|
|
6774
7584
|
dayjs.extend(customParseFormat);
|
|
6775
|
-
function
|
|
7585
|
+
function DateTimePicker$1({ value, onChange, format = 'date-time', showSeconds = false, labels = {
|
|
6776
7586
|
monthNamesShort: [
|
|
6777
|
-
'
|
|
6778
|
-
'
|
|
6779
|
-
'
|
|
6780
|
-
'
|
|
7587
|
+
'January',
|
|
7588
|
+
'February',
|
|
7589
|
+
'March',
|
|
7590
|
+
'April',
|
|
6781
7591
|
'May',
|
|
6782
|
-
'
|
|
6783
|
-
'
|
|
6784
|
-
'
|
|
6785
|
-
'
|
|
6786
|
-
'
|
|
6787
|
-
'
|
|
6788
|
-
'
|
|
7592
|
+
'June',
|
|
7593
|
+
'July',
|
|
7594
|
+
'August',
|
|
7595
|
+
'September',
|
|
7596
|
+
'October',
|
|
7597
|
+
'November',
|
|
7598
|
+
'December',
|
|
6789
7599
|
],
|
|
6790
7600
|
weekdayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
|
6791
7601
|
backButtonLabel: 'Back',
|
|
6792
|
-
forwardButtonLabel: '
|
|
6793
|
-
}, timezone = 'Asia/Hong_Kong', minDate, maxDate,
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
7602
|
+
forwardButtonLabel: 'Forward',
|
|
7603
|
+
}, timePickerLabels, timezone: tz = 'Asia/Hong_Kong', startTime, minDate, maxDate, portalled = false, defaultDate, defaultTime, showQuickActions = false, quickActionLabels = {
|
|
7604
|
+
yesterday: 'Yesterday',
|
|
7605
|
+
today: 'Today',
|
|
7606
|
+
tomorrow: 'Tomorrow',
|
|
7607
|
+
plus7Days: '+7 Days',
|
|
7608
|
+
}, showTimezoneSelector = false, timezoneOffset: controlledTimezoneOffset, onTimezoneOffsetChange, }) {
|
|
7609
|
+
const is24Hour = format === 'iso-date-time' || showSeconds;
|
|
7610
|
+
// Labels are used in calendarLabels useMemo
|
|
7611
|
+
// Parse value to get date and time
|
|
7612
|
+
const parsedValue = React.useMemo(() => {
|
|
7613
|
+
if (!value)
|
|
7614
|
+
return null;
|
|
7615
|
+
const dateObj = dayjs(value).tz(tz);
|
|
7616
|
+
if (!dateObj.isValid())
|
|
7617
|
+
return null;
|
|
7618
|
+
return dateObj;
|
|
7619
|
+
}, [value, tz]);
|
|
7620
|
+
// Initialize date state
|
|
7621
|
+
const [selectedDate, setSelectedDate] = React.useState(() => {
|
|
7622
|
+
if (parsedValue) {
|
|
7623
|
+
return parsedValue.toDate();
|
|
7624
|
+
}
|
|
7625
|
+
if (defaultDate) {
|
|
7626
|
+
const defaultDateObj = dayjs(defaultDate).tz(tz);
|
|
7627
|
+
return defaultDateObj.isValid() ? defaultDateObj.toDate() : new Date();
|
|
7628
|
+
}
|
|
7629
|
+
return new Date();
|
|
7630
|
+
});
|
|
7631
|
+
// Initialize time state
|
|
7632
|
+
const [hour, setHour] = React.useState(() => {
|
|
7633
|
+
if (parsedValue) {
|
|
7634
|
+
return parsedValue.hour();
|
|
7635
|
+
}
|
|
7636
|
+
if (defaultTime?.hour !== null && defaultTime?.hour !== undefined) {
|
|
7637
|
+
return defaultTime.hour;
|
|
7638
|
+
}
|
|
7639
|
+
return null;
|
|
7640
|
+
});
|
|
7641
|
+
const [minute, setMinute] = React.useState(() => {
|
|
7642
|
+
if (parsedValue) {
|
|
7643
|
+
return parsedValue.minute();
|
|
7644
|
+
}
|
|
7645
|
+
if (defaultTime?.minute !== null && defaultTime?.minute !== undefined) {
|
|
7646
|
+
return defaultTime.minute;
|
|
7647
|
+
}
|
|
7648
|
+
return null;
|
|
7649
|
+
});
|
|
7650
|
+
const [second, setSecond] = React.useState(() => {
|
|
7651
|
+
if (parsedValue) {
|
|
7652
|
+
return parsedValue.second();
|
|
7653
|
+
}
|
|
7654
|
+
if (defaultTime?.second !== null && defaultTime?.second !== undefined) {
|
|
7655
|
+
return defaultTime.second;
|
|
7656
|
+
}
|
|
7657
|
+
return showSeconds ? 0 : null;
|
|
7658
|
+
});
|
|
7659
|
+
const [meridiem, setMeridiem] = React.useState(() => {
|
|
7660
|
+
if (parsedValue) {
|
|
7661
|
+
const h = parsedValue.hour();
|
|
7662
|
+
return h < 12 ? 'am' : 'pm';
|
|
7663
|
+
}
|
|
7664
|
+
if (defaultTime?.meridiem !== null && defaultTime?.meridiem !== undefined) {
|
|
7665
|
+
return defaultTime.meridiem;
|
|
7666
|
+
}
|
|
7667
|
+
return is24Hour ? null : 'am';
|
|
7668
|
+
});
|
|
7669
|
+
// Popover state - separate for date, time, and timezone
|
|
7670
|
+
const [datePopoverOpen, setDatePopoverOpen] = React.useState(false);
|
|
7671
|
+
const [timePopoverOpen, setTimePopoverOpen] = React.useState(false);
|
|
7672
|
+
const [timezonePopoverOpen, setTimezonePopoverOpen] = React.useState(false);
|
|
7673
|
+
const [calendarPopoverOpen, setCalendarPopoverOpen] = React.useState(false);
|
|
7674
|
+
// Timezone offset state (controlled or uncontrolled)
|
|
7675
|
+
const [internalTimezoneOffset, setInternalTimezoneOffset] = React.useState(() => {
|
|
7676
|
+
if (controlledTimezoneOffset !== undefined) {
|
|
7677
|
+
return controlledTimezoneOffset;
|
|
7678
|
+
}
|
|
7679
|
+
if (parsedValue) {
|
|
7680
|
+
return parsedValue.format('Z');
|
|
7681
|
+
}
|
|
7682
|
+
// Default to +08:00
|
|
7683
|
+
return '+08:00';
|
|
7684
|
+
});
|
|
7685
|
+
// Use controlled prop if provided, otherwise use internal state
|
|
7686
|
+
const timezoneOffset = controlledTimezoneOffset ?? internalTimezoneOffset;
|
|
7687
|
+
// Update internal state when controlled prop changes
|
|
6797
7688
|
React.useEffect(() => {
|
|
6798
|
-
if (
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
7689
|
+
if (controlledTimezoneOffset !== undefined) {
|
|
7690
|
+
setInternalTimezoneOffset(controlledTimezoneOffset);
|
|
7691
|
+
}
|
|
7692
|
+
}, [controlledTimezoneOffset]);
|
|
7693
|
+
// Sync timezone offset when value changes (only if uncontrolled)
|
|
7694
|
+
React.useEffect(() => {
|
|
7695
|
+
if (controlledTimezoneOffset === undefined && parsedValue) {
|
|
7696
|
+
const offsetFromValue = parsedValue.format('Z');
|
|
7697
|
+
if (offsetFromValue !== timezoneOffset) {
|
|
7698
|
+
setInternalTimezoneOffset(offsetFromValue);
|
|
7699
|
+
}
|
|
7700
|
+
}
|
|
7701
|
+
}, [parsedValue, controlledTimezoneOffset, timezoneOffset]);
|
|
7702
|
+
// Sync timezone offset when value changes
|
|
7703
|
+
// Generate timezone offset options (UTC-12 to UTC+14)
|
|
7704
|
+
const timezoneOffsetOptions = React.useMemo(() => {
|
|
7705
|
+
const options = [];
|
|
7706
|
+
for (let offset = -12; offset <= 14; offset++) {
|
|
7707
|
+
const sign = offset >= 0 ? '+' : '-';
|
|
7708
|
+
const hours = Math.abs(offset).toString().padStart(2, '0');
|
|
7709
|
+
const value = `${sign}${hours}:00`;
|
|
7710
|
+
const label = `UTC${sign}${hours}:00`;
|
|
7711
|
+
options.push({ value, label });
|
|
7712
|
+
}
|
|
7713
|
+
return options;
|
|
7714
|
+
}, []);
|
|
7715
|
+
// Create collection for Select
|
|
7716
|
+
const { collection: timezoneCollection } = react.useListCollection({
|
|
7717
|
+
initialItems: timezoneOffsetOptions,
|
|
7718
|
+
itemToString: (item) => item.label,
|
|
7719
|
+
itemToValue: (item) => item.value,
|
|
7720
|
+
});
|
|
7721
|
+
// Ensure timezoneOffset value is valid (exists in collection)
|
|
7722
|
+
const validTimezoneOffset = React.useMemo(() => {
|
|
7723
|
+
if (!timezoneOffset)
|
|
7724
|
+
return undefined;
|
|
7725
|
+
const exists = timezoneOffsetOptions.some((opt) => opt.value === timezoneOffset);
|
|
7726
|
+
return exists ? timezoneOffset : undefined;
|
|
7727
|
+
}, [timezoneOffset, timezoneOffsetOptions]);
|
|
7728
|
+
// Date input state
|
|
7729
|
+
const [dateInputValue, setDateInputValue] = React.useState('');
|
|
7730
|
+
// Sync date input value with selected date
|
|
7731
|
+
React.useEffect(() => {
|
|
7732
|
+
if (selectedDate) {
|
|
7733
|
+
const formatted = dayjs(selectedDate).tz(tz).format('YYYY-MM-DD');
|
|
7734
|
+
setDateInputValue(formatted);
|
|
6805
7735
|
}
|
|
6806
7736
|
else {
|
|
6807
|
-
|
|
7737
|
+
setDateInputValue('');
|
|
6808
7738
|
}
|
|
6809
|
-
}, [
|
|
6810
|
-
//
|
|
6811
|
-
const
|
|
6812
|
-
? typeof value === 'string'
|
|
6813
|
-
? dayjs(value).tz(timezone).isValid()
|
|
6814
|
-
? dayjs(value).tz(timezone).toDate()
|
|
6815
|
-
: new Date()
|
|
6816
|
-
: value
|
|
6817
|
-
: new Date();
|
|
6818
|
-
// Shared function to parse and validate input value
|
|
6819
|
-
const parseAndValidateInput = (inputVal) => {
|
|
7739
|
+
}, [selectedDate, tz]);
|
|
7740
|
+
// Parse and validate date input
|
|
7741
|
+
const parseAndValidateDateInput = (inputVal) => {
|
|
6820
7742
|
// If empty, clear the value
|
|
6821
7743
|
if (!inputVal.trim()) {
|
|
6822
|
-
|
|
6823
|
-
|
|
7744
|
+
setSelectedDate(null);
|
|
7745
|
+
updateDateTime(null, hour, minute, second, meridiem);
|
|
6824
7746
|
return;
|
|
6825
7747
|
}
|
|
6826
|
-
// Try parsing with
|
|
6827
|
-
let parsedDate = dayjs(inputVal,
|
|
6828
|
-
// If that fails, try common
|
|
7748
|
+
// Try parsing with common date formats
|
|
7749
|
+
let parsedDate = dayjs(inputVal, 'YYYY-MM-DD', true);
|
|
7750
|
+
// If that fails, try other common formats
|
|
6829
7751
|
if (!parsedDate.isValid()) {
|
|
6830
7752
|
parsedDate = dayjs(inputVal);
|
|
6831
7753
|
}
|
|
6832
|
-
// If still invalid, try parsing with dateFormat
|
|
6833
|
-
if (!parsedDate.isValid()) {
|
|
6834
|
-
parsedDate = dayjs(inputVal, dateFormat, true);
|
|
6835
|
-
}
|
|
6836
7754
|
// If valid, check constraints and update
|
|
6837
7755
|
if (parsedDate.isValid()) {
|
|
6838
|
-
const dateObj = parsedDate.tz(
|
|
7756
|
+
const dateObj = parsedDate.tz(tz).toDate();
|
|
6839
7757
|
// Check min/max constraints
|
|
6840
7758
|
if (minDate && dateObj < minDate) {
|
|
6841
|
-
// Invalid: before minDate, reset to
|
|
6842
|
-
|
|
7759
|
+
// Invalid: before minDate, reset to current selected date
|
|
7760
|
+
if (selectedDate) {
|
|
7761
|
+
const formatted = dayjs(selectedDate).tz(tz).format('YYYY-MM-DD');
|
|
7762
|
+
setDateInputValue(formatted);
|
|
7763
|
+
}
|
|
7764
|
+
else {
|
|
7765
|
+
setDateInputValue('');
|
|
7766
|
+
}
|
|
6843
7767
|
return;
|
|
6844
7768
|
}
|
|
6845
7769
|
if (maxDate && dateObj > maxDate) {
|
|
6846
|
-
// Invalid: after maxDate, reset to
|
|
6847
|
-
|
|
7770
|
+
// Invalid: after maxDate, reset to current selected date
|
|
7771
|
+
if (selectedDate) {
|
|
7772
|
+
const formatted = dayjs(selectedDate).tz(tz).format('YYYY-MM-DD');
|
|
7773
|
+
setDateInputValue(formatted);
|
|
7774
|
+
}
|
|
7775
|
+
else {
|
|
7776
|
+
setDateInputValue('');
|
|
7777
|
+
}
|
|
6848
7778
|
return;
|
|
6849
7779
|
}
|
|
6850
|
-
// Valid date -
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
|
|
6854
|
-
|
|
7780
|
+
// Valid date - update selected date
|
|
7781
|
+
setSelectedDate(dateObj);
|
|
7782
|
+
updateDateTime(dateObj, hour, minute, second, meridiem);
|
|
7783
|
+
// Format and update input value
|
|
7784
|
+
const formatted = parsedDate.tz(tz).format('YYYY-MM-DD');
|
|
7785
|
+
setDateInputValue(formatted);
|
|
6855
7786
|
}
|
|
6856
7787
|
else {
|
|
6857
|
-
// Invalid date - reset to
|
|
6858
|
-
|
|
7788
|
+
// Invalid date - reset to current selected date
|
|
7789
|
+
if (selectedDate) {
|
|
7790
|
+
const formatted = dayjs(selectedDate).tz(tz).format('YYYY-MM-DD');
|
|
7791
|
+
setDateInputValue(formatted);
|
|
7792
|
+
}
|
|
7793
|
+
else {
|
|
7794
|
+
setDateInputValue('');
|
|
7795
|
+
}
|
|
6859
7796
|
}
|
|
6860
7797
|
};
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
if (value) {
|
|
6864
|
-
const formatted = typeof value === 'string'
|
|
6865
|
-
? dayjs(value).tz(timezone).isValid()
|
|
6866
|
-
? dayjs(value).tz(timezone).format(displayFormat)
|
|
6867
|
-
: ''
|
|
6868
|
-
: dayjs(value).tz(timezone).format(displayFormat);
|
|
6869
|
-
setInputValue(formatted);
|
|
6870
|
-
}
|
|
6871
|
-
else {
|
|
6872
|
-
setInputValue('');
|
|
6873
|
-
}
|
|
7798
|
+
const handleDateInputChange = (e) => {
|
|
7799
|
+
setDateInputValue(e.target.value);
|
|
6874
7800
|
};
|
|
6875
|
-
const
|
|
6876
|
-
|
|
6877
|
-
setInputValue(e.target.value);
|
|
7801
|
+
const handleDateInputBlur = () => {
|
|
7802
|
+
parseAndValidateDateInput(dateInputValue);
|
|
6878
7803
|
};
|
|
6879
|
-
const
|
|
6880
|
-
// Parse and validate when input loses focus
|
|
6881
|
-
parseAndValidateInput(inputValue);
|
|
6882
|
-
};
|
|
6883
|
-
const handleKeyDown = (e) => {
|
|
6884
|
-
// Parse and validate when Enter is pressed
|
|
7804
|
+
const handleDateInputKeyDown = (e) => {
|
|
6885
7805
|
if (e.key === 'Enter') {
|
|
6886
7806
|
e.preventDefault();
|
|
6887
|
-
|
|
7807
|
+
parseAndValidateDateInput(dateInputValue);
|
|
6888
7808
|
}
|
|
6889
7809
|
};
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
dayjs.
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
6903
|
-
onChange = (_newValue) => { }, startTime, selectedDate, timezone = 'Asia/Hong_Kong', portalled = true, labels = {
|
|
6904
|
-
placeholder: 'HH:mm:ss',
|
|
6905
|
-
emptyMessage: 'No time found',
|
|
6906
|
-
}, }) {
|
|
6907
|
-
// Generate time options (every 15 minutes, seconds always 0)
|
|
6908
|
-
const timeOptions = React.useMemo(() => {
|
|
6909
|
-
const options = [];
|
|
6910
|
-
// Get start time for comparison if provided
|
|
6911
|
-
let startDateTime = null;
|
|
6912
|
-
let shouldFilterByDate = false;
|
|
6913
|
-
if (startTime && selectedDate) {
|
|
6914
|
-
const startDateObj = dayjs(startTime).tz(timezone);
|
|
6915
|
-
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6916
|
-
if (startDateObj.isValid() && selectedDateObj.isValid()) {
|
|
6917
|
-
startDateTime = startDateObj;
|
|
6918
|
-
// Only filter if dates are the same
|
|
6919
|
-
shouldFilterByDate =
|
|
6920
|
-
startDateObj.format('YYYY-MM-DD') ===
|
|
6921
|
-
selectedDateObj.format('YYYY-MM-DD');
|
|
6922
|
-
}
|
|
6923
|
-
}
|
|
6924
|
-
for (let h = 0; h < 24; h++) {
|
|
6925
|
-
for (let m = 0; m < 60; m += 15) {
|
|
6926
|
-
const timeDisplay = `${h.toString().padStart(2, '0')}:${m
|
|
6927
|
-
.toString()
|
|
6928
|
-
.padStart(2, '0')}:00`;
|
|
6929
|
-
// Filter out times that would result in negative duration (only when dates are the same)
|
|
6930
|
-
if (startDateTime && selectedDate && shouldFilterByDate) {
|
|
6931
|
-
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6932
|
-
const optionDateTime = selectedDateObj
|
|
6933
|
-
.hour(h)
|
|
6934
|
-
.minute(m)
|
|
6935
|
-
.second(0)
|
|
6936
|
-
.millisecond(0);
|
|
6937
|
-
if (optionDateTime.isBefore(startDateTime)) {
|
|
6938
|
-
continue; // Skip this option as it would result in negative duration
|
|
6939
|
-
}
|
|
6940
|
-
}
|
|
6941
|
-
// Calculate duration if startTime is provided
|
|
6942
|
-
let durationText;
|
|
6943
|
-
if (startDateTime && selectedDate) {
|
|
6944
|
-
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6945
|
-
const optionDateTime = selectedDateObj
|
|
6946
|
-
.hour(h)
|
|
6947
|
-
.minute(m)
|
|
6948
|
-
.second(0)
|
|
6949
|
-
.millisecond(0);
|
|
6950
|
-
if (optionDateTime.isValid() &&
|
|
6951
|
-
optionDateTime.isAfter(startDateTime)) {
|
|
6952
|
-
const diffMs = optionDateTime.diff(startDateTime);
|
|
6953
|
-
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
6954
|
-
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
6955
|
-
const diffSeconds = Math.floor((diffMs % (1000 * 60)) / 1000);
|
|
6956
|
-
if (diffHours > 0 || diffMinutes > 0 || diffSeconds > 0) {
|
|
6957
|
-
let diffText = '';
|
|
6958
|
-
if (diffHours > 0) {
|
|
6959
|
-
diffText = `${diffHours}h ${diffMinutes}m`;
|
|
6960
|
-
}
|
|
6961
|
-
else if (diffMinutes > 0) {
|
|
6962
|
-
diffText = `${diffMinutes}m ${diffSeconds}s`;
|
|
6963
|
-
}
|
|
6964
|
-
else {
|
|
6965
|
-
diffText = `${diffSeconds}s`;
|
|
6966
|
-
}
|
|
6967
|
-
durationText = `+${diffText}`;
|
|
6968
|
-
}
|
|
6969
|
-
}
|
|
6970
|
-
}
|
|
6971
|
-
options.push({
|
|
6972
|
-
label: timeDisplay,
|
|
6973
|
-
value: `${h}:${m}:0`,
|
|
6974
|
-
hour: h,
|
|
6975
|
-
minute: m,
|
|
6976
|
-
second: 0,
|
|
6977
|
-
searchText: timeDisplay, // Use base time without duration for searching
|
|
6978
|
-
durationText,
|
|
6979
|
-
});
|
|
6980
|
-
}
|
|
6981
|
-
}
|
|
6982
|
-
return options;
|
|
6983
|
-
}, [startTime, selectedDate, timezone]);
|
|
6984
|
-
const { contains } = react.useFilter({ sensitivity: 'base' });
|
|
6985
|
-
const { collection, filter } = react.useListCollection({
|
|
6986
|
-
initialItems: timeOptions,
|
|
6987
|
-
itemToString: (item) => item.searchText, // Use searchText (without duration) for filtering
|
|
6988
|
-
itemToValue: (item) => item.value,
|
|
6989
|
-
filter: contains,
|
|
6990
|
-
});
|
|
6991
|
-
// Get current value string for combobox
|
|
6992
|
-
const currentValue = React.useMemo(() => {
|
|
6993
|
-
if (hour === null || minute === null || second === null) {
|
|
6994
|
-
return '';
|
|
6995
|
-
}
|
|
6996
|
-
return `${hour}:${minute}:${second}`;
|
|
6997
|
-
}, [hour, minute, second]);
|
|
6998
|
-
// Calculate duration difference
|
|
6999
|
-
const durationDiff = React.useMemo(() => {
|
|
7000
|
-
if (!startTime ||
|
|
7001
|
-
!selectedDate ||
|
|
7002
|
-
hour === null ||
|
|
7003
|
-
minute === null ||
|
|
7004
|
-
second === null) {
|
|
7005
|
-
return null;
|
|
7006
|
-
}
|
|
7007
|
-
const startDateObj = dayjs(startTime).tz(timezone);
|
|
7008
|
-
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
7009
|
-
const currentDateTime = selectedDateObj
|
|
7010
|
-
.hour(hour)
|
|
7011
|
-
.minute(minute)
|
|
7012
|
-
.second(second ?? 0)
|
|
7013
|
-
.millisecond(0);
|
|
7014
|
-
if (!startDateObj.isValid() || !currentDateTime.isValid()) {
|
|
7015
|
-
return null;
|
|
7016
|
-
}
|
|
7017
|
-
const diffMs = currentDateTime.diff(startDateObj);
|
|
7018
|
-
if (diffMs < 0) {
|
|
7019
|
-
return null;
|
|
7810
|
+
// Helper functions to get dates in the correct timezone
|
|
7811
|
+
const getToday = () => dayjs().tz(tz).startOf('day').toDate();
|
|
7812
|
+
const getYesterday = () => dayjs().tz(tz).subtract(1, 'day').startOf('day').toDate();
|
|
7813
|
+
const getTomorrow = () => dayjs().tz(tz).add(1, 'day').startOf('day').toDate();
|
|
7814
|
+
const getPlus7Days = () => dayjs().tz(tz).add(7, 'day').startOf('day').toDate();
|
|
7815
|
+
// Check if a date is within min/max constraints
|
|
7816
|
+
const isDateValid = (date) => {
|
|
7817
|
+
if (minDate) {
|
|
7818
|
+
const minDateStart = dayjs(minDate).tz(tz).startOf('day').toDate();
|
|
7819
|
+
const dateStart = dayjs(date).tz(tz).startOf('day').toDate();
|
|
7820
|
+
if (dateStart < minDateStart)
|
|
7821
|
+
return false;
|
|
7020
7822
|
}
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
if (diffHours > 0) {
|
|
7027
|
-
diffText = `${diffHours}h ${diffMinutes}m`;
|
|
7028
|
-
}
|
|
7029
|
-
else if (diffMinutes > 0) {
|
|
7030
|
-
diffText = `${diffMinutes}m ${diffSeconds}s`;
|
|
7031
|
-
}
|
|
7032
|
-
else {
|
|
7033
|
-
diffText = `${diffSeconds}s`;
|
|
7034
|
-
}
|
|
7035
|
-
return `+${diffText}`;
|
|
7823
|
+
if (maxDate) {
|
|
7824
|
+
const maxDateStart = dayjs(maxDate).tz(tz).startOf('day').toDate();
|
|
7825
|
+
const dateStart = dayjs(date).tz(tz).startOf('day').toDate();
|
|
7826
|
+
if (dateStart > maxDateStart)
|
|
7827
|
+
return false;
|
|
7036
7828
|
}
|
|
7037
|
-
return
|
|
7038
|
-
}, [hour, minute, second, startTime, selectedDate, timezone]);
|
|
7039
|
-
const handleClear = () => {
|
|
7040
|
-
setHour(null);
|
|
7041
|
-
setMinute(null);
|
|
7042
|
-
setSecond(null);
|
|
7043
|
-
filter(''); // Reset filter to show all options
|
|
7044
|
-
onChange({ hour: null, minute: null, second: null });
|
|
7829
|
+
return true;
|
|
7045
7830
|
};
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
if (selectedOption) {
|
|
7054
|
-
setHour(selectedOption.hour);
|
|
7055
|
-
setMinute(selectedOption.minute);
|
|
7056
|
-
setSecond(selectedOption.second);
|
|
7057
|
-
filter(''); // Reset filter after selection
|
|
7058
|
-
onChange({
|
|
7059
|
-
hour: selectedOption.hour,
|
|
7060
|
-
minute: selectedOption.minute,
|
|
7061
|
-
second: selectedOption.second,
|
|
7062
|
-
});
|
|
7831
|
+
// Handle quick action button clicks
|
|
7832
|
+
const handleQuickActionClick = (date) => {
|
|
7833
|
+
if (isDateValid(date)) {
|
|
7834
|
+
setSelectedDate(date);
|
|
7835
|
+
updateDateTime(date, hour, minute, second, meridiem);
|
|
7836
|
+
// Close the calendar popover if open
|
|
7837
|
+
setCalendarPopoverOpen(false);
|
|
7063
7838
|
}
|
|
7064
7839
|
};
|
|
7065
|
-
//
|
|
7066
|
-
const
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
const
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
// Validate ranges
|
|
7081
|
-
if (parsedHour >= 0 &&
|
|
7082
|
-
parsedHour <= 23 &&
|
|
7083
|
-
parsedMinute >= 0 &&
|
|
7084
|
-
parsedMinute <= 59 &&
|
|
7085
|
-
parsedSecond >= 0 &&
|
|
7086
|
-
parsedSecond <= 59) {
|
|
7087
|
-
setHour(parsedHour);
|
|
7088
|
-
setMinute(parsedMinute);
|
|
7089
|
-
setSecond(parsedSecond);
|
|
7090
|
-
onChange({
|
|
7091
|
-
hour: parsedHour,
|
|
7092
|
-
minute: parsedMinute,
|
|
7093
|
-
second: parsedSecond,
|
|
7094
|
-
});
|
|
7095
|
-
return;
|
|
7840
|
+
// Display text for buttons
|
|
7841
|
+
const dateDisplayText = React.useMemo(() => {
|
|
7842
|
+
if (!selectedDate)
|
|
7843
|
+
return 'Select date';
|
|
7844
|
+
return dayjs(selectedDate).tz(tz).format('YYYY-MM-DD');
|
|
7845
|
+
}, [selectedDate, tz]);
|
|
7846
|
+
const timeDisplayText = React.useMemo(() => {
|
|
7847
|
+
if (hour === null || minute === null)
|
|
7848
|
+
return 'Select time';
|
|
7849
|
+
if (is24Hour) {
|
|
7850
|
+
// 24-hour format: never show meridiem, always use 24-hour format (0-23)
|
|
7851
|
+
const hour24 = hour >= 0 && hour <= 23 ? hour : hour % 24;
|
|
7852
|
+
const s = second ?? 0;
|
|
7853
|
+
if (showSeconds) {
|
|
7854
|
+
return `${hour24.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
7096
7855
|
}
|
|
7856
|
+
return `${hour24.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
|
|
7097
7857
|
}
|
|
7098
7858
|
else {
|
|
7099
|
-
//
|
|
7100
|
-
const
|
|
7101
|
-
if (
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
parsedSecond >= 0 &&
|
|
7111
|
-
parsedSecond <= 59) {
|
|
7112
|
-
setHour(parsedHour);
|
|
7113
|
-
setMinute(parsedMinute);
|
|
7114
|
-
setSecond(parsedSecond);
|
|
7115
|
-
onChange({
|
|
7116
|
-
hour: parsedHour,
|
|
7117
|
-
minute: parsedMinute,
|
|
7118
|
-
second: parsedSecond,
|
|
7119
|
-
});
|
|
7120
|
-
return;
|
|
7121
|
-
}
|
|
7122
|
-
}
|
|
7123
|
-
}
|
|
7124
|
-
// Parse failed, select first result
|
|
7125
|
-
selectFirstResult();
|
|
7126
|
-
};
|
|
7127
|
-
// Select first result from filtered collection
|
|
7128
|
-
const selectFirstResult = () => {
|
|
7129
|
-
if (collection.items.length > 0) {
|
|
7130
|
-
const firstItem = collection.items[0];
|
|
7131
|
-
setHour(firstItem.hour);
|
|
7132
|
-
setMinute(firstItem.minute);
|
|
7133
|
-
setSecond(firstItem.second);
|
|
7134
|
-
filter(''); // Reset filter after selection
|
|
7135
|
-
onChange({
|
|
7136
|
-
hour: firstItem.hour,
|
|
7137
|
-
minute: firstItem.minute,
|
|
7138
|
-
second: firstItem.second,
|
|
7139
|
-
});
|
|
7140
|
-
}
|
|
7141
|
-
};
|
|
7142
|
-
const handleInputValueChange = (details) => {
|
|
7143
|
-
// Filter the collection based on input, but don't parse yet
|
|
7144
|
-
filter(details.inputValue);
|
|
7145
|
-
};
|
|
7146
|
-
const handleFocus = (e) => {
|
|
7147
|
-
// Select all text when focusing
|
|
7148
|
-
e.target.select();
|
|
7149
|
-
};
|
|
7150
|
-
const handleBlur = (e) => {
|
|
7151
|
-
// Parse and commit the input value when losing focus
|
|
7152
|
-
const inputValue = e.target.value;
|
|
7153
|
-
if (inputValue) {
|
|
7154
|
-
parseAndCommitInput(inputValue);
|
|
7155
|
-
}
|
|
7156
|
-
};
|
|
7157
|
-
const handleKeyDown = (e) => {
|
|
7158
|
-
// Commit input on Enter key
|
|
7159
|
-
if (e.key === 'Enter') {
|
|
7160
|
-
e.preventDefault();
|
|
7161
|
-
const inputValue = e.currentTarget.value;
|
|
7162
|
-
if (inputValue) {
|
|
7163
|
-
parseAndCommitInput(inputValue);
|
|
7164
|
-
}
|
|
7165
|
-
// Blur the input
|
|
7166
|
-
e.currentTarget?.blur();
|
|
7167
|
-
}
|
|
7168
|
-
};
|
|
7169
|
-
return (jsxRuntime.jsx(react.Flex, { direction: "column", gap: 3, children: jsxRuntime.jsxs(react.Flex, { alignItems: "center", gap: "2", width: "auto", minWidth: "300px", children: [jsxRuntime.jsxs(react.Combobox.Root, { collection: collection, value: currentValue ? [currentValue] : [], onValueChange: handleValueChange, onInputValueChange: handleInputValueChange, allowCustomValue: true, selectionBehavior: "replace", openOnClick: true, flex: 1, children: [jsxRuntime.jsxs(react.Combobox.Control, { children: [jsxRuntime.jsx(react.InputGroup, { startElement: jsxRuntime.jsx(bs.BsClock, {}), children: jsxRuntime.jsx(react.Combobox.Input, { placeholder: labels.placeholder, onFocus: handleFocus, onBlur: handleBlur, onKeyDown: handleKeyDown }) }), jsxRuntime.jsx(react.Combobox.IndicatorGroup, { children: jsxRuntime.jsx(react.Combobox.Trigger, {}) })] }), jsxRuntime.jsx(react.Portal, { disabled: !portalled, children: jsxRuntime.jsx(react.Combobox.Positioner, { children: jsxRuntime.jsxs(react.Combobox.Content, { children: [jsxRuntime.jsx(react.Combobox.Empty, { children: labels.emptyMessage }), collection.items.map((item) => (jsxRuntime.jsxs(react.Combobox.Item, { item: item, children: [jsxRuntime.jsxs(react.Flex, { alignItems: "center", gap: 2, width: "100%", children: [jsxRuntime.jsx(react.Text, { flex: 1, children: item.label }), item.durationText && (jsxRuntime.jsx(react.Tag.Root, { size: "sm", children: jsxRuntime.jsx(react.Tag.Label, { children: item.durationText }) }))] }), jsxRuntime.jsx(react.Combobox.ItemIndicator, {})] }, item.value)))] }) }) })] }), durationDiff && (jsxRuntime.jsx(react.Tag.Root, { size: "sm", children: jsxRuntime.jsx(react.Tag.Label, { children: durationDiff }) })), jsxRuntime.jsx(react.Button, { onClick: handleClear, size: "sm", variant: "ghost", children: jsxRuntime.jsx(react.Icon, { children: jsxRuntime.jsx(md.MdCancel, {}) }) })] }) }));
|
|
7170
|
-
}
|
|
7171
|
-
|
|
7172
|
-
dayjs.extend(utc);
|
|
7173
|
-
dayjs.extend(timezone);
|
|
7174
|
-
function DateTimePicker$1({ value, onChange, format = 'date-time', showSeconds = false, labels = {
|
|
7175
|
-
monthNamesShort: [
|
|
7176
|
-
'Jan',
|
|
7177
|
-
'Feb',
|
|
7178
|
-
'Mar',
|
|
7179
|
-
'Apr',
|
|
7180
|
-
'May',
|
|
7181
|
-
'Jun',
|
|
7182
|
-
'Jul',
|
|
7183
|
-
'Aug',
|
|
7184
|
-
'Sep',
|
|
7185
|
-
'Oct',
|
|
7186
|
-
'Nov',
|
|
7187
|
-
'Dec',
|
|
7188
|
-
],
|
|
7189
|
-
weekdayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
|
7190
|
-
backButtonLabel: 'Back',
|
|
7191
|
-
forwardButtonLabel: 'Next',
|
|
7192
|
-
}, timePickerLabels, timezone = 'Asia/Hong_Kong', startTime, minDate, maxDate, portalled = false, }) {
|
|
7193
|
-
console.log('[DateTimePicker] Component initialized with props:', {
|
|
7194
|
-
value,
|
|
7195
|
-
format,
|
|
7196
|
-
showSeconds,
|
|
7197
|
-
timezone,
|
|
7198
|
-
startTime,
|
|
7199
|
-
minDate,
|
|
7200
|
-
maxDate,
|
|
7201
|
-
});
|
|
7202
|
-
// Initialize selectedDate from value prop, converting ISO to YYYY-MM-DD format
|
|
7203
|
-
const getDateString = React.useCallback((val) => {
|
|
7204
|
-
if (!val)
|
|
7859
|
+
// 12-hour format: always show meridiem (AM/PM)
|
|
7860
|
+
const hour12 = hour >= 1 && hour <= 12 ? hour : hour % 12;
|
|
7861
|
+
if (meridiem === null)
|
|
7862
|
+
return 'Select time';
|
|
7863
|
+
const hourDisplay = hour12.toString();
|
|
7864
|
+
const minuteDisplay = minute.toString().padStart(2, '0');
|
|
7865
|
+
return `${hourDisplay}:${minuteDisplay} ${meridiem.toUpperCase()}`;
|
|
7866
|
+
}
|
|
7867
|
+
}, [hour, minute, second, meridiem, is24Hour, showSeconds]);
|
|
7868
|
+
const timezoneDisplayText = React.useMemo(() => {
|
|
7869
|
+
if (!showTimezoneSelector)
|
|
7205
7870
|
return '';
|
|
7206
|
-
|
|
7207
|
-
return
|
|
7208
|
-
}, [
|
|
7209
|
-
|
|
7210
|
-
// Helper to get time values from value prop with timezone
|
|
7211
|
-
const getTimeFromValue = React.useCallback((val) => {
|
|
7212
|
-
console.log('[DateTimePicker] getTimeFromValue called:', {
|
|
7213
|
-
val,
|
|
7214
|
-
timezone,
|
|
7215
|
-
showSeconds,
|
|
7216
|
-
});
|
|
7217
|
-
if (!val) {
|
|
7218
|
-
console.log('[DateTimePicker] No value provided, returning nulls');
|
|
7219
|
-
return {
|
|
7220
|
-
hour12: null,
|
|
7221
|
-
minute: null,
|
|
7222
|
-
meridiem: null,
|
|
7223
|
-
hour24: null,
|
|
7224
|
-
second: null,
|
|
7225
|
-
};
|
|
7226
|
-
}
|
|
7227
|
-
const dateObj = dayjs(val).tz(timezone);
|
|
7228
|
-
console.log('[DateTimePicker] Parsed date object:', {
|
|
7229
|
-
original: val,
|
|
7230
|
-
timezone,
|
|
7231
|
-
isValid: dateObj.isValid(),
|
|
7232
|
-
formatted: dateObj.format('YYYY-MM-DD HH:mm:ss Z'),
|
|
7233
|
-
hour24: dateObj.hour(),
|
|
7234
|
-
minute: dateObj.minute(),
|
|
7235
|
-
second: dateObj.second(),
|
|
7236
|
-
});
|
|
7237
|
-
if (!dateObj.isValid()) {
|
|
7238
|
-
console.log('[DateTimePicker] Invalid date object, returning nulls');
|
|
7239
|
-
return {
|
|
7240
|
-
hour12: null,
|
|
7241
|
-
minute: null,
|
|
7242
|
-
meridiem: null,
|
|
7243
|
-
hour24: null,
|
|
7244
|
-
second: null,
|
|
7245
|
-
};
|
|
7246
|
-
}
|
|
7247
|
-
const hour24Value = dateObj.hour();
|
|
7248
|
-
const hour12Value = hour24Value % 12 || 12;
|
|
7249
|
-
const minuteValue = dateObj.minute();
|
|
7250
|
-
const meridiemValue = hour24Value >= 12 ? 'pm' : 'am';
|
|
7251
|
-
const secondValue = showSeconds ? dateObj.second() : null;
|
|
7252
|
-
const result = {
|
|
7253
|
-
hour12: hour12Value,
|
|
7254
|
-
minute: minuteValue,
|
|
7255
|
-
meridiem: meridiemValue,
|
|
7256
|
-
hour24: hour24Value,
|
|
7257
|
-
second: secondValue,
|
|
7258
|
-
};
|
|
7259
|
-
console.log('[DateTimePicker] Extracted time values:', result);
|
|
7260
|
-
return result;
|
|
7261
|
-
}, [timezone, showSeconds]);
|
|
7262
|
-
const initialTime = getTimeFromValue(value);
|
|
7263
|
-
console.log('[DateTimePicker] Initial time from value:', {
|
|
7264
|
-
value,
|
|
7265
|
-
initialTime,
|
|
7266
|
-
});
|
|
7267
|
-
// Time state for 12-hour format
|
|
7268
|
-
const [hour12, setHour12] = React.useState(initialTime.hour12);
|
|
7269
|
-
const [minute, setMinute] = React.useState(initialTime.minute);
|
|
7270
|
-
const [meridiem, setMeridiem] = React.useState(initialTime.meridiem);
|
|
7271
|
-
// Time state for 24-hour format
|
|
7272
|
-
const [hour24, setHour24] = React.useState(initialTime.hour24);
|
|
7273
|
-
const [second, setSecond] = React.useState(initialTime.second);
|
|
7274
|
-
// Sync selectedDate and time states when value prop changes
|
|
7871
|
+
// Show offset as is (e.g., "+08:00")
|
|
7872
|
+
return timezoneOffset;
|
|
7873
|
+
}, [timezoneOffset, showTimezoneSelector]);
|
|
7874
|
+
// Update selectedDate when value changes externally
|
|
7275
7875
|
React.useEffect(() => {
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
setHour12(null);
|
|
7286
|
-
setMinute(null);
|
|
7287
|
-
setMeridiem(null);
|
|
7288
|
-
setHour24(null);
|
|
7289
|
-
setSecond(null);
|
|
7290
|
-
return;
|
|
7291
|
-
}
|
|
7292
|
-
// Check if value is valid
|
|
7293
|
-
const dateObj = dayjs(value).tz(timezone);
|
|
7294
|
-
if (!dateObj.isValid()) {
|
|
7295
|
-
console.log('[DateTimePicker] Invalid value, clearing all fields');
|
|
7296
|
-
setSelectedDate('');
|
|
7297
|
-
setHour12(null);
|
|
7298
|
-
setMinute(null);
|
|
7299
|
-
setMeridiem(null);
|
|
7300
|
-
setHour24(null);
|
|
7301
|
-
setSecond(null);
|
|
7302
|
-
return;
|
|
7303
|
-
}
|
|
7304
|
-
const dateString = getDateString(value);
|
|
7305
|
-
console.log('[DateTimePicker] Setting selectedDate:', dateString);
|
|
7306
|
-
setSelectedDate(dateString);
|
|
7307
|
-
const timeData = getTimeFromValue(value);
|
|
7308
|
-
console.log('[DateTimePicker] Updating time states:', {
|
|
7309
|
-
timeData,
|
|
7310
|
-
});
|
|
7311
|
-
setHour12(timeData.hour12);
|
|
7312
|
-
setMinute(timeData.minute);
|
|
7313
|
-
setMeridiem(timeData.meridiem);
|
|
7314
|
-
setHour24(timeData.hour24);
|
|
7315
|
-
setSecond(timeData.second);
|
|
7316
|
-
}, [value, getTimeFromValue, getDateString, timezone]);
|
|
7317
|
-
const handleDateChange = (date) => {
|
|
7318
|
-
console.log('[DateTimePicker] handleDateChange called:', {
|
|
7319
|
-
date,
|
|
7320
|
-
timezone,
|
|
7321
|
-
showSeconds,
|
|
7322
|
-
currentTimeStates: { hour12, minute, meridiem, hour24, second },
|
|
7323
|
-
});
|
|
7324
|
-
// If date is empty or invalid, clear all fields
|
|
7325
|
-
if (!date || date === '') {
|
|
7326
|
-
console.log('[DateTimePicker] Empty date, clearing all fields');
|
|
7327
|
-
setSelectedDate('');
|
|
7328
|
-
setHour12(null);
|
|
7329
|
-
setMinute(null);
|
|
7330
|
-
setMeridiem(null);
|
|
7331
|
-
setHour24(null);
|
|
7332
|
-
setSecond(null);
|
|
7333
|
-
onChange?.(undefined);
|
|
7334
|
-
return;
|
|
7876
|
+
if (parsedValue) {
|
|
7877
|
+
setSelectedDate(parsedValue.toDate());
|
|
7878
|
+
setHour(parsedValue.hour());
|
|
7879
|
+
setMinute(parsedValue.minute());
|
|
7880
|
+
setSecond(parsedValue.second());
|
|
7881
|
+
if (!is24Hour) {
|
|
7882
|
+
const h = parsedValue.hour();
|
|
7883
|
+
setMeridiem(h < 12 ? 'am' : 'pm');
|
|
7884
|
+
}
|
|
7335
7885
|
}
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
date,
|
|
7341
|
-
timezone,
|
|
7342
|
-
isValid: dateObj.isValid(),
|
|
7343
|
-
isoString: dateObj.toISOString(),
|
|
7344
|
-
formatted: dateObj.format('YYYY-MM-DD HH:mm:ss Z'),
|
|
7345
|
-
});
|
|
7346
|
-
if (!dateObj.isValid()) {
|
|
7347
|
-
console.warn('[DateTimePicker] Invalid date object in handleDateChange, clearing fields');
|
|
7348
|
-
setSelectedDate('');
|
|
7349
|
-
setHour12(null);
|
|
7350
|
-
setMinute(null);
|
|
7351
|
-
setMeridiem(null);
|
|
7352
|
-
setHour24(null);
|
|
7353
|
-
setSecond(null);
|
|
7886
|
+
}, [parsedValue, is24Hour]);
|
|
7887
|
+
// Combine date and time and call onChange
|
|
7888
|
+
const updateDateTime = (newDate, newHour, newMinute, newSecond, newMeridiem, timezoneOffsetOverride) => {
|
|
7889
|
+
if (!newDate || newHour === null || newMinute === null) {
|
|
7354
7890
|
onChange?.(undefined);
|
|
7355
7891
|
return;
|
|
7356
7892
|
}
|
|
7357
|
-
//
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
format
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
setMinute(data.minute);
|
|
7381
|
-
if (showSeconds) {
|
|
7382
|
-
setSecond(data.second ?? null);
|
|
7893
|
+
// Convert 12-hour to 24-hour if needed
|
|
7894
|
+
let hour24 = newHour;
|
|
7895
|
+
if (!is24Hour && newMeridiem) {
|
|
7896
|
+
// In 12-hour format, hour should be 1-12
|
|
7897
|
+
// If hour is > 12, it might already be in 24-hour format, convert it first
|
|
7898
|
+
let hour12 = newHour;
|
|
7899
|
+
if (newHour > 12) {
|
|
7900
|
+
// Hour is in 24-hour format, convert to 12-hour first
|
|
7901
|
+
if (newHour === 12) {
|
|
7902
|
+
hour12 = 12;
|
|
7903
|
+
}
|
|
7904
|
+
else {
|
|
7905
|
+
hour12 = newHour - 12;
|
|
7906
|
+
}
|
|
7907
|
+
}
|
|
7908
|
+
// Now convert 12-hour to 24-hour format (0-23)
|
|
7909
|
+
if (newMeridiem === 'am') {
|
|
7910
|
+
if (hour12 === 12) {
|
|
7911
|
+
hour24 = 0; // 12 AM = 0:00
|
|
7912
|
+
}
|
|
7913
|
+
else {
|
|
7914
|
+
hour24 = hour12; // 1-11 AM = 1-11
|
|
7915
|
+
}
|
|
7383
7916
|
}
|
|
7384
7917
|
else {
|
|
7385
|
-
//
|
|
7386
|
-
|
|
7918
|
+
// PM
|
|
7919
|
+
if (hour12 === 12) {
|
|
7920
|
+
hour24 = 12; // 12 PM = 12:00
|
|
7921
|
+
}
|
|
7922
|
+
else {
|
|
7923
|
+
hour24 = hour12 + 12; // 1-11 PM = 13-23
|
|
7924
|
+
}
|
|
7387
7925
|
}
|
|
7388
7926
|
}
|
|
7389
|
-
else {
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
//
|
|
7397
|
-
if (
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7927
|
+
else if (!is24Hour && !newMeridiem) {
|
|
7928
|
+
// If in 12-hour mode but no meridiem, assume the hour is already in 12-hour format
|
|
7929
|
+
// and default to AM (or keep as is if it's a valid 12-hour value)
|
|
7930
|
+
// This shouldn't happen in normal flow, but handle it gracefully
|
|
7931
|
+
hour24 = newHour;
|
|
7932
|
+
}
|
|
7933
|
+
// If timezone selector is enabled, create date-time without timezone conversion
|
|
7934
|
+
// to ensure the selected timestamp matches the picker values exactly
|
|
7935
|
+
if (showTimezoneSelector) {
|
|
7936
|
+
// Use override if provided, otherwise use state value
|
|
7937
|
+
const offsetToUse = timezoneOffsetOverride ?? timezoneOffset;
|
|
7938
|
+
// Create date-time from the Date object without timezone conversion
|
|
7939
|
+
// Extract year, month, day from the date
|
|
7940
|
+
const year = newDate.getFullYear();
|
|
7941
|
+
const month = newDate.getMonth();
|
|
7942
|
+
const day = newDate.getDate();
|
|
7943
|
+
// Create a date-time string with the exact values from the picker
|
|
7944
|
+
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')}`;
|
|
7945
|
+
// Ensure offset format is correct (should be +HH:mm or -HH:mm, not ending with Z)
|
|
7946
|
+
const cleanOffset = offsetToUse.replace(/Z$/, '');
|
|
7947
|
+
onChange?.(`${formattedDateTime}${cleanOffset}`);
|
|
7948
|
+
return;
|
|
7949
|
+
}
|
|
7950
|
+
// Normal mode: use timezone conversion
|
|
7951
|
+
let dateTime = dayjs(newDate)
|
|
7952
|
+
.tz(tz)
|
|
7953
|
+
.hour(hour24)
|
|
7954
|
+
.minute(newMinute)
|
|
7955
|
+
.second(newSecond ?? 0)
|
|
7956
|
+
.millisecond(0);
|
|
7957
|
+
if (!dateTime.isValid()) {
|
|
7405
7958
|
onChange?.(undefined);
|
|
7406
7959
|
return;
|
|
7407
7960
|
}
|
|
7408
|
-
|
|
7409
|
-
if (
|
|
7410
|
-
|
|
7961
|
+
// Format based on format prop
|
|
7962
|
+
if (format === 'iso-date-time') {
|
|
7963
|
+
onChange?.(dateTime.format('YYYY-MM-DDTHH:mm:ss'));
|
|
7411
7964
|
}
|
|
7412
7965
|
else {
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
setHour12(null);
|
|
7416
|
-
setMinute(null);
|
|
7417
|
-
setMeridiem(null);
|
|
7418
|
-
setHour24(null);
|
|
7419
|
-
setSecond(null);
|
|
7420
|
-
onChange?.(undefined);
|
|
7966
|
+
// date-time format with timezone
|
|
7967
|
+
onChange?.(dateTime.format('YYYY-MM-DDTHH:mm:ssZ'));
|
|
7421
7968
|
}
|
|
7422
7969
|
};
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
setMinute(null);
|
|
7435
|
-
setMeridiem(null);
|
|
7436
|
-
setHour24(null);
|
|
7437
|
-
setSecond(null);
|
|
7438
|
-
onChange?.(undefined);
|
|
7439
|
-
return;
|
|
7970
|
+
// Handle date selection
|
|
7971
|
+
const handleDateSelected = ({ date, }) => {
|
|
7972
|
+
setSelectedDate(date);
|
|
7973
|
+
updateDateTime(date, hour, minute, second, meridiem);
|
|
7974
|
+
};
|
|
7975
|
+
// Handle time change
|
|
7976
|
+
const handleTimeChange = (newHour, newMinute, newSecond, newMeridiem) => {
|
|
7977
|
+
setHour(newHour);
|
|
7978
|
+
setMinute(newMinute);
|
|
7979
|
+
if (is24Hour) {
|
|
7980
|
+
setSecond(newSecond);
|
|
7440
7981
|
}
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
if (!dateObj.isValid()) {
|
|
7444
|
-
console.warn('[DateTimePicker] Invalid date object in updateDateTime, clearing fields:', date);
|
|
7445
|
-
setSelectedDate('');
|
|
7446
|
-
setHour12(null);
|
|
7447
|
-
setMinute(null);
|
|
7448
|
-
setMeridiem(null);
|
|
7449
|
-
setHour24(null);
|
|
7450
|
-
setSecond(null);
|
|
7451
|
-
onChange?.(undefined);
|
|
7452
|
-
return;
|
|
7982
|
+
else {
|
|
7983
|
+
setMeridiem(newMeridiem);
|
|
7453
7984
|
}
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7985
|
+
if (selectedDate) {
|
|
7986
|
+
updateDateTime(selectedDate, newHour, newMinute, newSecond, newMeridiem);
|
|
7987
|
+
}
|
|
7988
|
+
};
|
|
7989
|
+
// Calendar hook
|
|
7990
|
+
const calendarProps = useCalendar({
|
|
7991
|
+
selected: selectedDate || undefined,
|
|
7992
|
+
date: selectedDate || undefined,
|
|
7993
|
+
minDate,
|
|
7994
|
+
maxDate,
|
|
7995
|
+
monthsToDisplay: 1,
|
|
7996
|
+
onDateSelected: handleDateSelected,
|
|
7997
|
+
});
|
|
7998
|
+
// Convert DateTimePickerLabels to DatePickerLabels format
|
|
7999
|
+
const calendarLabels = React.useMemo(() => ({
|
|
8000
|
+
monthNamesShort: labels.monthNamesShort || [
|
|
8001
|
+
'Jan',
|
|
8002
|
+
'Feb',
|
|
8003
|
+
'Mar',
|
|
8004
|
+
'Apr',
|
|
8005
|
+
'May',
|
|
8006
|
+
'Jun',
|
|
8007
|
+
'Jul',
|
|
8008
|
+
'Aug',
|
|
8009
|
+
'Sep',
|
|
8010
|
+
'Oct',
|
|
8011
|
+
'Nov',
|
|
8012
|
+
'Dec',
|
|
8013
|
+
],
|
|
8014
|
+
weekdayNamesShort: labels.weekdayNamesShort || [
|
|
8015
|
+
'Sun',
|
|
8016
|
+
'Mon',
|
|
8017
|
+
'Tue',
|
|
8018
|
+
'Wed',
|
|
8019
|
+
'Thu',
|
|
8020
|
+
'Fri',
|
|
8021
|
+
'Sat',
|
|
8022
|
+
],
|
|
8023
|
+
backButtonLabel: labels.backButtonLabel || 'Back',
|
|
8024
|
+
forwardButtonLabel: labels.forwardButtonLabel || 'Forward',
|
|
8025
|
+
todayLabel: quickActionLabels.today || 'Today',
|
|
8026
|
+
yesterdayLabel: quickActionLabels.yesterday || 'Yesterday',
|
|
8027
|
+
tomorrowLabel: quickActionLabels.tomorrow || 'Tomorrow',
|
|
8028
|
+
}), [labels, quickActionLabels]);
|
|
8029
|
+
// Generate time options
|
|
8030
|
+
const timeOptions = React.useMemo(() => {
|
|
8031
|
+
const options = [];
|
|
8032
|
+
// Get start time for comparison if provided
|
|
8033
|
+
let startDateTime = null;
|
|
8034
|
+
let shouldFilterByDate = false;
|
|
8035
|
+
if (startTime && selectedDate) {
|
|
8036
|
+
const startDateObj = dayjs(startTime).tz(tz);
|
|
8037
|
+
const selectedDateObj = dayjs(selectedDate).tz(tz);
|
|
8038
|
+
if (startDateObj.isValid() && selectedDateObj.isValid()) {
|
|
8039
|
+
startDateTime = startDateObj;
|
|
8040
|
+
shouldFilterByDate =
|
|
8041
|
+
startDateObj.format('YYYY-MM-DD') ===
|
|
8042
|
+
selectedDateObj.format('YYYY-MM-DD');
|
|
8043
|
+
}
|
|
8044
|
+
}
|
|
8045
|
+
if (is24Hour) {
|
|
8046
|
+
// Generate 24-hour format options
|
|
8047
|
+
for (let h = 0; h < 24; h++) {
|
|
8048
|
+
for (let m = 0; m < 60; m += 15) {
|
|
8049
|
+
// Filter out times that would result in negative duration
|
|
8050
|
+
if (startDateTime && selectedDate && shouldFilterByDate) {
|
|
8051
|
+
const selectedDateObj = dayjs(selectedDate).tz(tz);
|
|
8052
|
+
const optionDateTime = selectedDateObj
|
|
8053
|
+
.hour(h)
|
|
8054
|
+
.minute(m)
|
|
8055
|
+
.second(0)
|
|
8056
|
+
.millisecond(0);
|
|
8057
|
+
if (optionDateTime.isBefore(startDateTime)) {
|
|
8058
|
+
continue;
|
|
8059
|
+
}
|
|
8060
|
+
}
|
|
8061
|
+
// Calculate duration if startTime is provided
|
|
8062
|
+
let durationText;
|
|
8063
|
+
if (startDateTime && selectedDate) {
|
|
8064
|
+
const selectedDateObj = dayjs(selectedDate).tz(tz);
|
|
8065
|
+
const optionDateTime = selectedDateObj
|
|
8066
|
+
.hour(h)
|
|
8067
|
+
.minute(m)
|
|
8068
|
+
.second(0)
|
|
8069
|
+
.millisecond(0);
|
|
8070
|
+
if (optionDateTime.isValid() &&
|
|
8071
|
+
optionDateTime.isAfter(startDateTime)) {
|
|
8072
|
+
const diffMs = optionDateTime.diff(startDateTime);
|
|
8073
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
8074
|
+
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
8075
|
+
const diffSeconds = Math.floor((diffMs % (1000 * 60)) / 1000);
|
|
8076
|
+
if (diffHours > 0 || diffMinutes > 0 || diffSeconds > 0) {
|
|
8077
|
+
let diffText = '';
|
|
8078
|
+
if (diffHours > 0) {
|
|
8079
|
+
diffText = `${diffHours}h ${diffMinutes}m`;
|
|
8080
|
+
}
|
|
8081
|
+
else if (diffMinutes > 0) {
|
|
8082
|
+
diffText = `${diffMinutes}m ${diffSeconds}s`;
|
|
8083
|
+
}
|
|
8084
|
+
else {
|
|
8085
|
+
diffText = `${diffSeconds}s`;
|
|
8086
|
+
}
|
|
8087
|
+
durationText = `+${diffText}`;
|
|
8088
|
+
}
|
|
8089
|
+
}
|
|
8090
|
+
}
|
|
8091
|
+
const s = showSeconds ? 0 : 0;
|
|
8092
|
+
const timeDisplay = showSeconds
|
|
8093
|
+
? `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:00`
|
|
8094
|
+
: `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
|
|
8095
|
+
options.push({
|
|
8096
|
+
label: timeDisplay,
|
|
8097
|
+
value: `${h}:${m}:${s}`,
|
|
8098
|
+
hour: h,
|
|
8099
|
+
minute: m,
|
|
8100
|
+
second: s,
|
|
8101
|
+
searchText: timeDisplay,
|
|
8102
|
+
durationText,
|
|
8103
|
+
});
|
|
8104
|
+
}
|
|
7472
8105
|
}
|
|
7473
|
-
console.log('[DateTimePicker] ISO format - setting time on date:', {
|
|
7474
|
-
h,
|
|
7475
|
-
m,
|
|
7476
|
-
s,
|
|
7477
|
-
showSeconds,
|
|
7478
|
-
});
|
|
7479
|
-
if (h !== null)
|
|
7480
|
-
newDate.setHours(h);
|
|
7481
|
-
if (m !== null)
|
|
7482
|
-
newDate.setMinutes(m);
|
|
7483
|
-
newDate.setSeconds(s ?? 0);
|
|
7484
8106
|
}
|
|
7485
8107
|
else {
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
|
|
7505
|
-
|
|
8108
|
+
// Generate 12-hour format options
|
|
8109
|
+
for (let h = 1; h <= 12; h++) {
|
|
8110
|
+
for (let m = 0; m < 60; m += 15) {
|
|
8111
|
+
for (const mer of ['am', 'pm']) {
|
|
8112
|
+
// Convert 12-hour to 24-hour for comparison
|
|
8113
|
+
let hour24 = h;
|
|
8114
|
+
if (mer === 'am' && h === 12)
|
|
8115
|
+
hour24 = 0;
|
|
8116
|
+
else if (mer === 'pm' && h < 12)
|
|
8117
|
+
hour24 = h + 12;
|
|
8118
|
+
// Filter out times that would result in negative duration
|
|
8119
|
+
if (startDateTime && selectedDate && shouldFilterByDate) {
|
|
8120
|
+
const selectedDateObj = dayjs(selectedDate).tz(tz);
|
|
8121
|
+
const optionDateTime = selectedDateObj
|
|
8122
|
+
.hour(hour24)
|
|
8123
|
+
.minute(m)
|
|
8124
|
+
.second(0)
|
|
8125
|
+
.millisecond(0);
|
|
8126
|
+
if (optionDateTime.isBefore(startDateTime)) {
|
|
8127
|
+
continue;
|
|
8128
|
+
}
|
|
8129
|
+
}
|
|
8130
|
+
// Calculate duration if startTime is provided
|
|
8131
|
+
let durationText;
|
|
8132
|
+
if (startDateTime && selectedDate) {
|
|
8133
|
+
const selectedDateObj = dayjs(selectedDate).tz(tz);
|
|
8134
|
+
const optionDateTime = selectedDateObj
|
|
8135
|
+
.hour(hour24)
|
|
8136
|
+
.minute(m)
|
|
8137
|
+
.second(0)
|
|
8138
|
+
.millisecond(0);
|
|
8139
|
+
if (optionDateTime.isValid() &&
|
|
8140
|
+
optionDateTime.isAfter(startDateTime)) {
|
|
8141
|
+
const diffMs = optionDateTime.diff(startDateTime);
|
|
8142
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
8143
|
+
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
8144
|
+
const diffSeconds = Math.floor((diffMs % (1000 * 60)) / 1000);
|
|
8145
|
+
if (diffHours > 0 || diffMinutes > 0 || diffSeconds > 0) {
|
|
8146
|
+
let diffText = '';
|
|
8147
|
+
if (diffHours > 0) {
|
|
8148
|
+
diffText = `${diffHours}h ${diffMinutes}m`;
|
|
8149
|
+
}
|
|
8150
|
+
else if (diffMinutes > 0) {
|
|
8151
|
+
diffText = `${diffMinutes}m ${diffSeconds}s`;
|
|
8152
|
+
}
|
|
8153
|
+
else {
|
|
8154
|
+
diffText = `${diffSeconds}s`;
|
|
8155
|
+
}
|
|
8156
|
+
durationText = `+${diffText}`;
|
|
8157
|
+
}
|
|
8158
|
+
}
|
|
8159
|
+
}
|
|
8160
|
+
const hourDisplay = h.toString();
|
|
8161
|
+
const minuteDisplay = m.toString().padStart(2, '0');
|
|
8162
|
+
const timeDisplay = `${hourDisplay}:${minuteDisplay} ${mer.toUpperCase()}`;
|
|
8163
|
+
options.push({
|
|
8164
|
+
label: timeDisplay,
|
|
8165
|
+
value: `${h}:${m}:${mer}`,
|
|
8166
|
+
hour: h,
|
|
8167
|
+
minute: m,
|
|
8168
|
+
meridiem: mer,
|
|
8169
|
+
searchText: timeDisplay,
|
|
8170
|
+
durationText,
|
|
8171
|
+
});
|
|
8172
|
+
}
|
|
8173
|
+
}
|
|
7506
8174
|
}
|
|
7507
|
-
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
|
|
8175
|
+
// Sort 12-hour options by time
|
|
8176
|
+
return options.sort((a, b) => {
|
|
8177
|
+
const a12 = a;
|
|
8178
|
+
const b12 = b;
|
|
8179
|
+
let hour24A = a12.hour;
|
|
8180
|
+
if (a12.meridiem === 'am' && a12.hour === 12)
|
|
8181
|
+
hour24A = 0;
|
|
8182
|
+
else if (a12.meridiem === 'pm' && a12.hour < 12)
|
|
8183
|
+
hour24A = a12.hour + 12;
|
|
8184
|
+
let hour24B = b12.hour;
|
|
8185
|
+
if (b12.meridiem === 'am' && b12.hour === 12)
|
|
8186
|
+
hour24B = 0;
|
|
8187
|
+
else if (b12.meridiem === 'pm' && b12.hour < 12)
|
|
8188
|
+
hour24B = b12.hour + 12;
|
|
8189
|
+
if (hour24A !== hour24B) {
|
|
8190
|
+
return hour24A - hour24B;
|
|
8191
|
+
}
|
|
8192
|
+
return a12.minute - b12.minute;
|
|
7511
8193
|
});
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
8194
|
+
}
|
|
8195
|
+
return options;
|
|
8196
|
+
}, [startTime, selectedDate, tz, is24Hour, showSeconds]);
|
|
8197
|
+
// Time picker combobox setup
|
|
8198
|
+
const itemToString = React.useMemo(() => {
|
|
8199
|
+
return (item) => {
|
|
8200
|
+
return item.searchText;
|
|
8201
|
+
};
|
|
8202
|
+
}, []);
|
|
8203
|
+
const { contains } = react.useFilter({ sensitivity: 'base' });
|
|
8204
|
+
const customTimeFilter = React.useMemo(() => {
|
|
8205
|
+
if (is24Hour) {
|
|
8206
|
+
return contains;
|
|
8207
|
+
}
|
|
8208
|
+
return (itemText, filterText) => {
|
|
8209
|
+
if (!filterText) {
|
|
8210
|
+
return true;
|
|
7524
8211
|
}
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
});
|
|
8212
|
+
const lowerItemText = itemText.toLowerCase();
|
|
8213
|
+
const lowerFilterText = filterText.toLowerCase();
|
|
8214
|
+
if (lowerItemText.includes(lowerFilterText)) {
|
|
8215
|
+
return true;
|
|
7530
8216
|
}
|
|
7531
|
-
|
|
7532
|
-
|
|
8217
|
+
const item = timeOptions.find((opt) => opt.searchText.toLowerCase() === lowerItemText);
|
|
8218
|
+
if (!item || !('meridiem' in item)) {
|
|
8219
|
+
return false;
|
|
7533
8220
|
}
|
|
7534
|
-
|
|
7535
|
-
|
|
8221
|
+
let hour24 = item.hour;
|
|
8222
|
+
if (item.meridiem === 'am' && item.hour === 12)
|
|
8223
|
+
hour24 = 0;
|
|
8224
|
+
else if (item.meridiem === 'pm' && item.hour < 12)
|
|
8225
|
+
hour24 = item.hour + 12;
|
|
8226
|
+
const hour24Str = hour24.toString().padStart(2, '0');
|
|
8227
|
+
const minuteStr = item.minute.toString().padStart(2, '0');
|
|
8228
|
+
const formats = [
|
|
8229
|
+
`${hour24Str}:${minuteStr}`,
|
|
8230
|
+
`${hour24Str}${minuteStr}`,
|
|
8231
|
+
hour24Str,
|
|
8232
|
+
`${hour24}:${minuteStr}`,
|
|
8233
|
+
hour24.toString(),
|
|
8234
|
+
];
|
|
8235
|
+
return formats.some((format) => format.toLowerCase().includes(lowerFilterText) ||
|
|
8236
|
+
lowerFilterText.includes(format.toLowerCase()));
|
|
8237
|
+
};
|
|
8238
|
+
}, [timeOptions, is24Hour, contains]);
|
|
8239
|
+
const { collection, filter } = react.useListCollection({
|
|
8240
|
+
initialItems: timeOptions,
|
|
8241
|
+
itemToString: itemToString,
|
|
8242
|
+
itemToValue: (item) => item.value,
|
|
8243
|
+
filter: customTimeFilter,
|
|
8244
|
+
});
|
|
8245
|
+
// Get current value string for combobox (must match option.value format)
|
|
8246
|
+
const currentTimeValue = React.useMemo(() => {
|
|
8247
|
+
if (is24Hour) {
|
|
8248
|
+
if (hour === null || minute === null) {
|
|
8249
|
+
return '';
|
|
7536
8250
|
}
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
const finalISO = dayjs(newDate).tz(timezone).toISOString();
|
|
7540
|
-
console.log('[DateTimePicker] Final ISO string to emit:', {
|
|
7541
|
-
newDate: newDate.toISOString(),
|
|
7542
|
-
timezone,
|
|
7543
|
-
finalISO,
|
|
7544
|
-
});
|
|
7545
|
-
onChange?.(finalISO);
|
|
7546
|
-
};
|
|
7547
|
-
const handleClear = () => {
|
|
7548
|
-
setSelectedDate('');
|
|
7549
|
-
setHour12(null);
|
|
7550
|
-
setHour24(null);
|
|
7551
|
-
setMinute(null);
|
|
7552
|
-
setSecond(null);
|
|
7553
|
-
setMeridiem(null);
|
|
7554
|
-
onChange?.(undefined);
|
|
7555
|
-
};
|
|
7556
|
-
const isISO = format === 'iso-date-time';
|
|
7557
|
-
// Normalize startTime to ignore milliseconds
|
|
7558
|
-
const normalizedStartTime = startTime
|
|
7559
|
-
? dayjs(startTime).tz(timezone).millisecond(0).toISOString()
|
|
7560
|
-
: undefined;
|
|
7561
|
-
// Determine minDate: prioritize explicit minDate prop, then fall back to startTime
|
|
7562
|
-
const effectiveMinDate = minDate
|
|
7563
|
-
? minDate
|
|
7564
|
-
: normalizedStartTime && dayjs(normalizedStartTime).tz(timezone).isValid()
|
|
7565
|
-
? dayjs(normalizedStartTime).tz(timezone).startOf('day').toDate()
|
|
7566
|
-
: undefined;
|
|
7567
|
-
// Log current state before render
|
|
7568
|
-
React.useEffect(() => {
|
|
7569
|
-
console.log('[DateTimePicker] Current state before render:', {
|
|
7570
|
-
isISO,
|
|
7571
|
-
hour12,
|
|
7572
|
-
minute,
|
|
7573
|
-
meridiem,
|
|
7574
|
-
hour24,
|
|
7575
|
-
second,
|
|
7576
|
-
selectedDate,
|
|
7577
|
-
normalizedStartTime,
|
|
7578
|
-
timezone,
|
|
7579
|
-
});
|
|
7580
|
-
}, [
|
|
7581
|
-
isISO,
|
|
7582
|
-
hour12,
|
|
7583
|
-
minute,
|
|
7584
|
-
meridiem,
|
|
7585
|
-
hour24,
|
|
7586
|
-
second,
|
|
7587
|
-
selectedDate,
|
|
7588
|
-
normalizedStartTime,
|
|
7589
|
-
timezone,
|
|
7590
|
-
]);
|
|
7591
|
-
// Compute display text from current state
|
|
7592
|
-
const displayText = React.useMemo(() => {
|
|
7593
|
-
if (!selectedDate)
|
|
7594
|
-
return null;
|
|
7595
|
-
const dateObj = dayjs.tz(selectedDate, timezone);
|
|
7596
|
-
if (!dateObj.isValid())
|
|
7597
|
-
return null;
|
|
7598
|
-
if (isISO) {
|
|
7599
|
-
// For ISO format, use hour24, minute, second
|
|
7600
|
-
if (hour24 === null || minute === null)
|
|
7601
|
-
return null;
|
|
7602
|
-
const dateTimeObj = dateObj
|
|
7603
|
-
.hour(hour24)
|
|
7604
|
-
.minute(minute)
|
|
7605
|
-
.second(second ?? 0);
|
|
7606
|
-
return dateTimeObj.format(showSeconds ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm');
|
|
8251
|
+
const s = second ?? 0;
|
|
8252
|
+
return `${hour}:${minute}:${s}`;
|
|
7607
8253
|
}
|
|
7608
8254
|
else {
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7618
|
-
|
|
7619
|
-
|
|
8255
|
+
if (hour === null || minute === null || meridiem === null) {
|
|
8256
|
+
return '';
|
|
8257
|
+
}
|
|
8258
|
+
return `${hour}:${minute}:${meridiem}`;
|
|
8259
|
+
}
|
|
8260
|
+
}, [hour, minute, second, meridiem, is24Hour]);
|
|
8261
|
+
// Parse custom time input formats like "1400", "2pm", "14:00", "2:00 PM"
|
|
8262
|
+
const parseCustomTimeInput = (input) => {
|
|
8263
|
+
if (!input || !input.trim()) {
|
|
8264
|
+
return { hour: null, minute: null, second: null, meridiem: null };
|
|
8265
|
+
}
|
|
8266
|
+
const trimmed = input.trim().toLowerCase();
|
|
8267
|
+
// Try parsing 4-digit format without colon: "1400" -> 14:00
|
|
8268
|
+
const fourDigitMatch = trimmed.match(/^(\d{4})$/);
|
|
8269
|
+
if (fourDigitMatch) {
|
|
8270
|
+
const digits = fourDigitMatch[1];
|
|
8271
|
+
const hour = parseInt(digits.substring(0, 2), 10);
|
|
8272
|
+
const minute = parseInt(digits.substring(2, 4), 10);
|
|
8273
|
+
if (hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59) {
|
|
8274
|
+
if (is24Hour) {
|
|
8275
|
+
return { hour, minute, second: 0, meridiem: null };
|
|
8276
|
+
}
|
|
8277
|
+
else {
|
|
8278
|
+
// Convert to 12-hour format
|
|
8279
|
+
let hour12 = hour;
|
|
8280
|
+
let meridiem;
|
|
8281
|
+
if (hour === 0) {
|
|
8282
|
+
hour12 = 12;
|
|
8283
|
+
meridiem = 'am';
|
|
8284
|
+
}
|
|
8285
|
+
else if (hour === 12) {
|
|
8286
|
+
hour12 = 12;
|
|
8287
|
+
meridiem = 'pm';
|
|
8288
|
+
}
|
|
8289
|
+
else if (hour > 12) {
|
|
8290
|
+
hour12 = hour - 12;
|
|
8291
|
+
meridiem = 'pm';
|
|
8292
|
+
}
|
|
8293
|
+
else {
|
|
8294
|
+
hour12 = hour;
|
|
8295
|
+
meridiem = 'am';
|
|
8296
|
+
}
|
|
8297
|
+
return { hour: hour12, minute, second: null, meridiem };
|
|
8298
|
+
}
|
|
8299
|
+
}
|
|
7620
8300
|
}
|
|
7621
|
-
|
|
7622
|
-
|
|
7623
|
-
|
|
7624
|
-
|
|
7625
|
-
|
|
7626
|
-
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
8301
|
+
// Try parsing hour with meridiem: "2pm", "14pm", "2am"
|
|
8302
|
+
const hourMeridiemMatch = trimmed.match(/^(\d{1,2})\s*(am|pm)$/);
|
|
8303
|
+
if (hourMeridiemMatch && !is24Hour) {
|
|
8304
|
+
const hour12 = parseInt(hourMeridiemMatch[1], 10);
|
|
8305
|
+
const meridiem = hourMeridiemMatch[2];
|
|
8306
|
+
if (hour12 >= 1 && hour12 <= 12) {
|
|
8307
|
+
return { hour: hour12, minute: 0, second: null, meridiem };
|
|
8308
|
+
}
|
|
8309
|
+
}
|
|
8310
|
+
// Try parsing 24-hour format with hour only: "14" -> 14:00
|
|
8311
|
+
const hourOnlyMatch = trimmed.match(/^(\d{1,2})$/);
|
|
8312
|
+
if (hourOnlyMatch && is24Hour) {
|
|
8313
|
+
const hour = parseInt(hourOnlyMatch[1], 10);
|
|
8314
|
+
if (hour >= 0 && hour <= 23) {
|
|
8315
|
+
return { hour, minute: 0, second: 0, meridiem: null };
|
|
8316
|
+
}
|
|
8317
|
+
}
|
|
8318
|
+
// Try parsing standard formats: "14:00", "2:00 PM"
|
|
8319
|
+
const time24Pattern = /^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?$/;
|
|
8320
|
+
const match24 = trimmed.match(time24Pattern);
|
|
8321
|
+
if (match24) {
|
|
8322
|
+
const hour24 = parseInt(match24[1], 10);
|
|
8323
|
+
const minute = parseInt(match24[2], 10);
|
|
8324
|
+
const second = match24[3] ? parseInt(match24[3], 10) : 0;
|
|
8325
|
+
if (hour24 >= 0 &&
|
|
8326
|
+
hour24 <= 23 &&
|
|
8327
|
+
minute >= 0 &&
|
|
8328
|
+
minute <= 59 &&
|
|
8329
|
+
second >= 0 &&
|
|
8330
|
+
second <= 59) {
|
|
8331
|
+
if (is24Hour) {
|
|
8332
|
+
return { hour: hour24, minute, second, meridiem: null };
|
|
8333
|
+
}
|
|
8334
|
+
else {
|
|
8335
|
+
// Convert to 12-hour format
|
|
8336
|
+
let hour12 = hour24;
|
|
8337
|
+
let meridiem;
|
|
8338
|
+
if (hour24 === 0) {
|
|
8339
|
+
hour12 = 12;
|
|
8340
|
+
meridiem = 'am';
|
|
8341
|
+
}
|
|
8342
|
+
else if (hour24 === 12) {
|
|
8343
|
+
hour12 = 12;
|
|
8344
|
+
meridiem = 'pm';
|
|
8345
|
+
}
|
|
8346
|
+
else if (hour24 > 12) {
|
|
8347
|
+
hour12 = hour24 - 12;
|
|
8348
|
+
meridiem = 'pm';
|
|
7641
8349
|
}
|
|
7642
8350
|
else {
|
|
7643
|
-
|
|
7644
|
-
|
|
8351
|
+
hour12 = hour24;
|
|
8352
|
+
meridiem = 'am';
|
|
8353
|
+
}
|
|
8354
|
+
return { hour: hour12, minute, second: null, meridiem };
|
|
8355
|
+
}
|
|
8356
|
+
}
|
|
8357
|
+
}
|
|
8358
|
+
// Try parsing 12-hour format: "2:00 PM", "2:00PM"
|
|
8359
|
+
const time12Pattern = /^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?\s*(am|pm)$/;
|
|
8360
|
+
const match12 = trimmed.match(time12Pattern);
|
|
8361
|
+
if (match12 && !is24Hour) {
|
|
8362
|
+
const hour12 = parseInt(match12[1], 10);
|
|
8363
|
+
const minute = parseInt(match12[2], 10);
|
|
8364
|
+
const second = match12[3] ? parseInt(match12[3], 10) : null;
|
|
8365
|
+
const meridiem = match12[4];
|
|
8366
|
+
if (hour12 >= 1 &&
|
|
8367
|
+
hour12 <= 12 &&
|
|
8368
|
+
minute >= 0 &&
|
|
8369
|
+
minute <= 59 &&
|
|
8370
|
+
(second === null || (second >= 0 && second <= 59))) {
|
|
8371
|
+
return { hour: hour12, minute, second, meridiem };
|
|
8372
|
+
}
|
|
8373
|
+
}
|
|
8374
|
+
return { hour: null, minute: null, second: null, meridiem: null };
|
|
8375
|
+
};
|
|
8376
|
+
const handleTimeValueChange = (details) => {
|
|
8377
|
+
if (details.value.length === 0) {
|
|
8378
|
+
handleTimeChange(null, null, null, null);
|
|
8379
|
+
filter('');
|
|
8380
|
+
return;
|
|
8381
|
+
}
|
|
8382
|
+
const selectedValue = details.value[0];
|
|
8383
|
+
const selectedOption = timeOptions.find((opt) => opt.value === selectedValue);
|
|
8384
|
+
if (selectedOption) {
|
|
8385
|
+
filter('');
|
|
8386
|
+
if (is24Hour) {
|
|
8387
|
+
const opt24 = selectedOption;
|
|
8388
|
+
handleTimeChange(opt24.hour, opt24.minute, opt24.second, null);
|
|
8389
|
+
}
|
|
8390
|
+
else {
|
|
8391
|
+
const opt12 = selectedOption;
|
|
8392
|
+
handleTimeChange(opt12.hour, opt12.minute, null, opt12.meridiem);
|
|
8393
|
+
}
|
|
8394
|
+
}
|
|
8395
|
+
};
|
|
8396
|
+
// Track the current input value for Enter key handling
|
|
8397
|
+
const [timeInputValue, setTimeInputValue] = React.useState('');
|
|
8398
|
+
const handleTimeInputChange = (details) => {
|
|
8399
|
+
// Store the input value and filter
|
|
8400
|
+
setTimeInputValue(details.inputValue);
|
|
8401
|
+
filter(details.inputValue);
|
|
8402
|
+
};
|
|
8403
|
+
const handleTimeInputKeyDown = (e) => {
|
|
8404
|
+
if (e.key === 'Enter') {
|
|
8405
|
+
e.preventDefault();
|
|
8406
|
+
// Use the stored input value
|
|
8407
|
+
const parsed = parseCustomTimeInput(timeInputValue);
|
|
8408
|
+
if (parsed.hour !== null && parsed.minute !== null) {
|
|
8409
|
+
if (is24Hour) {
|
|
8410
|
+
handleTimeChange(parsed.hour, parsed.minute, parsed.second, null);
|
|
8411
|
+
}
|
|
8412
|
+
else {
|
|
8413
|
+
if (parsed.meridiem !== null) {
|
|
8414
|
+
handleTimeChange(parsed.hour, parsed.minute, null, parsed.meridiem);
|
|
7645
8415
|
}
|
|
7646
|
-
}
|
|
8416
|
+
}
|
|
8417
|
+
// Clear the filter and input value after applying
|
|
8418
|
+
filter('');
|
|
8419
|
+
setTimeInputValue('');
|
|
8420
|
+
// Close the popover if value is valid
|
|
8421
|
+
setTimePopoverOpen(false);
|
|
8422
|
+
}
|
|
8423
|
+
}
|
|
8424
|
+
};
|
|
8425
|
+
return (jsxRuntime.jsxs(react.Flex, { direction: "row", gap: 2, align: "center", children: [jsxRuntime.jsxs(react.Popover.Root, { open: datePopoverOpen, onOpenChange: (e) => setDatePopoverOpen(e.open), closeOnInteractOutside: true, autoFocus: false, children: [jsxRuntime.jsx(react.Popover.Trigger, { asChild: true, children: jsxRuntime.jsxs(react.Button, { size: "sm", variant: "outline", onClick: () => setDatePopoverOpen(true), justifyContent: "start", children: [jsxRuntime.jsx(md.MdDateRange, {}), dateDisplayText] }) }), portalled ? (jsxRuntime.jsx(react.Portal, { children: jsxRuntime.jsx(react.Popover.Positioner, { children: jsxRuntime.jsx(react.Popover.Content, { width: "fit-content", children: jsxRuntime.jsx(react.Popover.Body, { p: 4, children: jsxRuntime.jsxs(react.Grid, { gap: 4, children: [jsxRuntime.jsx(react.InputGroup, { endElement: jsxRuntime.jsxs(react.Popover.Root, { open: calendarPopoverOpen, onOpenChange: (e) => setCalendarPopoverOpen(e.open), closeOnInteractOutside: true, autoFocus: false, children: [jsxRuntime.jsx(react.Popover.Trigger, { asChild: true, children: jsxRuntime.jsx(react.Button, { variant: "ghost", size: "xs", "aria-label": "Open calendar", onClick: () => setCalendarPopoverOpen(true), children: jsxRuntime.jsx(md.MdDateRange, {}) }) }), jsxRuntime.jsx(react.Portal, { children: jsxRuntime.jsx(react.Popover.Positioner, { children: jsxRuntime.jsx(react.Popover.Content, { width: "fit-content", zIndex: 1500, children: jsxRuntime.jsx(react.Popover.Body, { p: 4, children: jsxRuntime.jsx(DatePickerContext.Provider, { value: { labels: calendarLabels }, children: jsxRuntime.jsx(Calendar, { ...calendarProps, firstDayOfWeek: 0 }) }) }) }) }) })] }), children: jsxRuntime.jsx(react.Input, { value: dateInputValue, onChange: handleDateInputChange, onBlur: handleDateInputBlur, onKeyDown: handleDateInputKeyDown, placeholder: "YYYY-MM-DD" }) }), showQuickActions && (jsxRuntime.jsxs(react.Grid, { templateColumns: "repeat(4, 1fr)", gap: 2, children: [jsxRuntime.jsx(react.Button, { size: "sm", variant: "outline", onClick: () => handleQuickActionClick(getYesterday()), disabled: !isDateValid(getYesterday()), children: quickActionLabels.yesterday }), jsxRuntime.jsx(react.Button, { size: "sm", variant: "outline", onClick: () => handleQuickActionClick(getToday()), disabled: !isDateValid(getToday()), children: quickActionLabels.today }), jsxRuntime.jsx(react.Button, { size: "sm", variant: "outline", onClick: () => handleQuickActionClick(getTomorrow()), disabled: !isDateValid(getTomorrow()), children: quickActionLabels.tomorrow }), jsxRuntime.jsx(react.Button, { size: "sm", variant: "outline", onClick: () => handleQuickActionClick(getPlus7Days()), disabled: !isDateValid(getPlus7Days()), children: quickActionLabels.plus7Days })] }))] }) }) }) }) })) : (jsxRuntime.jsx(react.Popover.Positioner, { children: jsxRuntime.jsx(react.Popover.Content, { width: "fit-content", children: jsxRuntime.jsx(react.Popover.Body, { p: 4, children: jsxRuntime.jsxs(react.Grid, { gap: 4, children: [jsxRuntime.jsx(react.InputGroup, { endElement: jsxRuntime.jsxs(react.Popover.Root, { open: calendarPopoverOpen, onOpenChange: (e) => setCalendarPopoverOpen(e.open), closeOnInteractOutside: true, autoFocus: false, children: [jsxRuntime.jsx(react.Popover.Trigger, { asChild: true, children: jsxRuntime.jsx(react.Button, { variant: "ghost", size: "xs", "aria-label": "Open calendar", onClick: () => setCalendarPopoverOpen(true), children: jsxRuntime.jsx(md.MdDateRange, {}) }) }), jsxRuntime.jsx(react.Portal, { children: jsxRuntime.jsx(react.Popover.Positioner, { children: jsxRuntime.jsx(react.Popover.Content, { width: "fit-content", zIndex: 1700, children: jsxRuntime.jsx(react.Popover.Body, { p: 4, children: jsxRuntime.jsx(DatePickerContext.Provider, { value: { labels: calendarLabels }, children: jsxRuntime.jsx(Calendar, { ...calendarProps, firstDayOfWeek: 0 }) }) }) }) }) })] }), children: jsxRuntime.jsx(react.Input, { value: dateInputValue, onChange: handleDateInputChange, onBlur: handleDateInputBlur, onKeyDown: handleDateInputKeyDown, placeholder: "YYYY-MM-DD" }) }), showQuickActions && (jsxRuntime.jsxs(react.Grid, { templateColumns: "repeat(4, 1fr)", gap: 2, children: [jsxRuntime.jsx(react.Button, { size: "sm", variant: "outline", onClick: () => handleQuickActionClick(getYesterday()), disabled: !isDateValid(getYesterday()), children: quickActionLabels.yesterday }), jsxRuntime.jsx(react.Button, { size: "sm", variant: "outline", onClick: () => handleQuickActionClick(getToday()), disabled: !isDateValid(getToday()), children: quickActionLabels.today }), jsxRuntime.jsx(react.Button, { size: "sm", variant: "outline", onClick: () => handleQuickActionClick(getTomorrow()), disabled: !isDateValid(getTomorrow()), children: quickActionLabels.tomorrow }), jsxRuntime.jsx(react.Button, { size: "sm", variant: "outline", onClick: () => handleQuickActionClick(getPlus7Days()), disabled: !isDateValid(getPlus7Days()), children: quickActionLabels.plus7Days })] }))] }) }) }) }))] }), jsxRuntime.jsxs(react.Popover.Root, { open: timePopoverOpen, onOpenChange: (e) => setTimePopoverOpen(e.open), closeOnInteractOutside: true, autoFocus: false, children: [jsxRuntime.jsx(react.Popover.Trigger, { asChild: true, children: jsxRuntime.jsxs(react.Button, { size: "sm", variant: "outline", onClick: () => setTimePopoverOpen(true), justifyContent: "start", children: [jsxRuntime.jsx(bs.BsClock, {}), timeDisplayText] }) }), portalled ? (jsxRuntime.jsx(react.Portal, { children: jsxRuntime.jsx(react.Popover.Positioner, { children: jsxRuntime.jsx(react.Popover.Content, { width: "fit-content", minW: "300px", children: jsxRuntime.jsx(react.Popover.Body, { p: 4, children: jsxRuntime.jsx(react.Grid, { gap: 2, children: jsxRuntime.jsxs(react.Combobox.Root, { value: currentTimeValue ? [currentTimeValue] : [], onValueChange: handleTimeValueChange, onInputValueChange: handleTimeInputChange, collection: collection, allowCustomValue: true, children: [jsxRuntime.jsxs(react.Combobox.Control, { children: [jsxRuntime.jsx(react.InputGroup, { startElement: jsxRuntime.jsx(bs.BsClock, {}), children: jsxRuntime.jsx(react.Combobox.Input, { placeholder: timePickerLabels?.placeholder ??
|
|
8426
|
+
(is24Hour ? 'HH:mm' : 'hh:mm AM/PM'), onKeyDown: handleTimeInputKeyDown }) }), jsxRuntime.jsx(react.Combobox.IndicatorGroup, { children: jsxRuntime.jsx(react.Combobox.Trigger, {}) })] }), jsxRuntime.jsx(react.Portal, { disabled: true, children: jsxRuntime.jsx(react.Combobox.Positioner, { children: jsxRuntime.jsxs(react.Combobox.Content, { children: [jsxRuntime.jsx(react.Combobox.Empty, { children: timePickerLabels?.emptyMessage ??
|
|
8427
|
+
'No time found' }), collection.items.map((item) => {
|
|
8428
|
+
const option = item;
|
|
8429
|
+
return (jsxRuntime.jsxs(react.Combobox.Item, { item: item, children: [jsxRuntime.jsxs(react.Flex, { justify: "space-between", align: "center", w: "100%", children: [jsxRuntime.jsx(react.Text, { children: option.label }), option.durationText && (jsxRuntime.jsx(react.Text, { fontSize: "xs", color: "gray.500", children: option.durationText }))] }), jsxRuntime.jsx(react.Combobox.ItemIndicator, {})] }, option.value));
|
|
8430
|
+
})] }) }) })] }) }) }) }) }) })) : (jsxRuntime.jsx(react.Popover.Positioner, { children: jsxRuntime.jsx(react.Popover.Content, { width: "fit-content", minW: "300px", children: jsxRuntime.jsx(react.Popover.Body, { p: 4, children: jsxRuntime.jsx(react.Grid, { gap: 2, children: jsxRuntime.jsxs(react.Combobox.Root, { value: currentTimeValue ? [currentTimeValue] : [], onValueChange: handleTimeValueChange, onInputValueChange: handleTimeInputChange, collection: collection, allowCustomValue: true, children: [jsxRuntime.jsxs(react.Combobox.Control, { children: [jsxRuntime.jsx(react.InputGroup, { startElement: jsxRuntime.jsx(bs.BsClock, {}), children: jsxRuntime.jsx(react.Combobox.Input, { placeholder: timePickerLabels?.placeholder ??
|
|
8431
|
+
(is24Hour ? 'HH:mm' : 'hh:mm AM/PM'), onKeyDown: handleTimeInputKeyDown }) }), jsxRuntime.jsx(react.Combobox.IndicatorGroup, { children: jsxRuntime.jsx(react.Combobox.Trigger, {}) })] }), jsxRuntime.jsx(react.Portal, { disabled: true, children: jsxRuntime.jsx(react.Combobox.Positioner, { children: jsxRuntime.jsxs(react.Combobox.Content, { children: [jsxRuntime.jsx(react.Combobox.Empty, { children: timePickerLabels?.emptyMessage ?? 'No time found' }), collection.items.map((item) => {
|
|
8432
|
+
const option = item;
|
|
8433
|
+
return (jsxRuntime.jsxs(react.Combobox.Item, { item: item, children: [jsxRuntime.jsxs(react.Flex, { justify: "space-between", align: "center", w: "100%", children: [jsxRuntime.jsx(react.Text, { children: option.label }), option.durationText && (jsxRuntime.jsx(react.Text, { fontSize: "xs", color: "gray.500", children: option.durationText }))] }), jsxRuntime.jsx(react.Combobox.ItemIndicator, {})] }, option.value));
|
|
8434
|
+
})] }) }) })] }) }) }) }) }))] }), showTimezoneSelector && (jsxRuntime.jsxs(react.Popover.Root, { open: timezonePopoverOpen, onOpenChange: (e) => setTimezonePopoverOpen(e.open), closeOnInteractOutside: true, autoFocus: false, children: [jsxRuntime.jsx(react.Popover.Trigger, { asChild: true, children: jsxRuntime.jsx(react.Button, { size: "sm", variant: "outline", onClick: () => setTimezonePopoverOpen(true), justifyContent: "start", children: timezoneDisplayText || 'Select timezone' }) }), portalled ? (jsxRuntime.jsx(react.Portal, { children: jsxRuntime.jsx(react.Popover.Positioner, { children: jsxRuntime.jsx(react.Popover.Content, { width: "fit-content", minW: "250px", children: jsxRuntime.jsx(react.Popover.Body, { p: 4, children: jsxRuntime.jsx(react.Grid, { gap: 2, children: jsxRuntime.jsxs(react.Select.Root, { size: "sm", collection: timezoneCollection, value: validTimezoneOffset ? [validTimezoneOffset] : [], onValueChange: (e) => {
|
|
8435
|
+
const newOffset = e.value[0];
|
|
8436
|
+
if (newOffset) {
|
|
8437
|
+
// Update controlled or internal state
|
|
8438
|
+
if (onTimezoneOffsetChange) {
|
|
8439
|
+
onTimezoneOffsetChange(newOffset);
|
|
8440
|
+
}
|
|
8441
|
+
else {
|
|
8442
|
+
setInternalTimezoneOffset(newOffset);
|
|
8443
|
+
}
|
|
8444
|
+
// Update date-time with new offset (pass it directly to avoid stale state)
|
|
8445
|
+
if (selectedDate &&
|
|
8446
|
+
hour !== null &&
|
|
8447
|
+
minute !== null) {
|
|
8448
|
+
updateDateTime(selectedDate, hour, minute, second, meridiem, newOffset);
|
|
8449
|
+
}
|
|
8450
|
+
// Close popover after selection
|
|
8451
|
+
setTimezonePopoverOpen(false);
|
|
8452
|
+
}
|
|
8453
|
+
}, children: [jsxRuntime.jsxs(react.Select.Control, { children: [jsxRuntime.jsx(react.Select.Trigger, {}), jsxRuntime.jsx(react.Select.IndicatorGroup, { children: jsxRuntime.jsx(react.Select.Indicator, {}) })] }), jsxRuntime.jsx(react.Select.Positioner, { children: jsxRuntime.jsx(react.Select.Content, { children: timezoneCollection.items.map((item) => (jsxRuntime.jsxs(react.Select.Item, { item: item, children: [jsxRuntime.jsx(react.Select.ItemText, { children: item.label }), jsxRuntime.jsx(react.Select.ItemIndicator, {})] }, item.value))) }) })] }) }) }) }) }) })) : (jsxRuntime.jsx(react.Popover.Positioner, { children: jsxRuntime.jsx(react.Popover.Content, { width: "fit-content", minW: "250px", children: jsxRuntime.jsx(react.Popover.Body, { p: 4, children: jsxRuntime.jsx(react.Grid, { gap: 2, children: jsxRuntime.jsxs(react.Select.Root, { size: "sm", collection: timezoneCollection, value: validTimezoneOffset ? [validTimezoneOffset] : [], onValueChange: (e) => {
|
|
8454
|
+
const newOffset = e.value[0];
|
|
8455
|
+
if (newOffset) {
|
|
8456
|
+
// Update controlled or internal state
|
|
8457
|
+
if (onTimezoneOffsetChange) {
|
|
8458
|
+
onTimezoneOffsetChange(newOffset);
|
|
8459
|
+
}
|
|
8460
|
+
else {
|
|
8461
|
+
setInternalTimezoneOffset(newOffset);
|
|
8462
|
+
}
|
|
8463
|
+
// Update date-time with new offset (pass it directly to avoid stale state)
|
|
8464
|
+
if (selectedDate &&
|
|
8465
|
+
hour !== null &&
|
|
8466
|
+
minute !== null) {
|
|
8467
|
+
updateDateTime(selectedDate, hour, minute, second, meridiem, newOffset);
|
|
8468
|
+
}
|
|
8469
|
+
// Close popover after selection
|
|
8470
|
+
setTimezonePopoverOpen(false);
|
|
8471
|
+
}
|
|
8472
|
+
}, children: [jsxRuntime.jsxs(react.Select.Control, { children: [jsxRuntime.jsx(react.Select.Trigger, {}), jsxRuntime.jsx(react.Select.IndicatorGroup, { children: jsxRuntime.jsx(react.Select.Indicator, {}) })] }), jsxRuntime.jsx(react.Select.Positioner, { children: jsxRuntime.jsx(react.Select.Content, { children: timezoneCollection.items.map((item) => (jsxRuntime.jsxs(react.Select.Item, { item: item, children: [jsxRuntime.jsx(react.Select.ItemText, { children: item.label }), jsxRuntime.jsx(react.Select.ItemIndicator, {})] }, item.value))) }) })] }) }) }) }) }))] }))] }));
|
|
7647
8473
|
}
|
|
7648
8474
|
|
|
7649
8475
|
dayjs.extend(utc);
|
|
@@ -7654,14 +8480,15 @@ const DateTimePicker = ({ column, schema, prefix, }) => {
|
|
|
7654
8480
|
const formI18n = useFormI18n(column, prefix, schema);
|
|
7655
8481
|
const { required, gridColumn = 'span 12', gridRow = 'span 1', displayDateFormat = 'YYYY-MM-DD HH:mm:ss',
|
|
7656
8482
|
// with timezone
|
|
7657
|
-
dateFormat = 'YYYY-MM-DD[T]HH:mm:ssZ', } = schema;
|
|
8483
|
+
dateFormat = 'YYYY-MM-DD[T]HH:mm:ssZ', dateTimePicker, } = schema;
|
|
7658
8484
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
7659
8485
|
const colLabel = formI18n.colLabel;
|
|
7660
|
-
|
|
8486
|
+
React.useState(false);
|
|
7661
8487
|
const selectedDate = watch(colLabel);
|
|
7662
|
-
|
|
8488
|
+
selectedDate && dayjs(selectedDate).tz(timezone).isValid()
|
|
7663
8489
|
? dayjs(selectedDate).tz(timezone).format(displayDateFormat)
|
|
7664
8490
|
: '';
|
|
8491
|
+
// Set default date on mount if no value exists
|
|
7665
8492
|
const dateTimePickerLabelsConfig = {
|
|
7666
8493
|
monthNamesShort: dateTimePickerLabels?.monthNamesShort ?? [
|
|
7667
8494
|
'January',
|
|
@@ -7701,11 +8528,10 @@ const DateTimePicker = ({ column, schema, prefix, }) => {
|
|
|
7701
8528
|
else {
|
|
7702
8529
|
setValue(colLabel, undefined);
|
|
7703
8530
|
}
|
|
7704
|
-
}, timezone: timezone, labels: dateTimePickerLabelsConfig, timePickerLabels: timePickerLabels
|
|
8531
|
+
}, timezone: timezone, labels: dateTimePickerLabelsConfig, timePickerLabels: timePickerLabels, showQuickActions: dateTimePicker?.showQuickActions ?? false, quickActionLabels: dateTimePickerLabels?.quickActionLabels ??
|
|
8532
|
+
dateTimePicker?.quickActionLabels, showTimezoneSelector: dateTimePicker?.showTimezoneSelector ?? false }));
|
|
7705
8533
|
return (jsxRuntime.jsx(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
7706
|
-
gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children:
|
|
7707
|
-
setOpen(true);
|
|
7708
|
-
}, justifyContent: 'start', children: [jsxRuntime.jsx(md.MdDateRange, {}), displayDate || ''] }) }), insideDialog ? (jsxRuntime.jsx(react.Popover.Positioner, { children: jsxRuntime.jsx(react.Popover.Content, { width: "fit-content", minW: "450px", minH: "25rem", children: jsxRuntime.jsx(react.Popover.Body, { children: dateTimePickerContent }) }) })) : (jsxRuntime.jsx(react.Portal, { children: jsxRuntime.jsx(react.Popover.Positioner, { children: jsxRuntime.jsx(react.Popover.Content, { width: "fit-content", minW: "450px", minH: "25rem", children: jsxRuntime.jsx(react.Popover.Body, { children: dateTimePickerContent }) }) }) }))] }) }));
|
|
8534
|
+
gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: dateTimePickerContent }));
|
|
7709
8535
|
};
|
|
7710
8536
|
|
|
7711
8537
|
const SchemaRenderer = ({ schema, prefix, column, }) => {
|
|
@@ -7826,7 +8652,7 @@ const BooleanViewer = ({ schema, column, prefix, }) => {
|
|
|
7826
8652
|
const value = watch(colLabel);
|
|
7827
8653
|
const formI18n = useFormI18n(column, prefix, schema);
|
|
7828
8654
|
return (jsxRuntime.jsxs(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
7829
|
-
gridRow, children: [jsxRuntime.jsx(react.Text, { children: value ?
|
|
8655
|
+
gridRow, children: [jsxRuntime.jsx(react.Text, { children: value ? 'True' : 'False' }), errors[`${column}`] && (jsxRuntime.jsx(react.Text, { color: 'red.400', children: formI18n.required() }))] }));
|
|
7830
8656
|
};
|
|
7831
8657
|
|
|
7832
8658
|
const CustomViewer = ({ column, schema, prefix }) => {
|
|
@@ -7858,23 +8684,22 @@ const DateViewer = ({ column, schema, prefix }) => {
|
|
|
7858
8684
|
|
|
7859
8685
|
const EnumViewer = ({ column, isMultiple = false, schema, prefix, }) => {
|
|
7860
8686
|
const { watch, formState: { errors }, } = reactHookForm.useFormContext();
|
|
7861
|
-
const formI18n = useFormI18n(column, prefix);
|
|
8687
|
+
const formI18n = useFormI18n(column, prefix, schema);
|
|
7862
8688
|
const { required } = schema;
|
|
7863
8689
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
7864
|
-
const { gridColumn =
|
|
8690
|
+
const { gridColumn = 'span 12', gridRow = 'span 1', renderDisplay } = schema;
|
|
7865
8691
|
const colLabel = formI18n.colLabel;
|
|
7866
8692
|
const watchEnum = watch(colLabel);
|
|
7867
8693
|
const watchEnums = (watch(colLabel) ?? []);
|
|
7868
|
-
|
|
7869
|
-
|
|
8694
|
+
const renderDisplayFunction = renderDisplay || defaultRenderDisplay;
|
|
8695
|
+
return (jsxRuntime.jsxs(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
8696
|
+
gridRow, children: [isMultiple && (jsxRuntime.jsx(react.Flex, { flexFlow: 'wrap', gap: 1, children: watchEnums.map((enumValue) => {
|
|
7870
8697
|
const item = enumValue;
|
|
7871
8698
|
if (item === undefined) {
|
|
7872
8699
|
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: "undefined" });
|
|
7873
8700
|
}
|
|
7874
|
-
return (jsxRuntime.jsx(Tag, { size: "lg", children:
|
|
7875
|
-
|
|
7876
|
-
: formI18n.t(item) }, item));
|
|
7877
|
-
}) })), !isMultiple && jsxRuntime.jsx(react.Text, { children: formI18n.t(watchEnum) }), errors[`${column}`] && (jsxRuntime.jsx(react.Text, { color: "red.400", children: formI18n.required() }))] }));
|
|
8701
|
+
return (jsxRuntime.jsx(Tag, { size: "lg", children: renderDisplayFunction(item) }, item));
|
|
8702
|
+
}) })), !isMultiple && jsxRuntime.jsx(react.Text, { children: renderDisplayFunction(watchEnum) }), errors[`${column}`] && (jsxRuntime.jsx(react.Text, { color: 'red.400', children: formI18n.required() }))] }));
|
|
7878
8703
|
};
|
|
7879
8704
|
|
|
7880
8705
|
const FileViewer = ({ column, schema, prefix }) => {
|
|
@@ -7994,31 +8819,35 @@ const StringViewer = ({ column, schema, prefix, }) => {
|
|
|
7994
8819
|
|
|
7995
8820
|
const TagViewer = ({ column, schema, prefix }) => {
|
|
7996
8821
|
const { watch, formState: { errors }, setValue, } = reactHookForm.useFormContext();
|
|
7997
|
-
const { serverUrl } = useSchemaContext();
|
|
7998
8822
|
if (schema.properties == undefined) {
|
|
7999
|
-
throw new Error(
|
|
8823
|
+
throw new Error('schema properties undefined when using DatePicker');
|
|
8000
8824
|
}
|
|
8001
|
-
const { gridColumn, gridRow, in_table, object_id_column } = schema;
|
|
8825
|
+
const { gridColumn, gridRow, in_table, object_id_column, tagPicker } = schema;
|
|
8002
8826
|
if (in_table === undefined) {
|
|
8003
|
-
throw new Error(
|
|
8827
|
+
throw new Error('in_table is undefined when using TagPicker');
|
|
8004
8828
|
}
|
|
8005
8829
|
if (object_id_column === undefined) {
|
|
8006
|
-
throw new Error(
|
|
8830
|
+
throw new Error('object_id_column is undefined when using TagPicker');
|
|
8831
|
+
}
|
|
8832
|
+
if (!tagPicker?.queryFn) {
|
|
8833
|
+
throw new Error('tagPicker.queryFn is required in schema. serverUrl has been removed.');
|
|
8007
8834
|
}
|
|
8008
8835
|
const query = reactQuery.useQuery({
|
|
8009
8836
|
queryKey: [`tagpicker`, in_table],
|
|
8010
8837
|
queryFn: async () => {
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
in_table: "tables_tags_view",
|
|
8838
|
+
const result = await tagPicker.queryFn({
|
|
8839
|
+
in_table: 'tables_tags_view',
|
|
8014
8840
|
where: [
|
|
8015
8841
|
{
|
|
8016
|
-
id:
|
|
8842
|
+
id: 'table_name',
|
|
8017
8843
|
value: [in_table],
|
|
8018
8844
|
},
|
|
8019
8845
|
],
|
|
8020
8846
|
limit: 100,
|
|
8847
|
+
offset: 0,
|
|
8848
|
+
searching: '',
|
|
8021
8849
|
});
|
|
8850
|
+
return result.data || { data: [] };
|
|
8022
8851
|
},
|
|
8023
8852
|
staleTime: 10000,
|
|
8024
8853
|
});
|
|
@@ -8026,17 +8855,19 @@ const TagViewer = ({ column, schema, prefix }) => {
|
|
|
8026
8855
|
const existingTagsQuery = reactQuery.useQuery({
|
|
8027
8856
|
queryKey: [`existing`, { in_table, object_id_column }, object_id],
|
|
8028
8857
|
queryFn: async () => {
|
|
8029
|
-
|
|
8030
|
-
serverUrl,
|
|
8858
|
+
const result = await tagPicker.queryFn({
|
|
8031
8859
|
in_table: in_table,
|
|
8032
8860
|
where: [
|
|
8033
8861
|
{
|
|
8034
8862
|
id: object_id_column,
|
|
8035
|
-
value: object_id[0],
|
|
8863
|
+
value: [object_id[0]],
|
|
8036
8864
|
},
|
|
8037
8865
|
],
|
|
8038
8866
|
limit: 100,
|
|
8867
|
+
offset: 0,
|
|
8868
|
+
searching: '',
|
|
8039
8869
|
});
|
|
8870
|
+
return result.data || { data: [] };
|
|
8040
8871
|
},
|
|
8041
8872
|
enabled: object_id != undefined,
|
|
8042
8873
|
staleTime: 10000,
|
|
@@ -8047,9 +8878,9 @@ const TagViewer = ({ column, schema, prefix }) => {
|
|
|
8047
8878
|
if (!!object_id === false) {
|
|
8048
8879
|
return jsxRuntime.jsx(jsxRuntime.Fragment, {});
|
|
8049
8880
|
}
|
|
8050
|
-
return (jsxRuntime.jsxs(react.Flex, { flexFlow:
|
|
8881
|
+
return (jsxRuntime.jsxs(react.Flex, { flexFlow: 'column', gap: 4, gridColumn,
|
|
8051
8882
|
gridRow, children: [isFetching && jsxRuntime.jsx(jsxRuntime.Fragment, { children: "isFetching" }), isLoading && jsxRuntime.jsx(jsxRuntime.Fragment, { children: "isLoading" }), isPending && jsxRuntime.jsx(jsxRuntime.Fragment, { children: "isPending" }), isError && jsxRuntime.jsx(jsxRuntime.Fragment, { children: "isError" }), dataList.map(({ parent_tag_name, all_tags, is_mutually_exclusive }) => {
|
|
8052
|
-
return (jsxRuntime.jsxs(react.Flex, { flexFlow:
|
|
8883
|
+
return (jsxRuntime.jsxs(react.Flex, { flexFlow: 'column', gap: 2, children: [jsxRuntime.jsx(react.Text, { children: parent_tag_name }), is_mutually_exclusive && (jsxRuntime.jsx(RadioCardRoot, { defaultValue: "next", variant: 'surface', onValueChange: (tagIds) => {
|
|
8053
8884
|
const existedTags = Object.values(all_tags)
|
|
8054
8885
|
.filter(({ id }) => {
|
|
8055
8886
|
return existingTagList.some(({ tag_id }) => tag_id === id);
|
|
@@ -8061,20 +8892,20 @@ const TagViewer = ({ column, schema, prefix }) => {
|
|
|
8061
8892
|
tagIds.value,
|
|
8062
8893
|
]);
|
|
8063
8894
|
setValue(`${column}.${parent_tag_name}.old`, existedTags);
|
|
8064
|
-
}, children: jsxRuntime.jsx(react.Flex, { flexFlow:
|
|
8895
|
+
}, children: jsxRuntime.jsx(react.Flex, { flexFlow: 'wrap', gap: 2, children: Object.entries(all_tags).map(([tagName, { id }]) => {
|
|
8065
8896
|
if (existingTagList.some(({ tag_id }) => tag_id === id)) {
|
|
8066
|
-
return (jsxRuntime.jsx(RadioCardItem, { label: tagName, value: id, flex:
|
|
8897
|
+
return (jsxRuntime.jsx(RadioCardItem, { label: tagName, value: id, flex: '0 0 0%', disabled: true }, `${tagName}-${id}`));
|
|
8067
8898
|
}
|
|
8068
|
-
return (jsxRuntime.jsx(RadioCardItem, { label: tagName, value: id, flex:
|
|
8899
|
+
return (jsxRuntime.jsx(RadioCardItem, { label: tagName, value: id, flex: '0 0 0%', colorPalette: 'blue' }, `${tagName}-${id}`));
|
|
8069
8900
|
}) }) })), !is_mutually_exclusive && (jsxRuntime.jsx(react.CheckboxGroup, { onValueChange: (tagIds) => {
|
|
8070
8901
|
setValue(`${column}.${parent_tag_name}.current`, tagIds);
|
|
8071
|
-
}, children: jsxRuntime.jsx(react.Flex, { flexFlow:
|
|
8902
|
+
}, children: jsxRuntime.jsx(react.Flex, { flexFlow: 'wrap', gap: 2, children: Object.entries(all_tags).map(([tagName, { id }]) => {
|
|
8072
8903
|
if (existingTagList.some(({ tag_id }) => tag_id === id)) {
|
|
8073
|
-
return (jsxRuntime.jsx(CheckboxCard, { label: tagName, value: id, flex:
|
|
8904
|
+
return (jsxRuntime.jsx(CheckboxCard, { label: tagName, value: id, flex: '0 0 0%', disabled: true, colorPalette: 'blue' }, `${tagName}-${id}`));
|
|
8074
8905
|
}
|
|
8075
|
-
return (jsxRuntime.jsx(CheckboxCard, { label: tagName, value: id, flex:
|
|
8906
|
+
return (jsxRuntime.jsx(CheckboxCard, { label: tagName, value: id, flex: '0 0 0%' }, `${tagName}-${id}`));
|
|
8076
8907
|
}) }) }))] }, `tag-${parent_tag_name}`));
|
|
8077
|
-
}), errors[`${column}`] && (jsxRuntime.jsx(react.Text, { color:
|
|
8908
|
+
}), errors[`${column}`] && (jsxRuntime.jsx(react.Text, { color: 'red.400', children: (errors[`${column}`]?.message ?? 'No error message') }))] }));
|
|
8078
8909
|
};
|
|
8079
8910
|
|
|
8080
8911
|
const TextAreaViewer = ({ column, schema, prefix, }) => {
|
|
@@ -8281,6 +9112,17 @@ const FormBody = () => {
|
|
|
8281
9112
|
|
|
8282
9113
|
const FormTitle = () => {
|
|
8283
9114
|
const { schema } = useSchemaContext();
|
|
9115
|
+
// Debug log when form title is missing
|
|
9116
|
+
if (!schema.title) {
|
|
9117
|
+
console.debug('[Form Title] Missing title in root schema. Add title property to schema.', {
|
|
9118
|
+
schema: {
|
|
9119
|
+
type: schema.type,
|
|
9120
|
+
properties: schema.properties
|
|
9121
|
+
? Object.keys(schema.properties)
|
|
9122
|
+
: undefined,
|
|
9123
|
+
},
|
|
9124
|
+
});
|
|
9125
|
+
}
|
|
8284
9126
|
return jsxRuntime.jsx(react.Heading, { children: schema.title ?? 'Form' });
|
|
8285
9127
|
};
|
|
8286
9128
|
|
|
@@ -9449,6 +10291,7 @@ exports.CardHeader = CardHeader;
|
|
|
9449
10291
|
exports.DataDisplay = DataDisplay;
|
|
9450
10292
|
exports.DataTable = DataTable;
|
|
9451
10293
|
exports.DataTableServer = DataTableServer;
|
|
10294
|
+
exports.DatePickerContext = DatePickerContext;
|
|
9452
10295
|
exports.DatePickerInput = DatePickerInput;
|
|
9453
10296
|
exports.DefaultCardTitle = DefaultCardTitle;
|
|
9454
10297
|
exports.DefaultForm = DefaultForm;
|