fix: replace native date inputs with custom DateInput/DateTimeInput components
Native <input type='date'> and <input type='datetime-local'> render their display format (mm/dd/yyyy vs dd/mm/yyyy, 12h vs 24h) based on browser/OS locale, ignoring HTML lang attributes in Firefox and inconsistently in Chrome. The previous lang=en-GB fix was unreliable. Create DateInput.svelte and DateTimeInput.svelte components that show dd/mm/yyyy (and DD/MM/YYYY HH:MM for datetime) by formatting the ISO value in JS, while delegating the actual picker to a hidden native input triggered via showPicker(). Supported in Chrome 99+, Firefox 101+, Safari 16+ (covers all modern browsers). Updated 8 component files across CollectionModal, ChecklistModal, NoteModal, ImmichSelect, CollectionMap, TransportationDetails, LodgingDetails, and LocationVisits.
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
let modal: HTMLDialogElement;
|
||||
import { t } from 'svelte-i18n';
|
||||
import DateInput from '$lib/components/DateInput.svelte';
|
||||
|
||||
import CheckboxIcon from '~icons/mdi/checkbox-multiple-marked-outline';
|
||||
|
||||
@@ -290,15 +291,14 @@
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
<input
|
||||
type="date"
|
||||
<DateInput
|
||||
id="date"
|
||||
name="date"
|
||||
readonly={isReadOnly}
|
||||
min={constrainDates ? collection.start_date : ''}
|
||||
max={constrainDates ? collection.end_date : ''}
|
||||
bind:value={newChecklist.date}
|
||||
class="input input-bordered w-full bg-base-100/80 focus:bg-base-100"
|
||||
inputClass="input input-bordered w-full bg-base-100/80 focus:bg-base-100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import MarkdownEditor from './MarkdownEditor.svelte';
|
||||
import DateInput from '$lib/components/DateInput.svelte';
|
||||
import { addToast } from '$lib/toasts';
|
||||
import { copyToClipboard } from '$lib/index';
|
||||
import type { Collection, ContentImage, SlimCollection } from '$lib/types';
|
||||
@@ -343,12 +344,10 @@
|
||||
{$t('adventures.start_date')}
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
<DateInput
|
||||
id="start_date"
|
||||
name="start_date"
|
||||
bind:value={collection.start_date}
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -360,12 +359,10 @@
|
||||
{$t('adventures.end_date')}
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
<DateInput
|
||||
id="end_date"
|
||||
name="end_date"
|
||||
bind:value={collection.end_date}
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
68
frontend/src/lib/components/DateInput.svelte
Normal file
68
frontend/src/lib/components/DateInput.svelte
Normal file
@@ -0,0 +1,68 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let value: string | null = ''; // ISO yyyy-mm-dd
|
||||
export let id: string = '';
|
||||
export let name: string = '';
|
||||
export let inputClass: string = 'input input-bordered w-full';
|
||||
export let disabled: boolean = false;
|
||||
export let min: string | null | undefined = undefined;
|
||||
export let max: string | null | undefined = undefined;
|
||||
export let required: boolean = false;
|
||||
export let readonly: boolean = false;
|
||||
|
||||
const dispatch = createEventDispatcher<{ change: Event }>();
|
||||
|
||||
let nativeInput: HTMLInputElement;
|
||||
|
||||
$: normalizedValue = value ?? '';
|
||||
$: normalizedMin = min ?? undefined;
|
||||
$: normalizedMax = max ?? undefined;
|
||||
|
||||
$: displayDate = formatDate(normalizedValue);
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
if (!iso || !/^\d{4}-\d{2}-\d{2}$/.test(iso)) return '';
|
||||
const [y, m, d] = iso.split('-');
|
||||
return `${d}/${m}/${y}`;
|
||||
}
|
||||
|
||||
function openPicker() {
|
||||
if (!disabled && !readonly) nativeInput?.showPicker?.();
|
||||
}
|
||||
|
||||
function handleChange(e: Event) {
|
||||
value = (e.currentTarget as HTMLInputElement).value;
|
||||
dispatch('change', e);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative w-full">
|
||||
<button
|
||||
type="button"
|
||||
class="{inputClass} text-left flex items-center justify-between"
|
||||
on:click={openPicker}
|
||||
disabled={disabled || readonly}
|
||||
>
|
||||
<span class={displayDate ? '' : 'opacity-40'}>{displayDate || 'DD/MM/YYYY'}</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 opacity-60" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 18H5V8h14v13zM7 10h5v5H7z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<input
|
||||
bind:this={nativeInput}
|
||||
type="date"
|
||||
{id}
|
||||
{name}
|
||||
value={normalizedValue}
|
||||
min={normalizedMin}
|
||||
max={normalizedMax}
|
||||
{required}
|
||||
{readonly}
|
||||
on:change={handleChange}
|
||||
{disabled}
|
||||
tabindex="-1"
|
||||
aria-hidden="true"
|
||||
style="position:absolute;opacity:0;width:1px;height:1px;top:0;left:0;pointer-events:none;z-index:-1"
|
||||
/>
|
||||
</div>
|
||||
72
frontend/src/lib/components/DateTimeInput.svelte
Normal file
72
frontend/src/lib/components/DateTimeInput.svelte
Normal file
@@ -0,0 +1,72 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let value: string | null = ''; // ISO yyyy-mm-ddTHH:MM
|
||||
export let id: string = '';
|
||||
export let name: string = '';
|
||||
export let inputClass: string = 'input input-bordered w-full';
|
||||
export let disabled: boolean = false;
|
||||
export let min: string | null | undefined = undefined;
|
||||
export let max: string | null | undefined = undefined;
|
||||
export let required: boolean = false;
|
||||
export let readonly: boolean = false;
|
||||
|
||||
const dispatch = createEventDispatcher<{ change: Event }>();
|
||||
|
||||
let nativeInput: HTMLInputElement;
|
||||
|
||||
$: normalizedValue = value ?? '';
|
||||
$: normalizedMin = min ?? undefined;
|
||||
$: normalizedMax = max ?? undefined;
|
||||
|
||||
$: displayDateTime = formatDateTime(normalizedValue);
|
||||
|
||||
function formatDateTime(iso: string): string {
|
||||
if (!iso) return '';
|
||||
const [datePart, timePart] = iso.split('T');
|
||||
if (!datePart) return '';
|
||||
const [y, m, d] = datePart.split('-');
|
||||
if (!y || !m || !d) return '';
|
||||
const timeStr = timePart ? timePart.slice(0, 5) : '';
|
||||
return timeStr ? `${d}/${m}/${y} ${timeStr}` : `${d}/${m}/${y}`;
|
||||
}
|
||||
|
||||
function openPicker() {
|
||||
if (!disabled && !readonly) nativeInput?.showPicker?.();
|
||||
}
|
||||
|
||||
function handleChange(e: Event) {
|
||||
value = (e.currentTarget as HTMLInputElement).value;
|
||||
dispatch('change', e);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative w-full">
|
||||
<button
|
||||
type="button"
|
||||
class="{inputClass} text-left flex items-center justify-between"
|
||||
on:click={openPicker}
|
||||
disabled={disabled || readonly}
|
||||
>
|
||||
<span class={displayDateTime ? '' : 'opacity-40'}>{displayDateTime || 'DD/MM/YYYY HH:MM'}</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 opacity-60" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 18H5V8h14v13zM7 10h5v5H7z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<input
|
||||
bind:this={nativeInput}
|
||||
type="datetime-local"
|
||||
{id}
|
||||
{name}
|
||||
value={normalizedValue}
|
||||
min={normalizedMin}
|
||||
max={normalizedMax}
|
||||
{required}
|
||||
{readonly}
|
||||
on:change={handleChange}
|
||||
{disabled}
|
||||
tabindex="-1"
|
||||
aria-hidden="true"
|
||||
style="position:absolute;opacity:0;width:1px;height:1px;top:0;left:0;pointer-events:none;z-index:-1"
|
||||
/>
|
||||
</div>
|
||||
@@ -3,6 +3,7 @@
|
||||
import { t } from 'svelte-i18n';
|
||||
import CheckIcon from '~icons/mdi/check';
|
||||
import CloseIcon from '~icons/mdi/close';
|
||||
import DateInput from '$lib/components/DateInput.svelte';
|
||||
import type { ImmichAlbum } from '$lib/types';
|
||||
import { debounce } from '$lib';
|
||||
|
||||
@@ -26,16 +27,18 @@
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
// Reactive statements
|
||||
$: {
|
||||
if (searchCategory === 'album' && currentAlbum) {
|
||||
immichImages = [];
|
||||
fetchAlbumAssets(currentAlbum);
|
||||
} else if (searchCategory === 'date' && selectedDate) {
|
||||
clearAlbumSelection();
|
||||
searchImmich();
|
||||
} else if (searchCategory === 'search') {
|
||||
clearAlbumSelection();
|
||||
}
|
||||
$: if (searchCategory === 'album' && currentAlbum) {
|
||||
immichImages = [];
|
||||
fetchAlbumAssets(currentAlbum);
|
||||
}
|
||||
|
||||
$: if (searchCategory === 'date' && selectedDate) {
|
||||
clearAlbumSelection();
|
||||
searchImmich();
|
||||
}
|
||||
|
||||
$: if (searchCategory === 'search') {
|
||||
clearAlbumSelection();
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
@@ -149,7 +152,11 @@
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
const searchImmich = debounce(() => {
|
||||
function searchImmich() {
|
||||
debouncedSearchImmich();
|
||||
}
|
||||
|
||||
const debouncedSearchImmich = debounce(() => {
|
||||
_searchImmich();
|
||||
}, 500);
|
||||
|
||||
@@ -245,11 +252,10 @@
|
||||
</div>
|
||||
{:else if searchCategory === 'date'}
|
||||
<div class="flex gap-2 items-center">
|
||||
<input
|
||||
<DateInput
|
||||
id="date-picker"
|
||||
type="date"
|
||||
bind:value={selectedDate}
|
||||
class="input input-bordered flex-1"
|
||||
inputClass="input input-bordered flex-1"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import MarkdownEditor from './MarkdownEditor.svelte';
|
||||
import DateInput from '$lib/components/DateInput.svelte';
|
||||
let modal: HTMLDialogElement;
|
||||
import { marked } from 'marked'; // Import the markdown parser
|
||||
|
||||
@@ -306,15 +307,14 @@
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
<input
|
||||
type="date"
|
||||
<DateInput
|
||||
id="date"
|
||||
name="date"
|
||||
readonly={isReadOnly}
|
||||
min={constrainDates ? collection.start_date : ''}
|
||||
max={constrainDates ? collection.end_date : ''}
|
||||
bind:value={newNote.date}
|
||||
class="input input-bordered w-full bg-base-100/80 focus:bg-base-100"
|
||||
inputClass="input input-bordered w-full bg-base-100/80 focus:bg-base-100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import PinIcon from '~icons/mdi/map-marker';
|
||||
import Clear from '~icons/mdi/close';
|
||||
import NewLocationModal from '$lib/components/locations/LocationModal.svelte';
|
||||
import DateInput from '$lib/components/DateInput.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { get as getStore } from 'svelte/store';
|
||||
import type { Collection, Location, User } from '$lib/types';
|
||||
@@ -855,26 +856,28 @@
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<label class="form-control">
|
||||
<div class="form-control">
|
||||
<span class="label label-text text-xs">{$t('adventures.start_date')}</span>
|
||||
<input
|
||||
type="date"
|
||||
<DateInput
|
||||
id="start-date-filter"
|
||||
name="start-date-filter"
|
||||
bind:value={startDateFilter}
|
||||
class="input input-sm input-bordered w-full"
|
||||
inputClass="input input-sm input-bordered w-full"
|
||||
min={collectionStartDateISO}
|
||||
max={collectionEndDateISO}
|
||||
/>
|
||||
</label>
|
||||
<label class="form-control">
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<span class="label label-text text-xs">{$t('adventures.end_date')}</span>
|
||||
<input
|
||||
type="date"
|
||||
<DateInput
|
||||
id="end-date-filter"
|
||||
name="end-date-filter"
|
||||
bind:value={endDateFilter}
|
||||
class="input input-sm input-bordered w-full"
|
||||
inputClass="input input-sm input-bordered w-full"
|
||||
min={collectionStartDateISO}
|
||||
max={collectionEndDateISO}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Routes & Activities Filter -->
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
TransportationVisit
|
||||
} from '$lib/types';
|
||||
import TimezoneSelector from '../TimezoneSelector.svelte';
|
||||
import DateInput from '$lib/components/DateInput.svelte';
|
||||
import DateTimeInput from '$lib/components/DateTimeInput.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { updateLocalDate, updateUTCDate, validateDateRange, formatUTCDate } from '$lib/dateUtils';
|
||||
import { onMount } from 'svelte';
|
||||
@@ -818,20 +820,18 @@
|
||||
{typeConfig.startLabel}
|
||||
</label>
|
||||
{#if allDay}
|
||||
<input
|
||||
<DateInput
|
||||
id="start-date-input"
|
||||
type="date"
|
||||
class="input input-bordered w-full mt-1"
|
||||
inputClass="input input-bordered w-full mt-1"
|
||||
bind:value={localStartDate}
|
||||
on:change={handleLocalDateChange}
|
||||
min={constrainDates ? constraintStartDate : ''}
|
||||
max={constrainDates ? constraintEndDate : ''}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
<DateTimeInput
|
||||
id="start-date-input"
|
||||
type="datetime-local"
|
||||
class="input input-bordered w-full mt-1"
|
||||
inputClass="input input-bordered w-full mt-1"
|
||||
bind:value={localStartDate}
|
||||
on:change={handleLocalDateChange}
|
||||
min={constrainDates ? constraintStartDate : ''}
|
||||
@@ -847,20 +847,18 @@
|
||||
{typeConfig.endLabel}
|
||||
</label>
|
||||
{#if allDay}
|
||||
<input
|
||||
<DateInput
|
||||
id="end-date-input"
|
||||
type="date"
|
||||
class="input input-bordered w-full mt-1"
|
||||
inputClass="input input-bordered w-full mt-1"
|
||||
bind:value={localEndDate}
|
||||
on:change={handleLocalDateChange}
|
||||
min={constrainDates ? localStartDate : ''}
|
||||
max={constrainDates ? constraintEndDate : ''}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
<DateTimeInput
|
||||
id="end-date-input"
|
||||
type="datetime-local"
|
||||
class="input input-bordered w-full mt-1"
|
||||
inputClass="input input-bordered w-full mt-1"
|
||||
bind:value={localEndDate}
|
||||
on:change={handleLocalDateChange}
|
||||
min={constrainDates ? localStartDate : ''}
|
||||
@@ -1193,13 +1191,12 @@
|
||||
class="label-text text-xs font-medium"
|
||||
for="start-date-{visit.id}">{$t('adventures.start_date')}</label
|
||||
>
|
||||
<input
|
||||
id="start-date-{visit.id}"
|
||||
type="datetime-local"
|
||||
class="input input-bordered input-sm w-full mt-1"
|
||||
bind:value={activityForm.start_date}
|
||||
readonly={!!pendingStravaImport[visit.id]}
|
||||
/>
|
||||
<DateTimeInput
|
||||
id="start-date-{visit.id}"
|
||||
inputClass="input input-bordered input-sm w-full mt-1"
|
||||
bind:value={activityForm.start_date}
|
||||
readonly={!!pendingStravaImport[visit.id]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Elevation Gain -->
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
import MarkdownEditor from '../MarkdownEditor.svelte';
|
||||
import TimezoneSelector from '../TimezoneSelector.svelte';
|
||||
import MoneyInput from '../shared/MoneyInput.svelte';
|
||||
import DateInput from '$lib/components/DateInput.svelte';
|
||||
import DateTimeInput from '$lib/components/DateTimeInput.svelte';
|
||||
import { DEFAULT_CURRENCY, normalizeMoneyPayload, toMoneyValue } from '$lib/money';
|
||||
// @ts-ignore
|
||||
import { DateTime } from 'luxon';
|
||||
@@ -733,20 +735,18 @@
|
||||
<span class="label-text font-medium">{$t('adventures.check_in')}</span>
|
||||
</label>
|
||||
{#if allDay}
|
||||
<input
|
||||
<DateInput
|
||||
id="check-in"
|
||||
type="date"
|
||||
class="input input-bordered bg-base-100/80 focus:bg-base-100"
|
||||
inputClass="input input-bordered bg-base-100/80 focus:bg-base-100"
|
||||
bind:value={localStartDate}
|
||||
on:change={handleLocalDateChange}
|
||||
min={constrainDates ? constraintStartDate : undefined}
|
||||
max={constrainDates ? constraintEndDate : undefined}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
<DateTimeInput
|
||||
id="check-in"
|
||||
type="datetime-local"
|
||||
class="input input-bordered bg-base-100/80 focus:bg-base-100"
|
||||
inputClass="input input-bordered bg-base-100/80 focus:bg-base-100"
|
||||
bind:value={localStartDate}
|
||||
on:change={handleLocalDateChange}
|
||||
min={constrainDates ? constraintStartDate : undefined}
|
||||
@@ -761,20 +761,18 @@
|
||||
<span class="label-text font-medium">{$t('adventures.check_out')}</span>
|
||||
</label>
|
||||
{#if allDay}
|
||||
<input
|
||||
<DateInput
|
||||
id="check-out"
|
||||
type="date"
|
||||
class="input input-bordered bg-base-100/80 focus:bg-base-100"
|
||||
inputClass="input input-bordered bg-base-100/80 focus:bg-base-100"
|
||||
bind:value={localEndDate}
|
||||
on:change={handleLocalDateChange}
|
||||
min={constrainDates ? constraintStartDate : undefined}
|
||||
max={constrainDates ? constraintEndDate : undefined}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
<DateTimeInput
|
||||
id="check-out"
|
||||
type="datetime-local"
|
||||
class="input input-bordered bg-base-100/80 focus:bg-base-100"
|
||||
inputClass="input input-bordered bg-base-100/80 focus:bg-base-100"
|
||||
bind:value={localEndDate}
|
||||
on:change={handleLocalDateChange}
|
||||
min={constrainDates ? constraintStartDate : undefined}
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
import MarkdownEditor from '../MarkdownEditor.svelte';
|
||||
import TimezoneSelector from '../TimezoneSelector.svelte';
|
||||
import MoneyInput from '../shared/MoneyInput.svelte';
|
||||
import DateInput from '$lib/components/DateInput.svelte';
|
||||
import DateTimeInput from '$lib/components/DateTimeInput.svelte';
|
||||
import { DEFAULT_CURRENCY, normalizeMoneyPayload, toMoneyValue } from '$lib/money';
|
||||
// @ts-ignore
|
||||
import { DateTime } from 'luxon';
|
||||
@@ -868,20 +870,18 @@
|
||||
<span class="label-text font-medium">{$t('transportation.departure_date')}</span>
|
||||
</label>
|
||||
{#if allDay}
|
||||
<input
|
||||
<DateInput
|
||||
id="departure-date"
|
||||
type="date"
|
||||
class="input input-bordered bg-base-100/80 focus:bg-base-100"
|
||||
inputClass="input input-bordered bg-base-100/80 focus:bg-base-100"
|
||||
bind:value={localStartDate}
|
||||
on:change={handleLocalDateChange}
|
||||
min={constrainDates ? constraintStartDate : undefined}
|
||||
max={constrainDates ? constraintEndDate : undefined}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
<DateTimeInput
|
||||
id="departure-date"
|
||||
type="datetime-local"
|
||||
class="input input-bordered bg-base-100/80 focus:bg-base-100"
|
||||
inputClass="input input-bordered bg-base-100/80 focus:bg-base-100"
|
||||
bind:value={localStartDate}
|
||||
on:change={handleLocalDateChange}
|
||||
min={constrainDates ? constraintStartDate : undefined}
|
||||
@@ -896,20 +896,18 @@
|
||||
<span class="label-text font-medium">{$t('transportation.arrival_date')}</span>
|
||||
</label>
|
||||
{#if allDay}
|
||||
<input
|
||||
<DateInput
|
||||
id="arrival-date"
|
||||
type="date"
|
||||
class="input input-bordered bg-base-100/80 focus:bg-base-100"
|
||||
inputClass="input input-bordered bg-base-100/80 focus:bg-base-100"
|
||||
bind:value={localEndDate}
|
||||
on:change={handleLocalDateChange}
|
||||
min={constrainDates ? constraintStartDate : undefined}
|
||||
max={constrainDates ? constraintEndDate : undefined}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
<DateTimeInput
|
||||
id="arrival-date"
|
||||
type="datetime-local"
|
||||
class="input input-bordered bg-base-100/80 focus:bg-base-100"
|
||||
inputClass="input input-bordered bg-base-100/80 focus:bg-base-100"
|
||||
bind:value={localEndDate}
|
||||
on:change={handleLocalDateChange}
|
||||
min={constrainDates ? constraintStartDate : undefined}
|
||||
|
||||
Reference in New Issue
Block a user