Преглед изворни кода

Neo: min date and read-only state for date and switcher fields (#5823)

RomanL пре 1 недеља
родитељ
комит
c924cc3c6f

+ 6 - 1
neo/packages/app/components/form-fields/datetime.js

@@ -15,6 +15,11 @@ export default function FormFieldDatetime({ name, value = '', type, ...props })
         }
     );
 
+    // UNA passes the lower bound as the input's `min` attr (unix ts or date string)
+    const min = props?.attrs?.min;
+    let minDate = min ? new Date(/^\d+$/.test(String(min)) ? Number(min) * 1000 : String(min).replace(' ', 'T')) : undefined;
+    if (minDate && isNaN(minDate)) minDate = undefined;
+
     const setParamValue = (next) => {
         const date = new Date(next * 1000);
         if (props?.db?.pass == 'Date' || props?.db?.pass == 'DateTs') {
@@ -30,7 +35,7 @@ export default function FormFieldDatetime({ name, value = '', type, ...props })
 
     return (
         <Field {...props} name={name} type={type}>
-            <DatePicker value={field.value} type={type} name={name} onChange={setParamValue} />
+            <DatePicker value={field.value} type={type} name={name} minDate={minDate} disabled={!!props?.attrs?.disabled} onChange={setParamValue} />
         </Field>
     );
 }

+ 3 - 1
neo/packages/app/components/form-fields/switcher.js

@@ -9,7 +9,8 @@ import { switcherInitialValue } from 'app/lib/form/field-initial-values';
 const switcherTheme = appSetting('theme', 'switcher');
 
 export default function FormFieldSwitcher(props) {
-    const { caption, size } = props;
+    const { caption, size, attrs } = props;
+    const disabled = !!attrs?.disabled;
     const resolvedSize = resolveControlSize(size);
     const sizeClass = switcherTheme.size?.[resolvedSize] ?? switcherTheme.size?.regular ?? '';
 
@@ -29,6 +30,7 @@ export default function FormFieldSwitcher(props) {
                     onValueChange={handleToggle}
                     value={!!field.value}
                     size={resolvedSize}
+                    disabled={disabled}
                 />
                 <Text className={switcherTheme['u-controls-switcher-text']}>
                     {caption}

+ 11 - 0
neo/packages/app/lib/form/field-initial-values.ts

@@ -57,6 +57,17 @@ export function doubleRangeInitialValue(input: Input) {
 export function datetimeInitialValue(input: Input) {
     const withTime = input.type === 'datetime';
     let value = input.value ?? '';
+    // value may come as a unix timestamp - convert it to the 'YYYY-MM-DD HH:MM:SSZ' string the picker expects
+    if (typeof value === 'number' || /^\d+$/.test(String(value))) {
+        const ts = Number(value);
+        if (ts > 0) {
+            const date = new Date(ts * 1000);
+            date.setMinutes(date.getMinutes() - date.getTimezoneOffset());
+            value = date.toISOString().replace('T', ' ').substring(0, 19) + 'Z';
+        } else {
+            value = '';
+        }
+    }
     if ((value == '0000-00-00 00:00:00Z' || value == '') && input.required == true) {
         const date = new Date();
         date.setHours(0, 0, 0, 0);

+ 15 - 7
neo/packages/app/ui/atoms/date-picker.tsx

@@ -89,13 +89,14 @@ for (let y = 1900; y <= 2100; y++) {
 
 const dateInputOpts = { yearPolicy: 'always' as const, month: '2-digit' as const };
 
-/** `weekStart`: first day of week, 0 = Sunday. */
-export function MonthCalendar({ date = new Date(), onSelect, selectedDate, weekStart = 1 }: { date?: Date; onSelect: (date: Date) => void; selectedDate?: Date | null; weekStart?: 0 | 1 }) {
+/** `weekStart`: first day of week, 0 = Sunday. Days before `minDate` (by calendar day) can't be selected. */
+export function MonthCalendar({ date = new Date(), onSelect, selectedDate, weekStart = 1, minDate }: { date?: Date; onSelect: (date: Date) => void; selectedDate?: Date | null; weekStart?: 0 | 1; minDate?: Date }) {
     const { t } = useTranslation();
     // Year and month to display
 
     const displayYear = date.getFullYear();
     const displayMonth = date.getMonth() + 1;
+    const minDay = minDate ? new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate()) : null;
 
     // First day of the month and total days
     const firstOfMonth = new Date(displayYear, displayMonth - 1, 1);
@@ -127,6 +128,7 @@ export function MonthCalendar({ date = new Date(), onSelect, selectedDate, weekS
                 <View key={wIdx} className="flex-row ">
                     {week.map((day, dIdx) => {
                         const isToday = selectedDate ? (new Date(displayYear, displayMonth - 1, day ?? 0)).toDateString() == selectedDate.toDateString() : null;
+                        const isDisabled = !!(day && minDay && new Date(displayYear, displayMonth - 1, day) < minDay);
 
                         return (
                             <View
@@ -138,10 +140,10 @@ export function MonthCalendar({ date = new Date(), onSelect, selectedDate, weekS
                                         className={`h-8 w-8 items-center justify-center rounded-full ${isToday ? 'bg-primary' : ''
                                             }`}
                                     >
-                                        <Pressable onPress={() => { onSelect(new Date(displayYear, displayMonth - 1, day, selectedDate ? selectedDate.getHours() : 0, selectedDate ? selectedDate.getMinutes() : 0, selectedDate ? selectedDate.getSeconds() : 0)) }}>
+                                        <Pressable disabled={isDisabled} accessibilityState={{ disabled: isDisabled }} onPress={() => { onSelect(new Date(displayYear, displayMonth - 1, day, selectedDate ? selectedDate.getHours() : 0, selectedDate ? selectedDate.getMinutes() : 0, selectedDate ? selectedDate.getSeconds() : 0)) }}>
                                             <Text
                                                 className={` text-base ${isToday ? 'text-white' : 'text-muted-foreground '
-                                                    }`}
+                                                    } ${isDisabled ? 'opacity-30' : ''}`}
                                             >
                                                 {day}
                                             </Text>
@@ -226,9 +228,13 @@ type DatePickerProps = {
     children?: ReactNode;
     /** Extra NeoButton props for the custom trigger. */
     buttonProps?: Record<string, any>;
+    /** Earliest selectable day. */
+    minDate?: Date;
+    /** Read-only: the calendar and time fields can't be opened or changed. */
+    disabled?: boolean;
 };
 
-export default function DatePicker({ name, value = '', type, onChange, children, buttonProps }: DatePickerProps) {
+export default function DatePicker({ name, value = '', type, onChange, children, buttonProps, minDate, disabled = false }: DatePickerProps) {
     const bIsTime = type === 'datetime';
     const timeMode = appSetting('forms', 'time');
     const { t } = useTranslation();
@@ -327,7 +333,7 @@ export default function DatePicker({ name, value = '', type, onChange, children,
                     <View className='  w-full '>
                         <CalendarHeader value={cValue} addMonth={addMonth} setDatePart={setDatePart} />
                         <View className='mt-4'>
-                            <MonthCalendar date={cValue} selectedDate={dValue} onSelect={onSelectDate} weekStart={weekStart} />
+                            <MonthCalendar date={cValue} selectedDate={dValue} onSelect={onSelectDate} weekStart={weekStart} minDate={minDate} />
                         </View>
 
                     </View>
@@ -339,15 +345,17 @@ export default function DatePicker({ name, value = '', type, onChange, children,
                     controlSize="small"
                     align="start"
                     {...buttonProps}
+                    disabled={disabled}
                     onPress={() => setShowModal(true)}
                     accessibilityLabel={dValue ? formatDate(dValue, t, dateDisplayOpts) : t('Select date')}
                 >
                     {children}
                 </NeoButton>
             ) : (
-            <Row className="w-full flex-nowrap items-center gap-3">
+            <Row className={`w-full flex-nowrap items-center gap-3 ${disabled ? 'opacity-50' : ''}`} pointerEvents={disabled ? 'none' : 'auto'}>
                 <Pressable
                     className="min-w-0 flex-1"
+                    disabled={disabled}
                     onPress={() => setShowModal(true)}
                     accessibilityRole="button"
                     accessibilityLabel={dValue ? formatDate(dValue, t, dateDisplayOpts) : t('Select date')}