@bsol-oss/react-datatable5 13.0.1-beta.0 → 13.0.1-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +71 -21
- package/dist/index.js +2119 -1250
- package/dist/index.mjs +2122 -1254
- package/dist/types/components/DatePicker/Calendar.d.ts +2 -0
- package/dist/types/components/DatePicker/DatePicker.d.ts +27 -0
- package/dist/types/components/DatePicker/DateTimePicker.d.ts +20 -4
- package/dist/types/components/DatePicker/UniversalPicker.d.ts +2 -3
- package/dist/types/components/DatePicker/index.d.ts +1 -2
- package/dist/types/components/Form/SchemaFormContext.d.ts +0 -1
- package/dist/types/components/Form/components/core/FormRoot.d.ts +1 -2
- package/dist/types/components/Form/components/fields/FilePicker.d.ts +1 -1
- package/dist/types/components/Form/components/fields/TagPicker.d.ts +1 -1
- package/dist/types/components/Form/components/fields/useIdPickerData.d.ts +8 -2
- package/dist/types/components/Form/components/types/CustomJSONSchema7.d.ts +43 -0
- package/dist/types/components/Form/components/viewers/EnumViewer.d.ts +1 -1
- package/dist/types/components/Form/components/viewers/TagViewer.d.ts +1 -1
- package/dist/types/components/Form/utils/getTableData.d.ts +1 -2
- package/dist/types/components/Form/utils/useFormI18n.d.ts +14 -30
- package/dist/types/components/TimePicker/TimePicker.d.ts +39 -10
- package/dist/types/index.d.ts +0 -1
- package/package.json +7 -4
- package/dist/types/components/DatePicker/DatePickerInput.d.ts +0 -18
- package/dist/types/components/DatePicker/IsoTimePicker.d.ts +0 -24
package/dist/index.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,43 +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
6012
|
const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
|
|
5655
6013
|
const { watch, getValues, formState: { errors }, setValue, } = reactHookForm.useFormContext();
|
|
5656
|
-
const {
|
|
5657
|
-
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
|
+
}
|
|
5658
6021
|
const { table, column: column_ref, customQueryFn, } = foreign_key;
|
|
5659
6022
|
const [searchText, setSearchText] = React.useState('');
|
|
5660
6023
|
const [debouncedSearchText, setDebouncedSearchText] = React.useState('');
|
|
@@ -5699,94 +6062,58 @@ const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
|
|
|
5699
6062
|
const missingIdsKey = React.useMemo(() => {
|
|
5700
6063
|
return JSON.stringify([...missingIds].sort());
|
|
5701
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]);
|
|
5702
6072
|
// Query to fetch initial values that are missing from idMap
|
|
5703
6073
|
// This query runs automatically when missingIds.length > 0 and updates idMap
|
|
5704
6074
|
const initialValuesQuery = reactQuery.useQuery({
|
|
5705
|
-
queryKey: [`idpicker-initial`, column, missingIdsKey],
|
|
6075
|
+
queryKey: [`idpicker-initial`, column, missingIdsKey, idMapStateKey],
|
|
5706
6076
|
queryFn: async () => {
|
|
5707
6077
|
if (missingIds.length === 0) {
|
|
5708
6078
|
return { data: [], count: 0 };
|
|
5709
6079
|
}
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
offset: 0,
|
|
5715
|
-
where: [
|
|
5716
|
-
{
|
|
5717
|
-
id: column_ref,
|
|
5718
|
-
value: missingIds.length === 1 ? missingIds[0] : missingIds,
|
|
5719
|
-
},
|
|
5720
|
-
],
|
|
5721
|
-
});
|
|
5722
|
-
setIdMap((state) => {
|
|
5723
|
-
return { ...state, ...idMap };
|
|
5724
|
-
});
|
|
5725
|
-
return data;
|
|
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 };
|
|
5726
6084
|
}
|
|
5727
|
-
const
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
limit: missingIds.length,
|
|
5732
|
-
offset: 0,
|
|
5733
|
-
where: [
|
|
5734
|
-
{
|
|
5735
|
-
id: column_ref,
|
|
5736
|
-
value: missingIds.length === 1 ? missingIds[0] : missingIds,
|
|
5737
|
-
},
|
|
5738
|
-
],
|
|
6085
|
+
const result = await loadInitialValues({
|
|
6086
|
+
ids: missingIds,
|
|
6087
|
+
foreign_key: foreign_key,
|
|
6088
|
+
setIdMap,
|
|
5739
6089
|
});
|
|
5740
|
-
|
|
5741
|
-
return [
|
|
5742
|
-
item[column_ref],
|
|
5743
|
-
{
|
|
5744
|
-
...item,
|
|
5745
|
-
},
|
|
5746
|
-
];
|
|
5747
|
-
}));
|
|
5748
|
-
setIdMap((state) => {
|
|
5749
|
-
return { ...state, ...newMap };
|
|
5750
|
-
});
|
|
5751
|
-
return data;
|
|
6090
|
+
return result.data;
|
|
5752
6091
|
},
|
|
5753
6092
|
enabled: missingIds.length > 0, // Only fetch if there are missing IDs
|
|
5754
|
-
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
|
|
5755
6096
|
});
|
|
5756
6097
|
const { isLoading: isLoadingInitialValues, isFetching: isFetchingInitialValues, } = initialValuesQuery;
|
|
5757
6098
|
// Query for search results (async loading)
|
|
5758
6099
|
const query = reactQuery.useQuery({
|
|
5759
6100
|
queryKey: [`idpicker`, { column, searchText: debouncedSearchText, limit }],
|
|
5760
6101
|
queryFn: async () => {
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
limit: limit,
|
|
5765
|
-
offset: 0,
|
|
5766
|
-
});
|
|
5767
|
-
setIdMap((state) => {
|
|
5768
|
-
return { ...state, ...idMap };
|
|
5769
|
-
});
|
|
5770
|
-
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.`);
|
|
5771
6105
|
}
|
|
5772
|
-
const data = await
|
|
5773
|
-
serverUrl,
|
|
6106
|
+
const { data, idMap } = await customQueryFn({
|
|
5774
6107
|
searching: debouncedSearchText ?? '',
|
|
5775
|
-
in_table: table,
|
|
5776
6108
|
limit: limit,
|
|
5777
6109
|
offset: 0,
|
|
5778
6110
|
});
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
{
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
];
|
|
5786
|
-
}));
|
|
5787
|
-
setIdMap((state) => {
|
|
5788
|
-
return { ...state, ...newMap };
|
|
5789
|
-
});
|
|
6111
|
+
// Update idMap with returned values
|
|
6112
|
+
if (idMap && Object.keys(idMap).length > 0) {
|
|
6113
|
+
setIdMap((state) => {
|
|
6114
|
+
return { ...state, ...idMap };
|
|
6115
|
+
});
|
|
6116
|
+
}
|
|
5790
6117
|
return data;
|
|
5791
6118
|
},
|
|
5792
6119
|
enabled: true, // Always enabled for combobox
|
|
@@ -5818,17 +6145,51 @@ const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
|
|
|
5818
6145
|
// Depend on idMapKey which only changes when items we care about change
|
|
5819
6146
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5820
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
|
+
};
|
|
5821
6167
|
// Transform data for combobox collection
|
|
5822
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)
|
|
5823
6170
|
// raw item is stored for custom rendering
|
|
5824
6171
|
// Also include items from idMap that match currentValue (for initial values display)
|
|
5825
6172
|
const comboboxItems = React.useMemo(() => {
|
|
5826
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
|
+
};
|
|
5827
6186
|
const itemsFromDataList = dataList.map((item) => {
|
|
5828
6187
|
const rendered = renderFn(item);
|
|
6188
|
+
const label = typeof rendered === 'string' ? rendered : JSON.stringify(item); // Use string for filtering
|
|
5829
6189
|
return {
|
|
5830
|
-
label
|
|
5831
|
-
|
|
6190
|
+
label, // Use string for filtering
|
|
6191
|
+
displayLabel: getDisplayString(rendered, label), // String representation for input display
|
|
6192
|
+
value: itemToValueFn(item),
|
|
5832
6193
|
raw: item,
|
|
5833
6194
|
};
|
|
5834
6195
|
});
|
|
@@ -5837,25 +6198,28 @@ const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
|
|
|
5837
6198
|
const itemsFromIdMap = idMapItems
|
|
5838
6199
|
.map((item) => {
|
|
5839
6200
|
// Check if this item is already in itemsFromDataList
|
|
5840
|
-
const alreadyIncluded = itemsFromDataList.some((i) => i.value ===
|
|
6201
|
+
const alreadyIncluded = itemsFromDataList.some((i) => i.value === itemToValueFn(item));
|
|
5841
6202
|
if (alreadyIncluded)
|
|
5842
6203
|
return null;
|
|
5843
6204
|
const rendered = renderFn(item);
|
|
6205
|
+
const label = typeof rendered === 'string' ? rendered : JSON.stringify(item);
|
|
5844
6206
|
return {
|
|
5845
|
-
label
|
|
5846
|
-
|
|
6207
|
+
label,
|
|
6208
|
+
displayLabel: getDisplayString(rendered, label), // String representation for input display
|
|
6209
|
+
value: itemToValueFn(item),
|
|
5847
6210
|
raw: item,
|
|
5848
6211
|
};
|
|
5849
6212
|
})
|
|
5850
6213
|
.filter((item) => item !== null);
|
|
5851
6214
|
return [...itemsFromIdMap, ...itemsFromDataList];
|
|
5852
|
-
}, [dataList, column_ref, renderDisplay, idMapItems]);
|
|
6215
|
+
}, [dataList, column_ref, renderDisplay, idMapItems, itemToValueFn]);
|
|
5853
6216
|
// Use filter hook for combobox
|
|
5854
6217
|
const { contains } = react.useFilter({ sensitivity: 'base' });
|
|
5855
6218
|
// Create collection for combobox
|
|
6219
|
+
// itemToString uses displayLabel to show rendered display in input when selected
|
|
5856
6220
|
const { collection, filter, set } = react.useListCollection({
|
|
5857
6221
|
initialItems: comboboxItems,
|
|
5858
|
-
itemToString: (item) => item.
|
|
6222
|
+
itemToString: (item) => item.displayLabel, // Use displayLabel for selected value display
|
|
5859
6223
|
itemToValue: (item) => item.value,
|
|
5860
6224
|
filter: contains,
|
|
5861
6225
|
});
|
|
@@ -5910,6 +6274,10 @@ const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
|
|
|
5910
6274
|
idPickerLabels,
|
|
5911
6275
|
insideDialog: insideDialog ?? false,
|
|
5912
6276
|
renderDisplay,
|
|
6277
|
+
itemToValue: itemToValueFn,
|
|
6278
|
+
itemToString: itemToStringFn,
|
|
6279
|
+
loadInitialValues: loadInitialValues ??
|
|
6280
|
+
(async () => ({ data: { data: [], count: 0 }, idMap: {} })), // Fallback if not provided
|
|
5913
6281
|
column_ref,
|
|
5914
6282
|
errors,
|
|
5915
6283
|
setValue,
|
|
@@ -5918,60 +6286,69 @@ const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
|
|
|
5918
6286
|
|
|
5919
6287
|
const IdPickerSingle = ({ column, schema, prefix, }) => {
|
|
5920
6288
|
const formI18n = useFormI18n(column, prefix, schema);
|
|
5921
|
-
const { required, gridColumn = 'span 12', gridRow = 'span 1'
|
|
6289
|
+
const { required, gridColumn = 'span 12', gridRow = 'span 1' } = schema;
|
|
5922
6290
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
5923
|
-
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({
|
|
5924
6292
|
column,
|
|
5925
6293
|
schema,
|
|
5926
6294
|
prefix,
|
|
5927
6295
|
isMultiple: false,
|
|
5928
6296
|
});
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
const
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
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 ? (
|
|
5956
6342
|
// Show skeleton items to prevent UI shift
|
|
5957
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
|
|
5958
6344
|
? idPickerLabels?.emptySearchResult ?? 'No results found'
|
|
5959
6345
|
: idPickerLabels?.initialResults ??
|
|
5960
|
-
'Start typing to search' })) : (jsxRuntime.jsx(jsxRuntime.Fragment, { children: collection.items.map((item, index) => (jsxRuntime.
|
|
5961
|
-
? renderDisplayFunction(item.raw)
|
|
5962
|
-
: 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 ? (
|
|
5963
|
-
// Show skeleton items to prevent UI shift
|
|
5964
|
-
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
|
|
5965
|
-
? idPickerLabels?.emptySearchResult ?? 'No results found'
|
|
5966
|
-
: idPickerLabels?.initialResults ??
|
|
5967
|
-
'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
|
|
5968
|
-
? renderDisplayFunction(item.raw)
|
|
5969
|
-
: 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}`))) })) }) }) }))] }) }));
|
|
5970
6347
|
};
|
|
5971
6348
|
|
|
5972
6349
|
const IdPickerMultiple = ({ column, schema, prefix, }) => {
|
|
5973
6350
|
const formI18n = useFormI18n(column, prefix, schema);
|
|
5974
|
-
const { required, gridColumn = 'span 12', gridRow = 'span 1'
|
|
6351
|
+
const { required, gridColumn = 'span 12', gridRow = 'span 1' } = schema;
|
|
5975
6352
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
5976
6353
|
const { colLabel, currentValue, searchText, setSearchText, isLoading, isFetching, isPending, isError, isSearching, isLoadingInitialValues, isFetchingInitialValues, missingIds, collection, idMap, idPickerLabels, insideDialog, renderDisplay: renderDisplayFn, errors, setValue, } = useIdPickerData({
|
|
5977
6354
|
column,
|
|
@@ -5985,7 +6362,8 @@ const IdPickerMultiple = ({ column, schema, prefix, }) => {
|
|
|
5985
6362
|
const handleValueChange = (details) => {
|
|
5986
6363
|
setValue(colLabel, details.value);
|
|
5987
6364
|
};
|
|
5988
|
-
|
|
6365
|
+
// Use renderDisplay from hook (which comes from schema) or fallback to default
|
|
6366
|
+
const renderDisplayFunction = renderDisplayFn || defaultRenderDisplay;
|
|
5989
6367
|
return (jsxRuntime.jsxs(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
5990
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) => {
|
|
5991
6369
|
const item = idMap[id];
|
|
@@ -6010,16 +6388,12 @@ const IdPickerMultiple = ({ column, schema, prefix, }) => {
|
|
|
6010
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
|
|
6011
6389
|
? idPickerLabels?.emptySearchResult ?? 'No results found'
|
|
6012
6390
|
: idPickerLabels?.initialResults ??
|
|
6013
|
-
'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.
|
|
6014
|
-
? renderDisplayFunction(item.raw)
|
|
6015
|
-
: 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 ? (
|
|
6016
6392
|
// Show skeleton items to prevent UI shift
|
|
6017
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
|
|
6018
6394
|
? idPickerLabels?.emptySearchResult ?? 'No results found'
|
|
6019
6395
|
: idPickerLabels?.initialResults ??
|
|
6020
|
-
'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.
|
|
6021
|
-
? renderDisplayFunction(item.raw)
|
|
6022
|
-
: 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}`))) })) }) }) }))] })] }));
|
|
6023
6397
|
};
|
|
6024
6398
|
|
|
6025
6399
|
const NumberInputRoot = React__namespace.forwardRef(function NumberInput(props, ref) {
|
|
@@ -6203,31 +6577,35 @@ react.RadioCard.ItemIndicator;
|
|
|
6203
6577
|
|
|
6204
6578
|
const TagPicker = ({ column, schema, prefix }) => {
|
|
6205
6579
|
const { watch, formState: { errors }, setValue, } = reactHookForm.useFormContext();
|
|
6206
|
-
const { serverUrl } = useSchemaContext();
|
|
6207
6580
|
if (schema.properties == undefined) {
|
|
6208
|
-
throw new Error(
|
|
6581
|
+
throw new Error('schema properties undefined when using DatePicker');
|
|
6209
6582
|
}
|
|
6210
|
-
const { gridColumn, gridRow, in_table, object_id_column } = schema;
|
|
6583
|
+
const { gridColumn, gridRow, in_table, object_id_column, tagPicker } = schema;
|
|
6211
6584
|
if (in_table === undefined) {
|
|
6212
|
-
throw new Error(
|
|
6585
|
+
throw new Error('in_table is undefined when using TagPicker');
|
|
6213
6586
|
}
|
|
6214
6587
|
if (object_id_column === undefined) {
|
|
6215
|
-
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.');
|
|
6216
6592
|
}
|
|
6217
6593
|
const query = reactQuery.useQuery({
|
|
6218
6594
|
queryKey: [`tagpicker`, in_table],
|
|
6219
6595
|
queryFn: async () => {
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
in_table: "tables_tags_view",
|
|
6596
|
+
const result = await tagPicker.queryFn({
|
|
6597
|
+
in_table: 'tables_tags_view',
|
|
6223
6598
|
where: [
|
|
6224
6599
|
{
|
|
6225
|
-
id:
|
|
6600
|
+
id: 'table_name',
|
|
6226
6601
|
value: [in_table],
|
|
6227
6602
|
},
|
|
6228
6603
|
],
|
|
6229
6604
|
limit: 100,
|
|
6605
|
+
offset: 0,
|
|
6606
|
+
searching: '',
|
|
6230
6607
|
});
|
|
6608
|
+
return result.data || { data: [] };
|
|
6231
6609
|
},
|
|
6232
6610
|
staleTime: 10000,
|
|
6233
6611
|
});
|
|
@@ -6235,17 +6613,19 @@ const TagPicker = ({ column, schema, prefix }) => {
|
|
|
6235
6613
|
const existingTagsQuery = reactQuery.useQuery({
|
|
6236
6614
|
queryKey: [`existing`, { in_table, object_id_column }, object_id],
|
|
6237
6615
|
queryFn: async () => {
|
|
6238
|
-
|
|
6239
|
-
serverUrl,
|
|
6616
|
+
const result = await tagPicker.queryFn({
|
|
6240
6617
|
in_table: in_table,
|
|
6241
6618
|
where: [
|
|
6242
6619
|
{
|
|
6243
6620
|
id: object_id_column,
|
|
6244
|
-
value: object_id[0],
|
|
6621
|
+
value: [object_id[0]],
|
|
6245
6622
|
},
|
|
6246
6623
|
],
|
|
6247
6624
|
limit: 100,
|
|
6625
|
+
offset: 0,
|
|
6626
|
+
searching: '',
|
|
6248
6627
|
});
|
|
6628
|
+
return result.data || { data: [] };
|
|
6249
6629
|
},
|
|
6250
6630
|
enabled: object_id != undefined,
|
|
6251
6631
|
staleTime: 10000,
|
|
@@ -6256,9 +6636,9 @@ const TagPicker = ({ column, schema, prefix }) => {
|
|
|
6256
6636
|
if (!!object_id === false) {
|
|
6257
6637
|
return jsxRuntime.jsx(jsxRuntime.Fragment, {});
|
|
6258
6638
|
}
|
|
6259
|
-
return (jsxRuntime.jsxs(react.Flex, { flexFlow:
|
|
6639
|
+
return (jsxRuntime.jsxs(react.Flex, { flexFlow: 'column', gap: 4, gridColumn,
|
|
6260
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 }) => {
|
|
6261
|
-
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) => {
|
|
6262
6642
|
const existedTags = Object.values(all_tags)
|
|
6263
6643
|
.filter(({ id }) => {
|
|
6264
6644
|
return existingTagList.some(({ tag_id }) => tag_id === id);
|
|
@@ -6270,20 +6650,20 @@ const TagPicker = ({ column, schema, prefix }) => {
|
|
|
6270
6650
|
tagIds.value,
|
|
6271
6651
|
]);
|
|
6272
6652
|
setValue(`${column}.${parent_tag_name}.old`, existedTags);
|
|
6273
|
-
}, children: jsxRuntime.jsx(react.Flex, { flexFlow:
|
|
6653
|
+
}, children: jsxRuntime.jsx(react.Flex, { flexFlow: 'wrap', gap: 2, children: Object.entries(all_tags).map(([tagName, { id }]) => {
|
|
6274
6654
|
if (existingTagList.some(({ tag_id }) => tag_id === id)) {
|
|
6275
|
-
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}`));
|
|
6276
6656
|
}
|
|
6277
|
-
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}`));
|
|
6278
6658
|
}) }) })), !is_mutually_exclusive && (jsxRuntime.jsx(react.CheckboxGroup, { onValueChange: (tagIds) => {
|
|
6279
6659
|
setValue(`${column}.${parent_tag_name}.current`, tagIds);
|
|
6280
|
-
}, children: jsxRuntime.jsx(react.Flex, { flexFlow:
|
|
6660
|
+
}, children: jsxRuntime.jsx(react.Flex, { flexFlow: 'wrap', gap: 2, children: Object.entries(all_tags).map(([tagName, { id }]) => {
|
|
6281
6661
|
if (existingTagList.some(({ tag_id }) => tag_id === id)) {
|
|
6282
|
-
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}`));
|
|
6283
6663
|
}
|
|
6284
|
-
return (jsxRuntime.jsx(CheckboxCard, { label: tagName, value: id, flex:
|
|
6664
|
+
return (jsxRuntime.jsx(CheckboxCard, { label: tagName, value: id, flex: '0 0 0%' }, `${tagName}-${id}`));
|
|
6285
6665
|
}) }) }))] }, `tag-${parent_tag_name}`));
|
|
6286
|
-
}), errors[`${column}`] && (jsxRuntime.jsx(react.Text, { color:
|
|
6666
|
+
}), errors[`${column}`] && (jsxRuntime.jsx(react.Text, { color: 'red.400', children: (errors[`${column}`]?.message ?? 'No error message') }))] }));
|
|
6287
6667
|
};
|
|
6288
6668
|
|
|
6289
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) => {
|
|
@@ -6390,11 +6770,193 @@ const TextAreaInput = ({ column, schema, prefix, }) => {
|
|
|
6390
6770
|
|
|
6391
6771
|
dayjs.extend(utc);
|
|
6392
6772
|
dayjs.extend(timezone);
|
|
6393
|
-
const TimePicker$1 = (
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
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
|
|
6398
6960
|
const timeOptions = React.useMemo(() => {
|
|
6399
6961
|
const options = [];
|
|
6400
6962
|
// Get start time for comparison if provided
|
|
@@ -6405,32 +6967,25 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
|
|
|
6405
6967
|
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6406
6968
|
if (startDateObj.isValid() && selectedDateObj.isValid()) {
|
|
6407
6969
|
startDateTime = startDateObj;
|
|
6408
|
-
// Only filter if dates are the same
|
|
6409
6970
|
shouldFilterByDate =
|
|
6410
6971
|
startDateObj.format('YYYY-MM-DD') ===
|
|
6411
6972
|
selectedDateObj.format('YYYY-MM-DD');
|
|
6412
6973
|
}
|
|
6413
6974
|
}
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
for (let
|
|
6417
|
-
for (
|
|
6418
|
-
//
|
|
6419
|
-
let hour24 = h;
|
|
6420
|
-
if (mer === 'am' && h === 12)
|
|
6421
|
-
hour24 = 0;
|
|
6422
|
-
else if (mer === 'pm' && h < 12)
|
|
6423
|
-
hour24 = h + 12;
|
|
6424
|
-
// 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
|
|
6425
6980
|
if (startDateTime && selectedDate && shouldFilterByDate) {
|
|
6426
6981
|
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6427
6982
|
const optionDateTime = selectedDateObj
|
|
6428
|
-
.hour(
|
|
6983
|
+
.hour(h)
|
|
6429
6984
|
.minute(m)
|
|
6430
6985
|
.second(0)
|
|
6431
6986
|
.millisecond(0);
|
|
6432
6987
|
if (optionDateTime.isBefore(startDateTime)) {
|
|
6433
|
-
continue;
|
|
6988
|
+
continue;
|
|
6434
6989
|
}
|
|
6435
6990
|
}
|
|
6436
6991
|
// Calculate duration if startTime is provided
|
|
@@ -6438,7 +6993,7 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
|
|
|
6438
6993
|
if (startDateTime && selectedDate) {
|
|
6439
6994
|
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6440
6995
|
const optionDateTime = selectedDateObj
|
|
6441
|
-
.hour(
|
|
6996
|
+
.hour(h)
|
|
6442
6997
|
.minute(m)
|
|
6443
6998
|
.second(0)
|
|
6444
6999
|
.millisecond(0);
|
|
@@ -6463,58 +7018,204 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
|
|
|
6463
7018
|
}
|
|
6464
7019
|
}
|
|
6465
7020
|
}
|
|
6466
|
-
const
|
|
6467
|
-
const minuteDisplay = m.toString().padStart(2, '0');
|
|
6468
|
-
const timeDisplay = `${hourDisplay}:${minuteDisplay} ${mer.toUpperCase()}`;
|
|
7021
|
+
const timeDisplay = `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:00`;
|
|
6469
7022
|
options.push({
|
|
6470
7023
|
label: timeDisplay,
|
|
6471
|
-
value: `${h}:${m}
|
|
7024
|
+
value: `${h}:${m}:0`,
|
|
6472
7025
|
hour: h,
|
|
6473
7026
|
minute: m,
|
|
6474
|
-
|
|
6475
|
-
searchText: timeDisplay,
|
|
7027
|
+
second: 0,
|
|
7028
|
+
searchText: timeDisplay,
|
|
6476
7029
|
durationText,
|
|
6477
7030
|
});
|
|
6478
7031
|
}
|
|
6479
7032
|
}
|
|
6480
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
|
+
}
|
|
6481
7122
|
return options;
|
|
6482
|
-
}, [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
|
|
6483
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]);
|
|
6484
7169
|
const { collection, filter } = react.useListCollection({
|
|
6485
7170
|
initialItems: timeOptions,
|
|
6486
|
-
itemToString:
|
|
7171
|
+
itemToString: itemToString,
|
|
6487
7172
|
itemToValue: (item) => item.value,
|
|
6488
|
-
filter:
|
|
7173
|
+
filter: customTimeFilter,
|
|
6489
7174
|
});
|
|
6490
7175
|
// Get current value string for combobox
|
|
6491
7176
|
const currentValue = React.useMemo(() => {
|
|
6492
|
-
if (
|
|
6493
|
-
|
|
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}`;
|
|
6494
7188
|
}
|
|
6495
|
-
|
|
6496
|
-
}, [hour, minute, meridiem]);
|
|
7189
|
+
}, [hour, minute, second, meridiem, is24Hour]);
|
|
6497
7190
|
// Calculate duration difference
|
|
6498
7191
|
const durationDiff = React.useMemo(() => {
|
|
6499
|
-
if (!startTime ||
|
|
6500
|
-
!selectedDate ||
|
|
6501
|
-
hour === null ||
|
|
6502
|
-
minute === null ||
|
|
6503
|
-
meridiem === null) {
|
|
7192
|
+
if (!startTime || !selectedDate || hour === null || minute === null) {
|
|
6504
7193
|
return null;
|
|
6505
7194
|
}
|
|
7195
|
+
if (is24Hour) {
|
|
7196
|
+
if (second === null)
|
|
7197
|
+
return null;
|
|
7198
|
+
}
|
|
7199
|
+
else {
|
|
7200
|
+
if (meridiem === null)
|
|
7201
|
+
return null;
|
|
7202
|
+
}
|
|
6506
7203
|
const startDateObj = dayjs(startTime).tz(timezone);
|
|
6507
7204
|
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6508
|
-
// Convert
|
|
7205
|
+
// Convert to 24-hour format
|
|
6509
7206
|
let hour24 = hour;
|
|
6510
|
-
if (
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
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
|
+
}
|
|
6514
7213
|
const currentDateTime = selectedDateObj
|
|
6515
7214
|
.hour(hour24)
|
|
6516
7215
|
.minute(minute)
|
|
6517
|
-
.second(
|
|
7216
|
+
.second(is24Hour && second !== null && second !== undefined
|
|
7217
|
+
? second
|
|
7218
|
+
: 0)
|
|
6518
7219
|
.millisecond(0);
|
|
6519
7220
|
if (!startDateObj.isValid() || !currentDateTime.isValid()) {
|
|
6520
7221
|
return null;
|
|
@@ -6540,13 +7241,28 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
|
|
|
6540
7241
|
return `+${diffText}`;
|
|
6541
7242
|
}
|
|
6542
7243
|
return null;
|
|
6543
|
-
}, [
|
|
7244
|
+
}, [
|
|
7245
|
+
hour,
|
|
7246
|
+
minute,
|
|
7247
|
+
second,
|
|
7248
|
+
meridiem,
|
|
7249
|
+
startTime,
|
|
7250
|
+
selectedDate,
|
|
7251
|
+
timezone,
|
|
7252
|
+
is24Hour,
|
|
7253
|
+
]);
|
|
6544
7254
|
const handleClear = () => {
|
|
6545
7255
|
setHour(null);
|
|
6546
7256
|
setMinute(null);
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
|
|
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('');
|
|
6550
7266
|
};
|
|
6551
7267
|
const handleValueChange = (details) => {
|
|
6552
7268
|
if (details.value.length === 0) {
|
|
@@ -6558,71 +7274,165 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
|
|
|
6558
7274
|
if (selectedOption) {
|
|
6559
7275
|
setHour(selectedOption.hour);
|
|
6560
7276
|
setMinute(selectedOption.minute);
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
}
|
|
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
|
+
}
|
|
6568
7290
|
}
|
|
6569
7291
|
};
|
|
6570
7292
|
// Parse input value and update state
|
|
6571
7293
|
const parseAndCommitInput = (value) => {
|
|
6572
7294
|
const trimmedValue = value.trim();
|
|
6573
|
-
// Filter the collection based on input
|
|
6574
7295
|
filter(trimmedValue);
|
|
6575
7296
|
if (!trimmedValue) {
|
|
6576
7297
|
return;
|
|
6577
7298
|
}
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
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
|
+
}
|
|
6599
7340
|
}
|
|
6600
7341
|
}
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
6610
|
-
|
|
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();
|
|
6611
7387
|
if (parsedHour >= 1 &&
|
|
6612
7388
|
parsedHour <= 12 &&
|
|
6613
7389
|
parsedMinute >= 0 &&
|
|
6614
7390
|
parsedMinute <= 59) {
|
|
6615
7391
|
setHour(parsedHour);
|
|
6616
7392
|
setMinute(parsedMinute);
|
|
6617
|
-
setMeridiem
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
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);
|
|
6623
7411
|
return;
|
|
6624
7412
|
}
|
|
6625
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
|
+
}
|
|
6626
7436
|
}
|
|
6627
7437
|
// Parse failed, select first result
|
|
6628
7438
|
selectFirstResult();
|
|
@@ -6633,58 +7443,87 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
|
|
|
6633
7443
|
const firstItem = collection.items[0];
|
|
6634
7444
|
setHour(firstItem.hour);
|
|
6635
7445
|
setMinute(firstItem.minute);
|
|
6636
|
-
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
}
|
|
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
|
+
}
|
|
6643
7459
|
}
|
|
6644
7460
|
};
|
|
6645
7461
|
const handleInputValueChange = (details) => {
|
|
6646
|
-
|
|
7462
|
+
if (is24Hour) {
|
|
7463
|
+
setInputValue(details.inputValue);
|
|
7464
|
+
}
|
|
6647
7465
|
filter(details.inputValue);
|
|
6648
7466
|
};
|
|
6649
7467
|
const handleFocus = (e) => {
|
|
6650
|
-
// Select all text when focusing
|
|
6651
7468
|
e.target.select();
|
|
6652
7469
|
};
|
|
6653
7470
|
const handleBlur = (e) => {
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
7471
|
+
const inputVal = e.target.value;
|
|
7472
|
+
if (is24Hour) {
|
|
7473
|
+
setInputValue(inputVal);
|
|
7474
|
+
}
|
|
7475
|
+
if (inputVal) {
|
|
7476
|
+
parseAndCommitInput(inputVal);
|
|
6658
7477
|
}
|
|
6659
7478
|
};
|
|
6660
7479
|
const handleKeyDown = (e) => {
|
|
6661
|
-
// Commit input on Enter key
|
|
6662
7480
|
if (e.key === 'Enter') {
|
|
6663
7481
|
e.preventDefault();
|
|
6664
|
-
const
|
|
6665
|
-
if (
|
|
6666
|
-
|
|
7482
|
+
const inputVal = e.currentTarget.value;
|
|
7483
|
+
if (is24Hour) {
|
|
7484
|
+
setInputValue(inputVal);
|
|
7485
|
+
}
|
|
7486
|
+
if (inputVal) {
|
|
7487
|
+
parseAndCommitInput(inputVal);
|
|
6667
7488
|
}
|
|
6668
|
-
// Blur the input
|
|
6669
7489
|
e.currentTarget?.blur();
|
|
6670
7490
|
}
|
|
6671
7491
|
};
|
|
6672
|
-
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 }) }))] }) }));
|
|
6673
7493
|
};
|
|
6674
7494
|
|
|
6675
7495
|
dayjs.extend(timezone);
|
|
6676
7496
|
const TimePicker = ({ column, schema, prefix }) => {
|
|
6677
7497
|
const { watch, formState: { errors }, setValue, } = reactHookForm.useFormContext();
|
|
6678
7498
|
const { timezone, insideDialog, timePickerLabels } = useSchemaContext();
|
|
6679
|
-
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;
|
|
6680
7500
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
6681
7501
|
const colLabel = `${prefix}${column}`;
|
|
6682
7502
|
const formI18n = useFormI18n(column, prefix, schema);
|
|
6683
7503
|
const [open, setOpen] = React.useState(false);
|
|
6684
7504
|
const value = watch(colLabel);
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
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
|
+
: '';
|
|
6688
7527
|
// Parse the initial time parts from the time string (HH:mm:ssZ)
|
|
6689
7528
|
const parseTime = (time) => {
|
|
6690
7529
|
if (!time)
|
|
@@ -6737,884 +7576,898 @@ const TimePicker = ({ column, schema, prefix }) => {
|
|
|
6737
7576
|
return (jsxRuntime.jsx(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
6738
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: () => {
|
|
6739
7578
|
setOpen(true);
|
|
6740
|
-
}, 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 }) }) }) }) }))] }) }));
|
|
6741
7580
|
};
|
|
6742
7581
|
|
|
6743
7582
|
dayjs.extend(utc);
|
|
6744
7583
|
dayjs.extend(timezone);
|
|
6745
7584
|
dayjs.extend(customParseFormat);
|
|
6746
|
-
function
|
|
7585
|
+
function DateTimePicker$1({ value, onChange, format = 'date-time', showSeconds = false, labels = {
|
|
6747
7586
|
monthNamesShort: [
|
|
6748
|
-
'
|
|
6749
|
-
'
|
|
6750
|
-
'
|
|
6751
|
-
'
|
|
7587
|
+
'January',
|
|
7588
|
+
'February',
|
|
7589
|
+
'March',
|
|
7590
|
+
'April',
|
|
6752
7591
|
'May',
|
|
6753
|
-
'
|
|
6754
|
-
'
|
|
6755
|
-
'
|
|
6756
|
-
'
|
|
6757
|
-
'
|
|
6758
|
-
'
|
|
6759
|
-
'
|
|
7592
|
+
'June',
|
|
7593
|
+
'July',
|
|
7594
|
+
'August',
|
|
7595
|
+
'September',
|
|
7596
|
+
'October',
|
|
7597
|
+
'November',
|
|
7598
|
+
'December',
|
|
6760
7599
|
],
|
|
6761
7600
|
weekdayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
|
6762
7601
|
backButtonLabel: 'Back',
|
|
6763
|
-
forwardButtonLabel: '
|
|
6764
|
-
}, timezone = 'Asia/Hong_Kong', minDate, maxDate,
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
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();
|
|
6779
7635
|
}
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
const selectedDate = value
|
|
6783
|
-
? typeof value === 'string'
|
|
6784
|
-
? dayjs(value).tz(timezone).isValid()
|
|
6785
|
-
? dayjs(value).tz(timezone).toDate()
|
|
6786
|
-
: new Date()
|
|
6787
|
-
: value
|
|
6788
|
-
: new Date();
|
|
6789
|
-
// Shared function to parse and validate input value
|
|
6790
|
-
const parseAndValidateInput = (inputVal) => {
|
|
6791
|
-
// If empty, clear the value
|
|
6792
|
-
if (!inputVal.trim()) {
|
|
6793
|
-
onChange?.(undefined);
|
|
6794
|
-
setInputValue('');
|
|
6795
|
-
return;
|
|
7636
|
+
if (defaultTime?.hour !== null && defaultTime?.hour !== undefined) {
|
|
7637
|
+
return defaultTime.hour;
|
|
6796
7638
|
}
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
if (
|
|
6801
|
-
|
|
7639
|
+
return null;
|
|
7640
|
+
});
|
|
7641
|
+
const [minute, setMinute] = React.useState(() => {
|
|
7642
|
+
if (parsedValue) {
|
|
7643
|
+
return parsedValue.minute();
|
|
6802
7644
|
}
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
parsedDate = dayjs(inputVal, dateFormat, true);
|
|
7645
|
+
if (defaultTime?.minute !== null && defaultTime?.minute !== undefined) {
|
|
7646
|
+
return defaultTime.minute;
|
|
6806
7647
|
}
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
// Invalid: before minDate, reset to prop value
|
|
6813
|
-
resetToPropValue();
|
|
6814
|
-
return;
|
|
6815
|
-
}
|
|
6816
|
-
if (maxDate && dateObj > maxDate) {
|
|
6817
|
-
// Invalid: after maxDate, reset to prop value
|
|
6818
|
-
resetToPropValue();
|
|
6819
|
-
return;
|
|
6820
|
-
}
|
|
6821
|
-
// Valid date - format and update
|
|
6822
|
-
const formattedDate = parsedDate.tz(timezone).format(dateFormat);
|
|
6823
|
-
const formattedDisplay = parsedDate.tz(timezone).format(displayFormat);
|
|
6824
|
-
onChange?.(formattedDate);
|
|
6825
|
-
setInputValue(formattedDisplay);
|
|
7648
|
+
return null;
|
|
7649
|
+
});
|
|
7650
|
+
const [second, setSecond] = React.useState(() => {
|
|
7651
|
+
if (parsedValue) {
|
|
7652
|
+
return parsedValue.second();
|
|
6826
7653
|
}
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
resetToPropValue();
|
|
7654
|
+
if (defaultTime?.second !== null && defaultTime?.second !== undefined) {
|
|
7655
|
+
return defaultTime.second;
|
|
6830
7656
|
}
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
const
|
|
6834
|
-
if (
|
|
6835
|
-
const
|
|
6836
|
-
|
|
6837
|
-
? dayjs(value).tz(timezone).format(displayFormat)
|
|
6838
|
-
: ''
|
|
6839
|
-
: dayjs(value).tz(timezone).format(displayFormat);
|
|
6840
|
-
setInputValue(formatted);
|
|
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';
|
|
6841
7663
|
}
|
|
6842
|
-
|
|
6843
|
-
|
|
7664
|
+
if (defaultTime?.meridiem !== null && defaultTime?.meridiem !== undefined) {
|
|
7665
|
+
return defaultTime.meridiem;
|
|
6844
7666
|
}
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
6850
|
-
const
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
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
|
|
7688
|
+
React.useEffect(() => {
|
|
7689
|
+
if (controlledTimezoneOffset !== undefined) {
|
|
7690
|
+
setInternalTimezoneOffset(controlledTimezoneOffset);
|
|
6859
7691
|
}
|
|
6860
|
-
};
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
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 }) }) }) }))] }));
|
|
6868
|
-
}
|
|
6869
|
-
|
|
6870
|
-
dayjs.extend(utc);
|
|
6871
|
-
dayjs.extend(timezone);
|
|
6872
|
-
function IsoTimePicker({ hour, setHour, minute, setMinute, second, setSecond,
|
|
6873
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
6874
|
-
onChange = (_newValue) => { }, startTime, selectedDate, timezone = 'Asia/Hong_Kong', portalled = true, labels = {
|
|
6875
|
-
placeholder: 'HH:mm:ss',
|
|
6876
|
-
emptyMessage: 'No time found',
|
|
6877
|
-
}, }) {
|
|
6878
|
-
// Generate time options (every 15 minutes, seconds always 0)
|
|
6879
|
-
const timeOptions = React.useMemo(() => {
|
|
6880
|
-
const options = [];
|
|
6881
|
-
// Get start time for comparison if provided
|
|
6882
|
-
let startDateTime = null;
|
|
6883
|
-
let shouldFilterByDate = false;
|
|
6884
|
-
if (startTime && selectedDate) {
|
|
6885
|
-
const startDateObj = dayjs(startTime).tz(timezone);
|
|
6886
|
-
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6887
|
-
if (startDateObj.isValid() && selectedDateObj.isValid()) {
|
|
6888
|
-
startDateTime = startDateObj;
|
|
6889
|
-
// Only filter if dates are the same
|
|
6890
|
-
shouldFilterByDate =
|
|
6891
|
-
startDateObj.format('YYYY-MM-DD') ===
|
|
6892
|
-
selectedDateObj.format('YYYY-MM-DD');
|
|
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);
|
|
6893
7699
|
}
|
|
6894
7700
|
}
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
.second(0)
|
|
6907
|
-
.millisecond(0);
|
|
6908
|
-
if (optionDateTime.isBefore(startDateTime)) {
|
|
6909
|
-
continue; // Skip this option as it would result in negative duration
|
|
6910
|
-
}
|
|
6911
|
-
}
|
|
6912
|
-
// Calculate duration if startTime is provided
|
|
6913
|
-
let durationText;
|
|
6914
|
-
if (startDateTime && selectedDate) {
|
|
6915
|
-
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6916
|
-
const optionDateTime = selectedDateObj
|
|
6917
|
-
.hour(h)
|
|
6918
|
-
.minute(m)
|
|
6919
|
-
.second(0)
|
|
6920
|
-
.millisecond(0);
|
|
6921
|
-
if (optionDateTime.isValid() &&
|
|
6922
|
-
optionDateTime.isAfter(startDateTime)) {
|
|
6923
|
-
const diffMs = optionDateTime.diff(startDateTime);
|
|
6924
|
-
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
6925
|
-
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
6926
|
-
const diffSeconds = Math.floor((diffMs % (1000 * 60)) / 1000);
|
|
6927
|
-
if (diffHours > 0 || diffMinutes > 0 || diffSeconds > 0) {
|
|
6928
|
-
let diffText = '';
|
|
6929
|
-
if (diffHours > 0) {
|
|
6930
|
-
diffText = `${diffHours}h ${diffMinutes}m`;
|
|
6931
|
-
}
|
|
6932
|
-
else if (diffMinutes > 0) {
|
|
6933
|
-
diffText = `${diffMinutes}m ${diffSeconds}s`;
|
|
6934
|
-
}
|
|
6935
|
-
else {
|
|
6936
|
-
diffText = `${diffSeconds}s`;
|
|
6937
|
-
}
|
|
6938
|
-
durationText = `+${diffText}`;
|
|
6939
|
-
}
|
|
6940
|
-
}
|
|
6941
|
-
}
|
|
6942
|
-
options.push({
|
|
6943
|
-
label: timeDisplay,
|
|
6944
|
-
value: `${h}:${m}:0`,
|
|
6945
|
-
hour: h,
|
|
6946
|
-
minute: m,
|
|
6947
|
-
second: 0,
|
|
6948
|
-
searchText: timeDisplay, // Use base time without duration for searching
|
|
6949
|
-
durationText,
|
|
6950
|
-
});
|
|
6951
|
-
}
|
|
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 });
|
|
6952
7712
|
}
|
|
6953
7713
|
return options;
|
|
6954
|
-
}, [
|
|
6955
|
-
|
|
6956
|
-
const { collection
|
|
6957
|
-
initialItems:
|
|
6958
|
-
itemToString: (item) => item.
|
|
7714
|
+
}, []);
|
|
7715
|
+
// Create collection for Select
|
|
7716
|
+
const { collection: timezoneCollection } = react.useListCollection({
|
|
7717
|
+
initialItems: timezoneOffsetOptions,
|
|
7718
|
+
itemToString: (item) => item.label,
|
|
6959
7719
|
itemToValue: (item) => item.value,
|
|
6960
|
-
filter: contains,
|
|
6961
7720
|
});
|
|
6962
|
-
//
|
|
6963
|
-
const
|
|
6964
|
-
if (
|
|
6965
|
-
return
|
|
6966
|
-
|
|
6967
|
-
return
|
|
6968
|
-
}, [
|
|
6969
|
-
//
|
|
6970
|
-
const
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
return null;
|
|
6977
|
-
}
|
|
6978
|
-
const startDateObj = dayjs(startTime).tz(timezone);
|
|
6979
|
-
const selectedDateObj = dayjs(selectedDate).tz(timezone);
|
|
6980
|
-
const currentDateTime = selectedDateObj
|
|
6981
|
-
.hour(hour)
|
|
6982
|
-
.minute(minute)
|
|
6983
|
-
.second(second ?? 0)
|
|
6984
|
-
.millisecond(0);
|
|
6985
|
-
if (!startDateObj.isValid() || !currentDateTime.isValid()) {
|
|
6986
|
-
return null;
|
|
6987
|
-
}
|
|
6988
|
-
const diffMs = currentDateTime.diff(startDateObj);
|
|
6989
|
-
if (diffMs < 0) {
|
|
6990
|
-
return null;
|
|
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);
|
|
6991
7735
|
}
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
const diffSeconds = Math.floor((diffMs % (1000 * 60)) / 1000);
|
|
6995
|
-
if (diffHours > 0 || diffMinutes > 0 || diffSeconds > 0) {
|
|
6996
|
-
let diffText = '';
|
|
6997
|
-
if (diffHours > 0) {
|
|
6998
|
-
diffText = `${diffHours}h ${diffMinutes}m`;
|
|
6999
|
-
}
|
|
7000
|
-
else if (diffMinutes > 0) {
|
|
7001
|
-
diffText = `${diffMinutes}m ${diffSeconds}s`;
|
|
7002
|
-
}
|
|
7003
|
-
else {
|
|
7004
|
-
diffText = `${diffSeconds}s`;
|
|
7005
|
-
}
|
|
7006
|
-
return `+${diffText}`;
|
|
7736
|
+
else {
|
|
7737
|
+
setDateInputValue('');
|
|
7007
7738
|
}
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
const
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
onChange({ hour: null, minute: null, second: null });
|
|
7016
|
-
};
|
|
7017
|
-
const handleValueChange = (details) => {
|
|
7018
|
-
if (details.value.length === 0) {
|
|
7019
|
-
handleClear();
|
|
7739
|
+
}, [selectedDate, tz]);
|
|
7740
|
+
// Parse and validate date input
|
|
7741
|
+
const parseAndValidateDateInput = (inputVal) => {
|
|
7742
|
+
// If empty, clear the value
|
|
7743
|
+
if (!inputVal.trim()) {
|
|
7744
|
+
setSelectedDate(null);
|
|
7745
|
+
updateDateTime(null, hour, minute, second, meridiem);
|
|
7020
7746
|
return;
|
|
7021
7747
|
}
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
setSecond(selectedOption.second);
|
|
7028
|
-
filter(''); // Reset filter after selection
|
|
7029
|
-
onChange({
|
|
7030
|
-
hour: selectedOption.hour,
|
|
7031
|
-
minute: selectedOption.minute,
|
|
7032
|
-
second: selectedOption.second,
|
|
7033
|
-
});
|
|
7034
|
-
}
|
|
7035
|
-
};
|
|
7036
|
-
// Parse input value and update state
|
|
7037
|
-
const parseAndCommitInput = (value) => {
|
|
7038
|
-
const trimmedValue = value.trim();
|
|
7039
|
-
// Filter the collection based on input
|
|
7040
|
-
filter(trimmedValue);
|
|
7041
|
-
if (!trimmedValue) {
|
|
7042
|
-
return;
|
|
7748
|
+
// Try parsing with common date formats
|
|
7749
|
+
let parsedDate = dayjs(inputVal, 'YYYY-MM-DD', true);
|
|
7750
|
+
// If that fails, try other common formats
|
|
7751
|
+
if (!parsedDate.isValid()) {
|
|
7752
|
+
parsedDate = dayjs(inputVal);
|
|
7043
7753
|
}
|
|
7044
|
-
//
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
parsedSecond <= 59) {
|
|
7058
|
-
setHour(parsedHour);
|
|
7059
|
-
setMinute(parsedMinute);
|
|
7060
|
-
setSecond(parsedSecond);
|
|
7061
|
-
onChange({
|
|
7062
|
-
hour: parsedHour,
|
|
7063
|
-
minute: parsedMinute,
|
|
7064
|
-
second: parsedSecond,
|
|
7065
|
-
});
|
|
7754
|
+
// If valid, check constraints and update
|
|
7755
|
+
if (parsedDate.isValid()) {
|
|
7756
|
+
const dateObj = parsedDate.tz(tz).toDate();
|
|
7757
|
+
// Check min/max constraints
|
|
7758
|
+
if (minDate && dateObj < minDate) {
|
|
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
|
+
}
|
|
7066
7767
|
return;
|
|
7067
7768
|
}
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
// Validate ranges
|
|
7077
|
-
if (parsedHour >= 0 &&
|
|
7078
|
-
parsedHour <= 23 &&
|
|
7079
|
-
parsedMinute >= 0 &&
|
|
7080
|
-
parsedMinute <= 59 &&
|
|
7081
|
-
parsedSecond >= 0 &&
|
|
7082
|
-
parsedSecond <= 59) {
|
|
7083
|
-
setHour(parsedHour);
|
|
7084
|
-
setMinute(parsedMinute);
|
|
7085
|
-
setSecond(parsedSecond);
|
|
7086
|
-
onChange({
|
|
7087
|
-
hour: parsedHour,
|
|
7088
|
-
minute: parsedMinute,
|
|
7089
|
-
second: parsedSecond,
|
|
7090
|
-
});
|
|
7091
|
-
return;
|
|
7769
|
+
if (maxDate && dateObj > maxDate) {
|
|
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('');
|
|
7092
7777
|
}
|
|
7778
|
+
return;
|
|
7093
7779
|
}
|
|
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);
|
|
7094
7786
|
}
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
setSecond(firstItem.second);
|
|
7105
|
-
filter(''); // Reset filter after selection
|
|
7106
|
-
onChange({
|
|
7107
|
-
hour: firstItem.hour,
|
|
7108
|
-
minute: firstItem.minute,
|
|
7109
|
-
second: firstItem.second,
|
|
7110
|
-
});
|
|
7787
|
+
else {
|
|
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
|
+
}
|
|
7111
7796
|
}
|
|
7112
7797
|
};
|
|
7113
|
-
const
|
|
7114
|
-
|
|
7115
|
-
filter(details.inputValue);
|
|
7798
|
+
const handleDateInputChange = (e) => {
|
|
7799
|
+
setDateInputValue(e.target.value);
|
|
7116
7800
|
};
|
|
7117
|
-
const
|
|
7118
|
-
|
|
7119
|
-
e.target.select();
|
|
7801
|
+
const handleDateInputBlur = () => {
|
|
7802
|
+
parseAndValidateDateInput(dateInputValue);
|
|
7120
7803
|
};
|
|
7121
|
-
const
|
|
7122
|
-
// Parse and commit the input value when losing focus
|
|
7123
|
-
const inputValue = e.target.value;
|
|
7124
|
-
if (inputValue) {
|
|
7125
|
-
parseAndCommitInput(inputValue);
|
|
7126
|
-
}
|
|
7127
|
-
};
|
|
7128
|
-
const handleKeyDown = (e) => {
|
|
7129
|
-
// Commit input on Enter key
|
|
7804
|
+
const handleDateInputKeyDown = (e) => {
|
|
7130
7805
|
if (e.key === 'Enter') {
|
|
7131
7806
|
e.preventDefault();
|
|
7132
|
-
|
|
7133
|
-
if (inputValue) {
|
|
7134
|
-
parseAndCommitInput(inputValue);
|
|
7135
|
-
}
|
|
7136
|
-
// Blur the input
|
|
7137
|
-
e.currentTarget?.blur();
|
|
7807
|
+
parseAndValidateDateInput(dateInputValue);
|
|
7138
7808
|
}
|
|
7139
7809
|
};
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
dayjs.
|
|
7144
|
-
dayjs.
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
'Jun',
|
|
7153
|
-
'Jul',
|
|
7154
|
-
'Aug',
|
|
7155
|
-
'Sep',
|
|
7156
|
-
'Oct',
|
|
7157
|
-
'Nov',
|
|
7158
|
-
'Dec',
|
|
7159
|
-
],
|
|
7160
|
-
weekdayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
|
7161
|
-
backButtonLabel: 'Back',
|
|
7162
|
-
forwardButtonLabel: 'Next',
|
|
7163
|
-
}, timePickerLabels, timezone = 'Asia/Hong_Kong', startTime, minDate, maxDate, portalled = false, }) {
|
|
7164
|
-
console.log('[DateTimePicker] Component initialized with props:', {
|
|
7165
|
-
value,
|
|
7166
|
-
format,
|
|
7167
|
-
showSeconds,
|
|
7168
|
-
timezone,
|
|
7169
|
-
startTime,
|
|
7170
|
-
minDate,
|
|
7171
|
-
maxDate,
|
|
7172
|
-
});
|
|
7173
|
-
// Initialize selectedDate from value prop, converting ISO to YYYY-MM-DD format
|
|
7174
|
-
const getDateString = React.useCallback((val) => {
|
|
7175
|
-
if (!val)
|
|
7176
|
-
return '';
|
|
7177
|
-
const dateObj = dayjs(val).tz(timezone);
|
|
7178
|
-
return dateObj.isValid() ? dateObj.format('YYYY-MM-DD') : '';
|
|
7179
|
-
}, [timezone]);
|
|
7180
|
-
const [selectedDate, setSelectedDate] = React.useState(getDateString(value));
|
|
7181
|
-
// Helper to get time values from value prop with timezone
|
|
7182
|
-
const getTimeFromValue = React.useCallback((val) => {
|
|
7183
|
-
console.log('[DateTimePicker] getTimeFromValue called:', {
|
|
7184
|
-
val,
|
|
7185
|
-
timezone,
|
|
7186
|
-
showSeconds,
|
|
7187
|
-
});
|
|
7188
|
-
if (!val) {
|
|
7189
|
-
console.log('[DateTimePicker] No value provided, returning nulls');
|
|
7190
|
-
return {
|
|
7191
|
-
hour12: null,
|
|
7192
|
-
minute: null,
|
|
7193
|
-
meridiem: null,
|
|
7194
|
-
hour24: null,
|
|
7195
|
-
second: null,
|
|
7196
|
-
};
|
|
7197
|
-
}
|
|
7198
|
-
const dateObj = dayjs(val).tz(timezone);
|
|
7199
|
-
console.log('[DateTimePicker] Parsed date object:', {
|
|
7200
|
-
original: val,
|
|
7201
|
-
timezone,
|
|
7202
|
-
isValid: dateObj.isValid(),
|
|
7203
|
-
formatted: dateObj.format('YYYY-MM-DD HH:mm:ss Z'),
|
|
7204
|
-
hour24: dateObj.hour(),
|
|
7205
|
-
minute: dateObj.minute(),
|
|
7206
|
-
second: dateObj.second(),
|
|
7207
|
-
});
|
|
7208
|
-
if (!dateObj.isValid()) {
|
|
7209
|
-
console.log('[DateTimePicker] Invalid date object, returning nulls');
|
|
7210
|
-
return {
|
|
7211
|
-
hour12: null,
|
|
7212
|
-
minute: null,
|
|
7213
|
-
meridiem: null,
|
|
7214
|
-
hour24: null,
|
|
7215
|
-
second: null,
|
|
7216
|
-
};
|
|
7217
|
-
}
|
|
7218
|
-
const hour24Value = dateObj.hour();
|
|
7219
|
-
const hour12Value = hour24Value % 12 || 12;
|
|
7220
|
-
const minuteValue = dateObj.minute();
|
|
7221
|
-
const meridiemValue = hour24Value >= 12 ? 'pm' : 'am';
|
|
7222
|
-
const secondValue = showSeconds ? dateObj.second() : null;
|
|
7223
|
-
const result = {
|
|
7224
|
-
hour12: hour12Value,
|
|
7225
|
-
minute: minuteValue,
|
|
7226
|
-
meridiem: meridiemValue,
|
|
7227
|
-
hour24: hour24Value,
|
|
7228
|
-
second: secondValue,
|
|
7229
|
-
};
|
|
7230
|
-
console.log('[DateTimePicker] Extracted time values:', result);
|
|
7231
|
-
return result;
|
|
7232
|
-
}, [timezone, showSeconds]);
|
|
7233
|
-
const initialTime = getTimeFromValue(value);
|
|
7234
|
-
console.log('[DateTimePicker] Initial time from value:', {
|
|
7235
|
-
value,
|
|
7236
|
-
initialTime,
|
|
7237
|
-
});
|
|
7238
|
-
// Time state for 12-hour format
|
|
7239
|
-
const [hour12, setHour12] = React.useState(initialTime.hour12);
|
|
7240
|
-
const [minute, setMinute] = React.useState(initialTime.minute);
|
|
7241
|
-
const [meridiem, setMeridiem] = React.useState(initialTime.meridiem);
|
|
7242
|
-
// Time state for 24-hour format
|
|
7243
|
-
const [hour24, setHour24] = React.useState(initialTime.hour24);
|
|
7244
|
-
const [second, setSecond] = React.useState(initialTime.second);
|
|
7245
|
-
// Sync selectedDate and time states when value prop changes
|
|
7246
|
-
React.useEffect(() => {
|
|
7247
|
-
console.log('[DateTimePicker] useEffect triggered - value changed:', {
|
|
7248
|
-
value,
|
|
7249
|
-
timezone,
|
|
7250
|
-
format,
|
|
7251
|
-
});
|
|
7252
|
-
// If value is null, undefined, or invalid, clear all fields
|
|
7253
|
-
if (!value || value === null || value === undefined) {
|
|
7254
|
-
console.log('[DateTimePicker] Value is null/undefined, clearing all fields');
|
|
7255
|
-
setSelectedDate('');
|
|
7256
|
-
setHour12(null);
|
|
7257
|
-
setMinute(null);
|
|
7258
|
-
setMeridiem(null);
|
|
7259
|
-
setHour24(null);
|
|
7260
|
-
setSecond(null);
|
|
7261
|
-
return;
|
|
7262
|
-
}
|
|
7263
|
-
// Check if value is valid
|
|
7264
|
-
const dateObj = dayjs(value).tz(timezone);
|
|
7265
|
-
if (!dateObj.isValid()) {
|
|
7266
|
-
console.log('[DateTimePicker] Invalid value, clearing all fields');
|
|
7267
|
-
setSelectedDate('');
|
|
7268
|
-
setHour12(null);
|
|
7269
|
-
setMinute(null);
|
|
7270
|
-
setMeridiem(null);
|
|
7271
|
-
setHour24(null);
|
|
7272
|
-
setSecond(null);
|
|
7273
|
-
return;
|
|
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;
|
|
7274
7822
|
}
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
timeData,
|
|
7281
|
-
});
|
|
7282
|
-
setHour12(timeData.hour12);
|
|
7283
|
-
setMinute(timeData.minute);
|
|
7284
|
-
setMeridiem(timeData.meridiem);
|
|
7285
|
-
setHour24(timeData.hour24);
|
|
7286
|
-
setSecond(timeData.second);
|
|
7287
|
-
}, [value, getTimeFromValue, getDateString, timezone]);
|
|
7288
|
-
const handleDateChange = (date) => {
|
|
7289
|
-
console.log('[DateTimePicker] handleDateChange called:', {
|
|
7290
|
-
date,
|
|
7291
|
-
timezone,
|
|
7292
|
-
showSeconds,
|
|
7293
|
-
currentTimeStates: { hour12, minute, meridiem, hour24, second },
|
|
7294
|
-
});
|
|
7295
|
-
// If date is empty or invalid, clear all fields
|
|
7296
|
-
if (!date || date === '') {
|
|
7297
|
-
console.log('[DateTimePicker] Empty date, clearing all fields');
|
|
7298
|
-
setSelectedDate('');
|
|
7299
|
-
setHour12(null);
|
|
7300
|
-
setMinute(null);
|
|
7301
|
-
setMeridiem(null);
|
|
7302
|
-
setHour24(null);
|
|
7303
|
-
setSecond(null);
|
|
7304
|
-
onChange?.(undefined);
|
|
7305
|
-
return;
|
|
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;
|
|
7306
7828
|
}
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
});
|
|
7317
|
-
if (!dateObj.isValid()) {
|
|
7318
|
-
console.warn('[DateTimePicker] Invalid date object in handleDateChange, clearing fields');
|
|
7319
|
-
setSelectedDate('');
|
|
7320
|
-
setHour12(null);
|
|
7321
|
-
setMinute(null);
|
|
7322
|
-
setMeridiem(null);
|
|
7323
|
-
setHour24(null);
|
|
7324
|
-
setSecond(null);
|
|
7325
|
-
onChange?.(undefined);
|
|
7326
|
-
return;
|
|
7829
|
+
return true;
|
|
7830
|
+
};
|
|
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);
|
|
7327
7838
|
}
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
7839
|
+
};
|
|
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')}`;
|
|
7855
|
+
}
|
|
7856
|
+
return `${hour24.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
|
|
7333
7857
|
}
|
|
7334
7858
|
else {
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
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)
|
|
7870
|
+
return '';
|
|
7871
|
+
// Show offset as is (e.g., "+08:00")
|
|
7872
|
+
return timezoneOffset;
|
|
7873
|
+
}, [timezoneOffset, showTimezoneSelector]);
|
|
7874
|
+
// Update selectedDate when value changes externally
|
|
7875
|
+
React.useEffect(() => {
|
|
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
|
+
}
|
|
7338
7885
|
}
|
|
7339
|
-
};
|
|
7340
|
-
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
if (
|
|
7353
|
-
|
|
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) {
|
|
7890
|
+
onChange?.(undefined);
|
|
7891
|
+
return;
|
|
7892
|
+
}
|
|
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
|
+
}
|
|
7354
7916
|
}
|
|
7355
7917
|
else {
|
|
7356
|
-
//
|
|
7357
|
-
|
|
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
|
+
}
|
|
7358
7925
|
}
|
|
7359
7926
|
}
|
|
7360
|
-
else {
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
//
|
|
7368
|
-
if (
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
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
|
+
onChange?.(`${formattedDateTime}${offsetToUse}`);
|
|
7946
|
+
return;
|
|
7947
|
+
}
|
|
7948
|
+
// Normal mode: use timezone conversion
|
|
7949
|
+
let dateTime = dayjs(newDate)
|
|
7950
|
+
.tz(tz)
|
|
7951
|
+
.hour(hour24)
|
|
7952
|
+
.minute(newMinute)
|
|
7953
|
+
.second(newSecond ?? 0)
|
|
7954
|
+
.millisecond(0);
|
|
7955
|
+
if (!dateTime.isValid()) {
|
|
7376
7956
|
onChange?.(undefined);
|
|
7377
7957
|
return;
|
|
7378
7958
|
}
|
|
7379
|
-
|
|
7380
|
-
if (
|
|
7381
|
-
|
|
7959
|
+
// Format based on format prop
|
|
7960
|
+
if (format === 'iso-date-time') {
|
|
7961
|
+
onChange?.(dateTime.format('YYYY-MM-DDTHH:mm:ss'));
|
|
7382
7962
|
}
|
|
7383
7963
|
else {
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
setHour12(null);
|
|
7387
|
-
setMinute(null);
|
|
7388
|
-
setMeridiem(null);
|
|
7389
|
-
setHour24(null);
|
|
7390
|
-
setSecond(null);
|
|
7391
|
-
onChange?.(undefined);
|
|
7964
|
+
// date-time format with timezone
|
|
7965
|
+
onChange?.(dateTime.format('YYYY-MM-DDTHH:mm:ssZ'));
|
|
7392
7966
|
}
|
|
7393
7967
|
};
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
setMinute(null);
|
|
7406
|
-
setMeridiem(null);
|
|
7407
|
-
setHour24(null);
|
|
7408
|
-
setSecond(null);
|
|
7409
|
-
onChange?.(undefined);
|
|
7410
|
-
return;
|
|
7968
|
+
// Handle date selection
|
|
7969
|
+
const handleDateSelected = ({ date, }) => {
|
|
7970
|
+
setSelectedDate(date);
|
|
7971
|
+
updateDateTime(date, hour, minute, second, meridiem);
|
|
7972
|
+
};
|
|
7973
|
+
// Handle time change
|
|
7974
|
+
const handleTimeChange = (newHour, newMinute, newSecond, newMeridiem) => {
|
|
7975
|
+
setHour(newHour);
|
|
7976
|
+
setMinute(newMinute);
|
|
7977
|
+
if (is24Hour) {
|
|
7978
|
+
setSecond(newSecond);
|
|
7411
7979
|
}
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
if (!dateObj.isValid()) {
|
|
7415
|
-
console.warn('[DateTimePicker] Invalid date object in updateDateTime, clearing fields:', date);
|
|
7416
|
-
setSelectedDate('');
|
|
7417
|
-
setHour12(null);
|
|
7418
|
-
setMinute(null);
|
|
7419
|
-
setMeridiem(null);
|
|
7420
|
-
setHour24(null);
|
|
7421
|
-
setSecond(null);
|
|
7422
|
-
onChange?.(undefined);
|
|
7423
|
-
return;
|
|
7980
|
+
else {
|
|
7981
|
+
setMeridiem(newMeridiem);
|
|
7424
7982
|
}
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7983
|
+
if (selectedDate) {
|
|
7984
|
+
updateDateTime(selectedDate, newHour, newMinute, newSecond, newMeridiem);
|
|
7985
|
+
}
|
|
7986
|
+
};
|
|
7987
|
+
// Calendar hook
|
|
7988
|
+
const calendarProps = useCalendar({
|
|
7989
|
+
selected: selectedDate || undefined,
|
|
7990
|
+
date: selectedDate || undefined,
|
|
7991
|
+
minDate,
|
|
7992
|
+
maxDate,
|
|
7993
|
+
monthsToDisplay: 1,
|
|
7994
|
+
onDateSelected: handleDateSelected,
|
|
7995
|
+
});
|
|
7996
|
+
// Convert DateTimePickerLabels to DatePickerLabels format
|
|
7997
|
+
const calendarLabels = React.useMemo(() => ({
|
|
7998
|
+
monthNamesShort: labels.monthNamesShort || [
|
|
7999
|
+
'Jan',
|
|
8000
|
+
'Feb',
|
|
8001
|
+
'Mar',
|
|
8002
|
+
'Apr',
|
|
8003
|
+
'May',
|
|
8004
|
+
'Jun',
|
|
8005
|
+
'Jul',
|
|
8006
|
+
'Aug',
|
|
8007
|
+
'Sep',
|
|
8008
|
+
'Oct',
|
|
8009
|
+
'Nov',
|
|
8010
|
+
'Dec',
|
|
8011
|
+
],
|
|
8012
|
+
weekdayNamesShort: labels.weekdayNamesShort || [
|
|
8013
|
+
'Sun',
|
|
8014
|
+
'Mon',
|
|
8015
|
+
'Tue',
|
|
8016
|
+
'Wed',
|
|
8017
|
+
'Thu',
|
|
8018
|
+
'Fri',
|
|
8019
|
+
'Sat',
|
|
8020
|
+
],
|
|
8021
|
+
backButtonLabel: labels.backButtonLabel || 'Back',
|
|
8022
|
+
forwardButtonLabel: labels.forwardButtonLabel || 'Forward',
|
|
8023
|
+
todayLabel: quickActionLabels.today || 'Today',
|
|
8024
|
+
yesterdayLabel: quickActionLabels.yesterday || 'Yesterday',
|
|
8025
|
+
tomorrowLabel: quickActionLabels.tomorrow || 'Tomorrow',
|
|
8026
|
+
}), [labels, quickActionLabels]);
|
|
8027
|
+
// Generate time options
|
|
8028
|
+
const timeOptions = React.useMemo(() => {
|
|
8029
|
+
const options = [];
|
|
8030
|
+
// Get start time for comparison if provided
|
|
8031
|
+
let startDateTime = null;
|
|
8032
|
+
let shouldFilterByDate = false;
|
|
8033
|
+
if (startTime && selectedDate) {
|
|
8034
|
+
const startDateObj = dayjs(startTime).tz(tz);
|
|
8035
|
+
const selectedDateObj = dayjs(selectedDate).tz(tz);
|
|
8036
|
+
if (startDateObj.isValid() && selectedDateObj.isValid()) {
|
|
8037
|
+
startDateTime = startDateObj;
|
|
8038
|
+
shouldFilterByDate =
|
|
8039
|
+
startDateObj.format('YYYY-MM-DD') ===
|
|
8040
|
+
selectedDateObj.format('YYYY-MM-DD');
|
|
8041
|
+
}
|
|
8042
|
+
}
|
|
8043
|
+
if (is24Hour) {
|
|
8044
|
+
// Generate 24-hour format options
|
|
8045
|
+
for (let h = 0; h < 24; h++) {
|
|
8046
|
+
for (let m = 0; m < 60; m += 15) {
|
|
8047
|
+
// Filter out times that would result in negative duration
|
|
8048
|
+
if (startDateTime && selectedDate && shouldFilterByDate) {
|
|
8049
|
+
const selectedDateObj = dayjs(selectedDate).tz(tz);
|
|
8050
|
+
const optionDateTime = selectedDateObj
|
|
8051
|
+
.hour(h)
|
|
8052
|
+
.minute(m)
|
|
8053
|
+
.second(0)
|
|
8054
|
+
.millisecond(0);
|
|
8055
|
+
if (optionDateTime.isBefore(startDateTime)) {
|
|
8056
|
+
continue;
|
|
8057
|
+
}
|
|
8058
|
+
}
|
|
8059
|
+
// Calculate duration if startTime is provided
|
|
8060
|
+
let durationText;
|
|
8061
|
+
if (startDateTime && selectedDate) {
|
|
8062
|
+
const selectedDateObj = dayjs(selectedDate).tz(tz);
|
|
8063
|
+
const optionDateTime = selectedDateObj
|
|
8064
|
+
.hour(h)
|
|
8065
|
+
.minute(m)
|
|
8066
|
+
.second(0)
|
|
8067
|
+
.millisecond(0);
|
|
8068
|
+
if (optionDateTime.isValid() &&
|
|
8069
|
+
optionDateTime.isAfter(startDateTime)) {
|
|
8070
|
+
const diffMs = optionDateTime.diff(startDateTime);
|
|
8071
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
8072
|
+
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
8073
|
+
const diffSeconds = Math.floor((diffMs % (1000 * 60)) / 1000);
|
|
8074
|
+
if (diffHours > 0 || diffMinutes > 0 || diffSeconds > 0) {
|
|
8075
|
+
let diffText = '';
|
|
8076
|
+
if (diffHours > 0) {
|
|
8077
|
+
diffText = `${diffHours}h ${diffMinutes}m`;
|
|
8078
|
+
}
|
|
8079
|
+
else if (diffMinutes > 0) {
|
|
8080
|
+
diffText = `${diffMinutes}m ${diffSeconds}s`;
|
|
8081
|
+
}
|
|
8082
|
+
else {
|
|
8083
|
+
diffText = `${diffSeconds}s`;
|
|
8084
|
+
}
|
|
8085
|
+
durationText = `+${diffText}`;
|
|
8086
|
+
}
|
|
8087
|
+
}
|
|
8088
|
+
}
|
|
8089
|
+
const s = showSeconds ? 0 : 0;
|
|
8090
|
+
const timeDisplay = showSeconds
|
|
8091
|
+
? `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:00`
|
|
8092
|
+
: `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
|
|
8093
|
+
options.push({
|
|
8094
|
+
label: timeDisplay,
|
|
8095
|
+
value: `${h}:${m}:${s}`,
|
|
8096
|
+
hour: h,
|
|
8097
|
+
minute: m,
|
|
8098
|
+
second: s,
|
|
8099
|
+
searchText: timeDisplay,
|
|
8100
|
+
durationText,
|
|
8101
|
+
});
|
|
8102
|
+
}
|
|
7443
8103
|
}
|
|
7444
|
-
console.log('[DateTimePicker] ISO format - setting time on date:', {
|
|
7445
|
-
h,
|
|
7446
|
-
m,
|
|
7447
|
-
s,
|
|
7448
|
-
showSeconds,
|
|
7449
|
-
});
|
|
7450
|
-
if (h !== null)
|
|
7451
|
-
newDate.setHours(h);
|
|
7452
|
-
if (m !== null)
|
|
7453
|
-
newDate.setMinutes(m);
|
|
7454
|
-
newDate.setSeconds(s ?? 0);
|
|
7455
8104
|
}
|
|
7456
8105
|
else {
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
8106
|
+
// Generate 12-hour format options
|
|
8107
|
+
for (let h = 1; h <= 12; h++) {
|
|
8108
|
+
for (let m = 0; m < 60; m += 15) {
|
|
8109
|
+
for (const mer of ['am', 'pm']) {
|
|
8110
|
+
// Convert 12-hour to 24-hour for comparison
|
|
8111
|
+
let hour24 = h;
|
|
8112
|
+
if (mer === 'am' && h === 12)
|
|
8113
|
+
hour24 = 0;
|
|
8114
|
+
else if (mer === 'pm' && h < 12)
|
|
8115
|
+
hour24 = h + 12;
|
|
8116
|
+
// Filter out times that would result in negative duration
|
|
8117
|
+
if (startDateTime && selectedDate && shouldFilterByDate) {
|
|
8118
|
+
const selectedDateObj = dayjs(selectedDate).tz(tz);
|
|
8119
|
+
const optionDateTime = selectedDateObj
|
|
8120
|
+
.hour(hour24)
|
|
8121
|
+
.minute(m)
|
|
8122
|
+
.second(0)
|
|
8123
|
+
.millisecond(0);
|
|
8124
|
+
if (optionDateTime.isBefore(startDateTime)) {
|
|
8125
|
+
continue;
|
|
8126
|
+
}
|
|
8127
|
+
}
|
|
8128
|
+
// Calculate duration if startTime is provided
|
|
8129
|
+
let durationText;
|
|
8130
|
+
if (startDateTime && selectedDate) {
|
|
8131
|
+
const selectedDateObj = dayjs(selectedDate).tz(tz);
|
|
8132
|
+
const optionDateTime = selectedDateObj
|
|
8133
|
+
.hour(hour24)
|
|
8134
|
+
.minute(m)
|
|
8135
|
+
.second(0)
|
|
8136
|
+
.millisecond(0);
|
|
8137
|
+
if (optionDateTime.isValid() &&
|
|
8138
|
+
optionDateTime.isAfter(startDateTime)) {
|
|
8139
|
+
const diffMs = optionDateTime.diff(startDateTime);
|
|
8140
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
8141
|
+
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
8142
|
+
const diffSeconds = Math.floor((diffMs % (1000 * 60)) / 1000);
|
|
8143
|
+
if (diffHours > 0 || diffMinutes > 0 || diffSeconds > 0) {
|
|
8144
|
+
let diffText = '';
|
|
8145
|
+
if (diffHours > 0) {
|
|
8146
|
+
diffText = `${diffHours}h ${diffMinutes}m`;
|
|
8147
|
+
}
|
|
8148
|
+
else if (diffMinutes > 0) {
|
|
8149
|
+
diffText = `${diffMinutes}m ${diffSeconds}s`;
|
|
8150
|
+
}
|
|
8151
|
+
else {
|
|
8152
|
+
diffText = `${diffSeconds}s`;
|
|
8153
|
+
}
|
|
8154
|
+
durationText = `+${diffText}`;
|
|
8155
|
+
}
|
|
8156
|
+
}
|
|
8157
|
+
}
|
|
8158
|
+
const hourDisplay = h.toString();
|
|
8159
|
+
const minuteDisplay = m.toString().padStart(2, '0');
|
|
8160
|
+
const timeDisplay = `${hourDisplay}:${minuteDisplay} ${mer.toUpperCase()}`;
|
|
8161
|
+
options.push({
|
|
8162
|
+
label: timeDisplay,
|
|
8163
|
+
value: `${h}:${m}:${mer}`,
|
|
8164
|
+
hour: h,
|
|
8165
|
+
minute: m,
|
|
8166
|
+
meridiem: mer,
|
|
8167
|
+
searchText: timeDisplay,
|
|
8168
|
+
durationText,
|
|
8169
|
+
});
|
|
8170
|
+
}
|
|
8171
|
+
}
|
|
7477
8172
|
}
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
8173
|
+
// Sort 12-hour options by time
|
|
8174
|
+
return options.sort((a, b) => {
|
|
8175
|
+
const a12 = a;
|
|
8176
|
+
const b12 = b;
|
|
8177
|
+
let hour24A = a12.hour;
|
|
8178
|
+
if (a12.meridiem === 'am' && a12.hour === 12)
|
|
8179
|
+
hour24A = 0;
|
|
8180
|
+
else if (a12.meridiem === 'pm' && a12.hour < 12)
|
|
8181
|
+
hour24A = a12.hour + 12;
|
|
8182
|
+
let hour24B = b12.hour;
|
|
8183
|
+
if (b12.meridiem === 'am' && b12.hour === 12)
|
|
8184
|
+
hour24B = 0;
|
|
8185
|
+
else if (b12.meridiem === 'pm' && b12.hour < 12)
|
|
8186
|
+
hour24B = b12.hour + 12;
|
|
8187
|
+
if (hour24A !== hour24B) {
|
|
8188
|
+
return hour24A - hour24B;
|
|
8189
|
+
}
|
|
8190
|
+
return a12.minute - b12.minute;
|
|
7482
8191
|
});
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
8192
|
+
}
|
|
8193
|
+
return options;
|
|
8194
|
+
}, [startTime, selectedDate, tz, is24Hour, showSeconds]);
|
|
8195
|
+
// Time picker combobox setup
|
|
8196
|
+
const itemToString = React.useMemo(() => {
|
|
8197
|
+
return (item) => {
|
|
8198
|
+
return item.searchText;
|
|
8199
|
+
};
|
|
8200
|
+
}, []);
|
|
8201
|
+
const { contains } = react.useFilter({ sensitivity: 'base' });
|
|
8202
|
+
const customTimeFilter = React.useMemo(() => {
|
|
8203
|
+
if (is24Hour) {
|
|
8204
|
+
return contains;
|
|
8205
|
+
}
|
|
8206
|
+
return (itemText, filterText) => {
|
|
8207
|
+
if (!filterText) {
|
|
8208
|
+
return true;
|
|
7495
8209
|
}
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
});
|
|
8210
|
+
const lowerItemText = itemText.toLowerCase();
|
|
8211
|
+
const lowerFilterText = filterText.toLowerCase();
|
|
8212
|
+
if (lowerItemText.includes(lowerFilterText)) {
|
|
8213
|
+
return true;
|
|
7501
8214
|
}
|
|
7502
|
-
|
|
7503
|
-
|
|
8215
|
+
const item = timeOptions.find((opt) => opt.searchText.toLowerCase() === lowerItemText);
|
|
8216
|
+
if (!item || !('meridiem' in item)) {
|
|
8217
|
+
return false;
|
|
7504
8218
|
}
|
|
7505
|
-
|
|
7506
|
-
|
|
8219
|
+
let hour24 = item.hour;
|
|
8220
|
+
if (item.meridiem === 'am' && item.hour === 12)
|
|
8221
|
+
hour24 = 0;
|
|
8222
|
+
else if (item.meridiem === 'pm' && item.hour < 12)
|
|
8223
|
+
hour24 = item.hour + 12;
|
|
8224
|
+
const hour24Str = hour24.toString().padStart(2, '0');
|
|
8225
|
+
const minuteStr = item.minute.toString().padStart(2, '0');
|
|
8226
|
+
const formats = [
|
|
8227
|
+
`${hour24Str}:${minuteStr}`,
|
|
8228
|
+
`${hour24Str}${minuteStr}`,
|
|
8229
|
+
hour24Str,
|
|
8230
|
+
`${hour24}:${minuteStr}`,
|
|
8231
|
+
hour24.toString(),
|
|
8232
|
+
];
|
|
8233
|
+
return formats.some((format) => format.toLowerCase().includes(lowerFilterText) ||
|
|
8234
|
+
lowerFilterText.includes(format.toLowerCase()));
|
|
8235
|
+
};
|
|
8236
|
+
}, [timeOptions, is24Hour, contains]);
|
|
8237
|
+
const { collection, filter } = react.useListCollection({
|
|
8238
|
+
initialItems: timeOptions,
|
|
8239
|
+
itemToString: itemToString,
|
|
8240
|
+
itemToValue: (item) => item.value,
|
|
8241
|
+
filter: customTimeFilter,
|
|
8242
|
+
});
|
|
8243
|
+
// Get current value string for combobox (must match option.value format)
|
|
8244
|
+
const currentTimeValue = React.useMemo(() => {
|
|
8245
|
+
if (is24Hour) {
|
|
8246
|
+
if (hour === null || minute === null) {
|
|
8247
|
+
return '';
|
|
7507
8248
|
}
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
const finalISO = dayjs(newDate).tz(timezone).toISOString();
|
|
7511
|
-
console.log('[DateTimePicker] Final ISO string to emit:', {
|
|
7512
|
-
newDate: newDate.toISOString(),
|
|
7513
|
-
timezone,
|
|
7514
|
-
finalISO,
|
|
7515
|
-
});
|
|
7516
|
-
onChange?.(finalISO);
|
|
7517
|
-
};
|
|
7518
|
-
const handleClear = () => {
|
|
7519
|
-
setSelectedDate('');
|
|
7520
|
-
setHour12(null);
|
|
7521
|
-
setHour24(null);
|
|
7522
|
-
setMinute(null);
|
|
7523
|
-
setSecond(null);
|
|
7524
|
-
setMeridiem(null);
|
|
7525
|
-
onChange?.(undefined);
|
|
7526
|
-
};
|
|
7527
|
-
const isISO = format === 'iso-date-time';
|
|
7528
|
-
// Normalize startTime to ignore milliseconds
|
|
7529
|
-
const normalizedStartTime = startTime
|
|
7530
|
-
? dayjs(startTime).tz(timezone).millisecond(0).toISOString()
|
|
7531
|
-
: undefined;
|
|
7532
|
-
// Determine minDate: prioritize explicit minDate prop, then fall back to startTime
|
|
7533
|
-
const effectiveMinDate = minDate
|
|
7534
|
-
? minDate
|
|
7535
|
-
: normalizedStartTime && dayjs(normalizedStartTime).tz(timezone).isValid()
|
|
7536
|
-
? dayjs(normalizedStartTime).tz(timezone).startOf('day').toDate()
|
|
7537
|
-
: undefined;
|
|
7538
|
-
// Log current state before render
|
|
7539
|
-
React.useEffect(() => {
|
|
7540
|
-
console.log('[DateTimePicker] Current state before render:', {
|
|
7541
|
-
isISO,
|
|
7542
|
-
hour12,
|
|
7543
|
-
minute,
|
|
7544
|
-
meridiem,
|
|
7545
|
-
hour24,
|
|
7546
|
-
second,
|
|
7547
|
-
selectedDate,
|
|
7548
|
-
normalizedStartTime,
|
|
7549
|
-
timezone,
|
|
7550
|
-
});
|
|
7551
|
-
}, [
|
|
7552
|
-
isISO,
|
|
7553
|
-
hour12,
|
|
7554
|
-
minute,
|
|
7555
|
-
meridiem,
|
|
7556
|
-
hour24,
|
|
7557
|
-
second,
|
|
7558
|
-
selectedDate,
|
|
7559
|
-
normalizedStartTime,
|
|
7560
|
-
timezone,
|
|
7561
|
-
]);
|
|
7562
|
-
// Compute display text from current state
|
|
7563
|
-
const displayText = React.useMemo(() => {
|
|
7564
|
-
if (!selectedDate)
|
|
7565
|
-
return null;
|
|
7566
|
-
const dateObj = dayjs.tz(selectedDate, timezone);
|
|
7567
|
-
if (!dateObj.isValid())
|
|
7568
|
-
return null;
|
|
7569
|
-
if (isISO) {
|
|
7570
|
-
// For ISO format, use hour24, minute, second
|
|
7571
|
-
if (hour24 === null || minute === null)
|
|
7572
|
-
return null;
|
|
7573
|
-
const dateTimeObj = dateObj
|
|
7574
|
-
.hour(hour24)
|
|
7575
|
-
.minute(minute)
|
|
7576
|
-
.second(second ?? 0);
|
|
7577
|
-
return dateTimeObj.format(showSeconds ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm');
|
|
8249
|
+
const s = second ?? 0;
|
|
8250
|
+
return `${hour}:${minute}:${s}`;
|
|
7578
8251
|
}
|
|
7579
8252
|
else {
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
7588
|
-
|
|
7589
|
-
|
|
7590
|
-
|
|
8253
|
+
if (hour === null || minute === null || meridiem === null) {
|
|
8254
|
+
return '';
|
|
8255
|
+
}
|
|
8256
|
+
return `${hour}:${minute}:${meridiem}`;
|
|
8257
|
+
}
|
|
8258
|
+
}, [hour, minute, second, meridiem, is24Hour]);
|
|
8259
|
+
// Parse custom time input formats like "1400", "2pm", "14:00", "2:00 PM"
|
|
8260
|
+
const parseCustomTimeInput = (input) => {
|
|
8261
|
+
if (!input || !input.trim()) {
|
|
8262
|
+
return { hour: null, minute: null, second: null, meridiem: null };
|
|
8263
|
+
}
|
|
8264
|
+
const trimmed = input.trim().toLowerCase();
|
|
8265
|
+
// Try parsing 4-digit format without colon: "1400" -> 14:00
|
|
8266
|
+
const fourDigitMatch = trimmed.match(/^(\d{4})$/);
|
|
8267
|
+
if (fourDigitMatch) {
|
|
8268
|
+
const digits = fourDigitMatch[1];
|
|
8269
|
+
const hour = parseInt(digits.substring(0, 2), 10);
|
|
8270
|
+
const minute = parseInt(digits.substring(2, 4), 10);
|
|
8271
|
+
if (hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59) {
|
|
8272
|
+
if (is24Hour) {
|
|
8273
|
+
return { hour, minute, second: 0, meridiem: null };
|
|
8274
|
+
}
|
|
8275
|
+
else {
|
|
8276
|
+
// Convert to 12-hour format
|
|
8277
|
+
let hour12 = hour;
|
|
8278
|
+
let meridiem;
|
|
8279
|
+
if (hour === 0) {
|
|
8280
|
+
hour12 = 12;
|
|
8281
|
+
meridiem = 'am';
|
|
8282
|
+
}
|
|
8283
|
+
else if (hour === 12) {
|
|
8284
|
+
hour12 = 12;
|
|
8285
|
+
meridiem = 'pm';
|
|
8286
|
+
}
|
|
8287
|
+
else if (hour > 12) {
|
|
8288
|
+
hour12 = hour - 12;
|
|
8289
|
+
meridiem = 'pm';
|
|
8290
|
+
}
|
|
8291
|
+
else {
|
|
8292
|
+
hour12 = hour;
|
|
8293
|
+
meridiem = 'am';
|
|
8294
|
+
}
|
|
8295
|
+
return { hour: hour12, minute, second: null, meridiem };
|
|
8296
|
+
}
|
|
8297
|
+
}
|
|
7591
8298
|
}
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
8299
|
+
// Try parsing hour with meridiem: "2pm", "14pm", "2am"
|
|
8300
|
+
const hourMeridiemMatch = trimmed.match(/^(\d{1,2})\s*(am|pm)$/);
|
|
8301
|
+
if (hourMeridiemMatch && !is24Hour) {
|
|
8302
|
+
const hour12 = parseInt(hourMeridiemMatch[1], 10);
|
|
8303
|
+
const meridiem = hourMeridiemMatch[2];
|
|
8304
|
+
if (hour12 >= 1 && hour12 <= 12) {
|
|
8305
|
+
return { hour: hour12, minute: 0, second: null, meridiem };
|
|
8306
|
+
}
|
|
8307
|
+
}
|
|
8308
|
+
// Try parsing 24-hour format with hour only: "14" -> 14:00
|
|
8309
|
+
const hourOnlyMatch = trimmed.match(/^(\d{1,2})$/);
|
|
8310
|
+
if (hourOnlyMatch && is24Hour) {
|
|
8311
|
+
const hour = parseInt(hourOnlyMatch[1], 10);
|
|
8312
|
+
if (hour >= 0 && hour <= 23) {
|
|
8313
|
+
return { hour, minute: 0, second: 0, meridiem: null };
|
|
8314
|
+
}
|
|
8315
|
+
}
|
|
8316
|
+
// Try parsing standard formats: "14:00", "2:00 PM"
|
|
8317
|
+
const time24Pattern = /^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?$/;
|
|
8318
|
+
const match24 = trimmed.match(time24Pattern);
|
|
8319
|
+
if (match24) {
|
|
8320
|
+
const hour24 = parseInt(match24[1], 10);
|
|
8321
|
+
const minute = parseInt(match24[2], 10);
|
|
8322
|
+
const second = match24[3] ? parseInt(match24[3], 10) : 0;
|
|
8323
|
+
if (hour24 >= 0 &&
|
|
8324
|
+
hour24 <= 23 &&
|
|
8325
|
+
minute >= 0 &&
|
|
8326
|
+
minute <= 59 &&
|
|
8327
|
+
second >= 0 &&
|
|
8328
|
+
second <= 59) {
|
|
8329
|
+
if (is24Hour) {
|
|
8330
|
+
return { hour: hour24, minute, second, meridiem: null };
|
|
8331
|
+
}
|
|
8332
|
+
else {
|
|
8333
|
+
// Convert to 12-hour format
|
|
8334
|
+
let hour12 = hour24;
|
|
8335
|
+
let meridiem;
|
|
8336
|
+
if (hour24 === 0) {
|
|
8337
|
+
hour12 = 12;
|
|
8338
|
+
meridiem = 'am';
|
|
8339
|
+
}
|
|
8340
|
+
else if (hour24 === 12) {
|
|
8341
|
+
hour12 = 12;
|
|
8342
|
+
meridiem = 'pm';
|
|
8343
|
+
}
|
|
8344
|
+
else if (hour24 > 12) {
|
|
8345
|
+
hour12 = hour24 - 12;
|
|
8346
|
+
meridiem = 'pm';
|
|
7612
8347
|
}
|
|
7613
8348
|
else {
|
|
7614
|
-
|
|
7615
|
-
|
|
8349
|
+
hour12 = hour24;
|
|
8350
|
+
meridiem = 'am';
|
|
8351
|
+
}
|
|
8352
|
+
return { hour: hour12, minute, second: null, meridiem };
|
|
8353
|
+
}
|
|
8354
|
+
}
|
|
8355
|
+
}
|
|
8356
|
+
// Try parsing 12-hour format: "2:00 PM", "2:00PM"
|
|
8357
|
+
const time12Pattern = /^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?\s*(am|pm)$/;
|
|
8358
|
+
const match12 = trimmed.match(time12Pattern);
|
|
8359
|
+
if (match12 && !is24Hour) {
|
|
8360
|
+
const hour12 = parseInt(match12[1], 10);
|
|
8361
|
+
const minute = parseInt(match12[2], 10);
|
|
8362
|
+
const second = match12[3] ? parseInt(match12[3], 10) : null;
|
|
8363
|
+
const meridiem = match12[4];
|
|
8364
|
+
if (hour12 >= 1 &&
|
|
8365
|
+
hour12 <= 12 &&
|
|
8366
|
+
minute >= 0 &&
|
|
8367
|
+
minute <= 59 &&
|
|
8368
|
+
(second === null || (second >= 0 && second <= 59))) {
|
|
8369
|
+
return { hour: hour12, minute, second, meridiem };
|
|
8370
|
+
}
|
|
8371
|
+
}
|
|
8372
|
+
return { hour: null, minute: null, second: null, meridiem: null };
|
|
8373
|
+
};
|
|
8374
|
+
const handleTimeValueChange = (details) => {
|
|
8375
|
+
if (details.value.length === 0) {
|
|
8376
|
+
handleTimeChange(null, null, null, null);
|
|
8377
|
+
filter('');
|
|
8378
|
+
return;
|
|
8379
|
+
}
|
|
8380
|
+
const selectedValue = details.value[0];
|
|
8381
|
+
const selectedOption = timeOptions.find((opt) => opt.value === selectedValue);
|
|
8382
|
+
if (selectedOption) {
|
|
8383
|
+
filter('');
|
|
8384
|
+
if (is24Hour) {
|
|
8385
|
+
const opt24 = selectedOption;
|
|
8386
|
+
handleTimeChange(opt24.hour, opt24.minute, opt24.second, null);
|
|
8387
|
+
}
|
|
8388
|
+
else {
|
|
8389
|
+
const opt12 = selectedOption;
|
|
8390
|
+
handleTimeChange(opt12.hour, opt12.minute, null, opt12.meridiem);
|
|
8391
|
+
}
|
|
8392
|
+
}
|
|
8393
|
+
};
|
|
8394
|
+
// Track the current input value for Enter key handling
|
|
8395
|
+
const [timeInputValue, setTimeInputValue] = React.useState('');
|
|
8396
|
+
const handleTimeInputChange = (details) => {
|
|
8397
|
+
// Store the input value and filter
|
|
8398
|
+
setTimeInputValue(details.inputValue);
|
|
8399
|
+
filter(details.inputValue);
|
|
8400
|
+
};
|
|
8401
|
+
const handleTimeInputKeyDown = (e) => {
|
|
8402
|
+
if (e.key === 'Enter') {
|
|
8403
|
+
e.preventDefault();
|
|
8404
|
+
// Use the stored input value
|
|
8405
|
+
const parsed = parseCustomTimeInput(timeInputValue);
|
|
8406
|
+
if (parsed.hour !== null && parsed.minute !== null) {
|
|
8407
|
+
if (is24Hour) {
|
|
8408
|
+
handleTimeChange(parsed.hour, parsed.minute, parsed.second, null);
|
|
8409
|
+
}
|
|
8410
|
+
else {
|
|
8411
|
+
if (parsed.meridiem !== null) {
|
|
8412
|
+
handleTimeChange(parsed.hour, parsed.minute, null, parsed.meridiem);
|
|
7616
8413
|
}
|
|
7617
|
-
}
|
|
8414
|
+
}
|
|
8415
|
+
// Clear the filter and input value after applying
|
|
8416
|
+
filter('');
|
|
8417
|
+
setTimeInputValue('');
|
|
8418
|
+
// Close the popover if value is valid
|
|
8419
|
+
setTimePopoverOpen(false);
|
|
8420
|
+
}
|
|
8421
|
+
}
|
|
8422
|
+
};
|
|
8423
|
+
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 ??
|
|
8424
|
+
(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 ??
|
|
8425
|
+
'No time found' }), collection.items.map((item) => {
|
|
8426
|
+
const option = item;
|
|
8427
|
+
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));
|
|
8428
|
+
})] }) }) })] }) }) }) }) }) })) : (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 ??
|
|
8429
|
+
(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) => {
|
|
8430
|
+
const option = item;
|
|
8431
|
+
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));
|
|
8432
|
+
})] }) }) })] }) }) }) }) }))] }), 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) => {
|
|
8433
|
+
const newOffset = e.value[0];
|
|
8434
|
+
if (newOffset) {
|
|
8435
|
+
// Update controlled or internal state
|
|
8436
|
+
if (onTimezoneOffsetChange) {
|
|
8437
|
+
onTimezoneOffsetChange(newOffset);
|
|
8438
|
+
}
|
|
8439
|
+
else {
|
|
8440
|
+
setInternalTimezoneOffset(newOffset);
|
|
8441
|
+
}
|
|
8442
|
+
// Update date-time with new offset (pass it directly to avoid stale state)
|
|
8443
|
+
if (selectedDate &&
|
|
8444
|
+
hour !== null &&
|
|
8445
|
+
minute !== null) {
|
|
8446
|
+
updateDateTime(selectedDate, hour, minute, second, meridiem, newOffset);
|
|
8447
|
+
}
|
|
8448
|
+
// Close popover after selection
|
|
8449
|
+
setTimezonePopoverOpen(false);
|
|
8450
|
+
}
|
|
8451
|
+
}, 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) => {
|
|
8452
|
+
const newOffset = e.value[0];
|
|
8453
|
+
if (newOffset) {
|
|
8454
|
+
// Update controlled or internal state
|
|
8455
|
+
if (onTimezoneOffsetChange) {
|
|
8456
|
+
onTimezoneOffsetChange(newOffset);
|
|
8457
|
+
}
|
|
8458
|
+
else {
|
|
8459
|
+
setInternalTimezoneOffset(newOffset);
|
|
8460
|
+
}
|
|
8461
|
+
// Update date-time with new offset (pass it directly to avoid stale state)
|
|
8462
|
+
if (selectedDate &&
|
|
8463
|
+
hour !== null &&
|
|
8464
|
+
minute !== null) {
|
|
8465
|
+
updateDateTime(selectedDate, hour, minute, second, meridiem, newOffset);
|
|
8466
|
+
}
|
|
8467
|
+
// Close popover after selection
|
|
8468
|
+
setTimezonePopoverOpen(false);
|
|
8469
|
+
}
|
|
8470
|
+
}, 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))) }) })] }) }) }) }) }))] }))] }));
|
|
7618
8471
|
}
|
|
7619
8472
|
|
|
7620
8473
|
dayjs.extend(utc);
|
|
@@ -7625,14 +8478,15 @@ const DateTimePicker = ({ column, schema, prefix, }) => {
|
|
|
7625
8478
|
const formI18n = useFormI18n(column, prefix, schema);
|
|
7626
8479
|
const { required, gridColumn = 'span 12', gridRow = 'span 1', displayDateFormat = 'YYYY-MM-DD HH:mm:ss',
|
|
7627
8480
|
// with timezone
|
|
7628
|
-
dateFormat = 'YYYY-MM-DD[T]HH:mm:ssZ', } = schema;
|
|
8481
|
+
dateFormat = 'YYYY-MM-DD[T]HH:mm:ssZ', dateTimePicker, } = schema;
|
|
7629
8482
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
7630
8483
|
const colLabel = formI18n.colLabel;
|
|
7631
|
-
|
|
8484
|
+
React.useState(false);
|
|
7632
8485
|
const selectedDate = watch(colLabel);
|
|
7633
|
-
|
|
8486
|
+
selectedDate && dayjs(selectedDate).tz(timezone).isValid()
|
|
7634
8487
|
? dayjs(selectedDate).tz(timezone).format(displayDateFormat)
|
|
7635
8488
|
: '';
|
|
8489
|
+
// Set default date on mount if no value exists
|
|
7636
8490
|
const dateTimePickerLabelsConfig = {
|
|
7637
8491
|
monthNamesShort: dateTimePickerLabels?.monthNamesShort ?? [
|
|
7638
8492
|
'January',
|
|
@@ -7672,11 +8526,9 @@ const DateTimePicker = ({ column, schema, prefix, }) => {
|
|
|
7672
8526
|
else {
|
|
7673
8527
|
setValue(colLabel, undefined);
|
|
7674
8528
|
}
|
|
7675
|
-
}, timezone: timezone, labels: dateTimePickerLabelsConfig, timePickerLabels: timePickerLabels }));
|
|
8529
|
+
}, timezone: timezone, labels: dateTimePickerLabelsConfig, timePickerLabels: timePickerLabels, showQuickActions: dateTimePicker?.showQuickActions ?? false, quickActionLabels: dateTimePicker?.quickActionLabels, showTimezoneSelector: dateTimePicker?.showTimezoneSelector ?? false }));
|
|
7676
8530
|
return (jsxRuntime.jsx(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
7677
|
-
gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children:
|
|
7678
|
-
setOpen(true);
|
|
7679
|
-
}, 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 }) }) }) }))] }) }));
|
|
8531
|
+
gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: dateTimePickerContent }));
|
|
7680
8532
|
};
|
|
7681
8533
|
|
|
7682
8534
|
const SchemaRenderer = ({ schema, prefix, column, }) => {
|
|
@@ -7797,7 +8649,7 @@ const BooleanViewer = ({ schema, column, prefix, }) => {
|
|
|
7797
8649
|
const value = watch(colLabel);
|
|
7798
8650
|
const formI18n = useFormI18n(column, prefix, schema);
|
|
7799
8651
|
return (jsxRuntime.jsxs(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
7800
|
-
gridRow, children: [jsxRuntime.jsx(react.Text, { children: value ?
|
|
8652
|
+
gridRow, children: [jsxRuntime.jsx(react.Text, { children: value ? 'True' : 'False' }), errors[`${column}`] && (jsxRuntime.jsx(react.Text, { color: 'red.400', children: formI18n.required() }))] }));
|
|
7801
8653
|
};
|
|
7802
8654
|
|
|
7803
8655
|
const CustomViewer = ({ column, schema, prefix }) => {
|
|
@@ -7829,23 +8681,22 @@ const DateViewer = ({ column, schema, prefix }) => {
|
|
|
7829
8681
|
|
|
7830
8682
|
const EnumViewer = ({ column, isMultiple = false, schema, prefix, }) => {
|
|
7831
8683
|
const { watch, formState: { errors }, } = reactHookForm.useFormContext();
|
|
7832
|
-
const formI18n = useFormI18n(column, prefix);
|
|
8684
|
+
const formI18n = useFormI18n(column, prefix, schema);
|
|
7833
8685
|
const { required } = schema;
|
|
7834
8686
|
const isRequired = required?.some((columnId) => columnId === column);
|
|
7835
|
-
const { gridColumn =
|
|
8687
|
+
const { gridColumn = 'span 12', gridRow = 'span 1', renderDisplay } = schema;
|
|
7836
8688
|
const colLabel = formI18n.colLabel;
|
|
7837
8689
|
const watchEnum = watch(colLabel);
|
|
7838
8690
|
const watchEnums = (watch(colLabel) ?? []);
|
|
7839
|
-
|
|
7840
|
-
|
|
8691
|
+
const renderDisplayFunction = renderDisplay || defaultRenderDisplay;
|
|
8692
|
+
return (jsxRuntime.jsxs(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
|
|
8693
|
+
gridRow, children: [isMultiple && (jsxRuntime.jsx(react.Flex, { flexFlow: 'wrap', gap: 1, children: watchEnums.map((enumValue) => {
|
|
7841
8694
|
const item = enumValue;
|
|
7842
8695
|
if (item === undefined) {
|
|
7843
8696
|
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: "undefined" });
|
|
7844
8697
|
}
|
|
7845
|
-
return (jsxRuntime.jsx(Tag, { size: "lg", children:
|
|
7846
|
-
|
|
7847
|
-
: formI18n.t(item) }, item));
|
|
7848
|
-
}) })), !isMultiple && jsxRuntime.jsx(react.Text, { children: formI18n.t(watchEnum) }), errors[`${column}`] && (jsxRuntime.jsx(react.Text, { color: "red.400", children: formI18n.required() }))] }));
|
|
8698
|
+
return (jsxRuntime.jsx(Tag, { size: "lg", children: renderDisplayFunction(item) }, item));
|
|
8699
|
+
}) })), !isMultiple && jsxRuntime.jsx(react.Text, { children: renderDisplayFunction(watchEnum) }), errors[`${column}`] && (jsxRuntime.jsx(react.Text, { color: 'red.400', children: formI18n.required() }))] }));
|
|
7849
8700
|
};
|
|
7850
8701
|
|
|
7851
8702
|
const FileViewer = ({ column, schema, prefix }) => {
|
|
@@ -7965,31 +8816,35 @@ const StringViewer = ({ column, schema, prefix, }) => {
|
|
|
7965
8816
|
|
|
7966
8817
|
const TagViewer = ({ column, schema, prefix }) => {
|
|
7967
8818
|
const { watch, formState: { errors }, setValue, } = reactHookForm.useFormContext();
|
|
7968
|
-
const { serverUrl } = useSchemaContext();
|
|
7969
8819
|
if (schema.properties == undefined) {
|
|
7970
|
-
throw new Error(
|
|
8820
|
+
throw new Error('schema properties undefined when using DatePicker');
|
|
7971
8821
|
}
|
|
7972
|
-
const { gridColumn, gridRow, in_table, object_id_column } = schema;
|
|
8822
|
+
const { gridColumn, gridRow, in_table, object_id_column, tagPicker } = schema;
|
|
7973
8823
|
if (in_table === undefined) {
|
|
7974
|
-
throw new Error(
|
|
8824
|
+
throw new Error('in_table is undefined when using TagPicker');
|
|
7975
8825
|
}
|
|
7976
8826
|
if (object_id_column === undefined) {
|
|
7977
|
-
throw new Error(
|
|
8827
|
+
throw new Error('object_id_column is undefined when using TagPicker');
|
|
8828
|
+
}
|
|
8829
|
+
if (!tagPicker?.queryFn) {
|
|
8830
|
+
throw new Error('tagPicker.queryFn is required in schema. serverUrl has been removed.');
|
|
7978
8831
|
}
|
|
7979
8832
|
const query = reactQuery.useQuery({
|
|
7980
8833
|
queryKey: [`tagpicker`, in_table],
|
|
7981
8834
|
queryFn: async () => {
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
in_table: "tables_tags_view",
|
|
8835
|
+
const result = await tagPicker.queryFn({
|
|
8836
|
+
in_table: 'tables_tags_view',
|
|
7985
8837
|
where: [
|
|
7986
8838
|
{
|
|
7987
|
-
id:
|
|
8839
|
+
id: 'table_name',
|
|
7988
8840
|
value: [in_table],
|
|
7989
8841
|
},
|
|
7990
8842
|
],
|
|
7991
8843
|
limit: 100,
|
|
8844
|
+
offset: 0,
|
|
8845
|
+
searching: '',
|
|
7992
8846
|
});
|
|
8847
|
+
return result.data || { data: [] };
|
|
7993
8848
|
},
|
|
7994
8849
|
staleTime: 10000,
|
|
7995
8850
|
});
|
|
@@ -7997,17 +8852,19 @@ const TagViewer = ({ column, schema, prefix }) => {
|
|
|
7997
8852
|
const existingTagsQuery = reactQuery.useQuery({
|
|
7998
8853
|
queryKey: [`existing`, { in_table, object_id_column }, object_id],
|
|
7999
8854
|
queryFn: async () => {
|
|
8000
|
-
|
|
8001
|
-
serverUrl,
|
|
8855
|
+
const result = await tagPicker.queryFn({
|
|
8002
8856
|
in_table: in_table,
|
|
8003
8857
|
where: [
|
|
8004
8858
|
{
|
|
8005
8859
|
id: object_id_column,
|
|
8006
|
-
value: object_id[0],
|
|
8860
|
+
value: [object_id[0]],
|
|
8007
8861
|
},
|
|
8008
8862
|
],
|
|
8009
8863
|
limit: 100,
|
|
8864
|
+
offset: 0,
|
|
8865
|
+
searching: '',
|
|
8010
8866
|
});
|
|
8867
|
+
return result.data || { data: [] };
|
|
8011
8868
|
},
|
|
8012
8869
|
enabled: object_id != undefined,
|
|
8013
8870
|
staleTime: 10000,
|
|
@@ -8018,9 +8875,9 @@ const TagViewer = ({ column, schema, prefix }) => {
|
|
|
8018
8875
|
if (!!object_id === false) {
|
|
8019
8876
|
return jsxRuntime.jsx(jsxRuntime.Fragment, {});
|
|
8020
8877
|
}
|
|
8021
|
-
return (jsxRuntime.jsxs(react.Flex, { flexFlow:
|
|
8878
|
+
return (jsxRuntime.jsxs(react.Flex, { flexFlow: 'column', gap: 4, gridColumn,
|
|
8022
8879
|
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 }) => {
|
|
8023
|
-
return (jsxRuntime.jsxs(react.Flex, { flexFlow:
|
|
8880
|
+
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) => {
|
|
8024
8881
|
const existedTags = Object.values(all_tags)
|
|
8025
8882
|
.filter(({ id }) => {
|
|
8026
8883
|
return existingTagList.some(({ tag_id }) => tag_id === id);
|
|
@@ -8032,20 +8889,20 @@ const TagViewer = ({ column, schema, prefix }) => {
|
|
|
8032
8889
|
tagIds.value,
|
|
8033
8890
|
]);
|
|
8034
8891
|
setValue(`${column}.${parent_tag_name}.old`, existedTags);
|
|
8035
|
-
}, children: jsxRuntime.jsx(react.Flex, { flexFlow:
|
|
8892
|
+
}, children: jsxRuntime.jsx(react.Flex, { flexFlow: 'wrap', gap: 2, children: Object.entries(all_tags).map(([tagName, { id }]) => {
|
|
8036
8893
|
if (existingTagList.some(({ tag_id }) => tag_id === id)) {
|
|
8037
|
-
return (jsxRuntime.jsx(RadioCardItem, { label: tagName, value: id, flex:
|
|
8894
|
+
return (jsxRuntime.jsx(RadioCardItem, { label: tagName, value: id, flex: '0 0 0%', disabled: true }, `${tagName}-${id}`));
|
|
8038
8895
|
}
|
|
8039
|
-
return (jsxRuntime.jsx(RadioCardItem, { label: tagName, value: id, flex:
|
|
8896
|
+
return (jsxRuntime.jsx(RadioCardItem, { label: tagName, value: id, flex: '0 0 0%', colorPalette: 'blue' }, `${tagName}-${id}`));
|
|
8040
8897
|
}) }) })), !is_mutually_exclusive && (jsxRuntime.jsx(react.CheckboxGroup, { onValueChange: (tagIds) => {
|
|
8041
8898
|
setValue(`${column}.${parent_tag_name}.current`, tagIds);
|
|
8042
|
-
}, children: jsxRuntime.jsx(react.Flex, { flexFlow:
|
|
8899
|
+
}, children: jsxRuntime.jsx(react.Flex, { flexFlow: 'wrap', gap: 2, children: Object.entries(all_tags).map(([tagName, { id }]) => {
|
|
8043
8900
|
if (existingTagList.some(({ tag_id }) => tag_id === id)) {
|
|
8044
|
-
return (jsxRuntime.jsx(CheckboxCard, { label: tagName, value: id, flex:
|
|
8901
|
+
return (jsxRuntime.jsx(CheckboxCard, { label: tagName, value: id, flex: '0 0 0%', disabled: true, colorPalette: 'blue' }, `${tagName}-${id}`));
|
|
8045
8902
|
}
|
|
8046
|
-
return (jsxRuntime.jsx(CheckboxCard, { label: tagName, value: id, flex:
|
|
8903
|
+
return (jsxRuntime.jsx(CheckboxCard, { label: tagName, value: id, flex: '0 0 0%' }, `${tagName}-${id}`));
|
|
8047
8904
|
}) }) }))] }, `tag-${parent_tag_name}`));
|
|
8048
|
-
}), errors[`${column}`] && (jsxRuntime.jsx(react.Text, { color:
|
|
8905
|
+
}), errors[`${column}`] && (jsxRuntime.jsx(react.Text, { color: 'red.400', children: (errors[`${column}`]?.message ?? 'No error message') }))] }));
|
|
8049
8906
|
};
|
|
8050
8907
|
|
|
8051
8908
|
const TextAreaViewer = ({ column, schema, prefix, }) => {
|
|
@@ -8252,6 +9109,17 @@ const FormBody = () => {
|
|
|
8252
9109
|
|
|
8253
9110
|
const FormTitle = () => {
|
|
8254
9111
|
const { schema } = useSchemaContext();
|
|
9112
|
+
// Debug log when form title is missing
|
|
9113
|
+
if (!schema.title) {
|
|
9114
|
+
console.debug('[Form Title] Missing title in root schema. Add title property to schema.', {
|
|
9115
|
+
schema: {
|
|
9116
|
+
type: schema.type,
|
|
9117
|
+
properties: schema.properties
|
|
9118
|
+
? Object.keys(schema.properties)
|
|
9119
|
+
: undefined,
|
|
9120
|
+
},
|
|
9121
|
+
});
|
|
9122
|
+
}
|
|
8255
9123
|
return jsxRuntime.jsx(react.Heading, { children: schema.title ?? 'Form' });
|
|
8256
9124
|
};
|
|
8257
9125
|
|
|
@@ -9420,6 +10288,7 @@ exports.CardHeader = CardHeader;
|
|
|
9420
10288
|
exports.DataDisplay = DataDisplay;
|
|
9421
10289
|
exports.DataTable = DataTable;
|
|
9422
10290
|
exports.DataTableServer = DataTableServer;
|
|
10291
|
+
exports.DatePickerContext = DatePickerContext;
|
|
9423
10292
|
exports.DatePickerInput = DatePickerInput;
|
|
9424
10293
|
exports.DefaultCardTitle = DefaultCardTitle;
|
|
9425
10294
|
exports.DefaultForm = DefaultForm;
|