New Date input (#368)

Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
Chirag Chhatrala
2024-04-15 18:39:19 +05:30
committed by GitHub
parent 1dd02cc147
commit 4f4f7128fa
10 changed files with 501 additions and 256 deletions

View File

@@ -1,38 +1,82 @@
<template>
<input-wrapper
v-bind="inputWrapperProps"
>
<InputWrapper v-bind="props">
<template #label>
<slot name="label" />
</template>
<div v-if="!dateRange" class="flex">
<input :id="id?id:name" v-model="fromDate" :type="useTime ? 'datetime-local' : 'date'" :class="inputClasses"
:disabled="disabled?true:null"
:style="inputStyle" :name="name" data-date-format="YYYY-MM-DD"
:min="setMinDate" :max="setMaxDate"
<UPopover
v-model:open="pickerOpen"
:disabled="props.disabled"
:popper="{ placement: 'bottom-start' }"
>
<button
class="cursor-pointer overflow-hidden"
:class="inputClasses"
:disabled="props.disabled"
>
</div>
<div v-else :class="inputClasses">
<div class="flex -mx-2">
<p class="text-gray-900 px-4">
From
</p>
<input :id="id?id:name" v-model="fromDate" :type="useTime ? 'datetime-local' : 'date'" :disabled="disabled?true:null"
:style="inputStyle" :name="name" data-date-format="YYYY-MM-DD"
class="flex-grow border-transparent focus:outline-none "
:min="setMinDate" :max="setMaxDate"
>
<p class="text-gray-900 px-4">
To
</p>
<input v-if="dateRange" :id="id?id:name" v-model="toDate" :type="useTime ? 'datetime-local' : 'date'"
:disabled="disabled?true:null"
:style="inputStyle" :name="name" class="flex-grow border-transparent focus:outline-none"
:min="setMinDate" :max="setMaxDate"
>
</div>
</div>
<div class="flex items-center min-w-0">
<div
class="flex-grow min-w-0 flex items-center gap-x-2"
:class="[
props.theme.default.inputSpacing.vertical,
props.theme.default.inputSpacing.horizontal,
{'hover:bg-gray-50 dark:hover:bg-gray-900': !props.disabled}
]"
>
<Icon
name="heroicons:calendar-20-solid"
class="w-4 h-4 flex-shrink-0"
dynamic
/>
<div class="flex-grow truncate overflow-hidden">
<p class="flex-grow truncate h-[24px]">
{{ formattedDatePreview }}
</p>
</div>
</div>
<button
v-if="fromDate && !props.disabled"
class="hover:bg-gray-50 dark:hover:bg-gray-900 border-l px-2"
:class="[props.theme.default.inputSpacing.vertical]"
@click.prevent="clear()"
>
<Icon
name="heroicons:x-mark-20-solid"
class="w-5 h-5 text-gray-500"
width="2em"
dynamic
/>
</button>
</div>
</button>
<template #panel="{ close }">
<DatePicker
v-if="props.dateRange"
v-model.range="modeledValue"
:mode="props.withTime ? 'dateTime' : 'date'"
is-required
borderless
:min-date="minDate"
:max-date="maxDate"
:is-dark="props.isDark"
color="form-color"
@close="close"
/>
<DatePicker
v-else
v-model="modeledValue"
:mode="props.withTime ? 'dateTime' : 'date'"
is-required
borderless
:min-date="minDate"
:max-date="maxDate"
:is-dark="props.isDark"
color="form-color"
@close="close"
/>
</template>
</UPopover>
<template #help>
<slot name="help" />
@@ -40,148 +84,173 @@
<template #error>
<slot name="error" />
</template>
</input-wrapper>
</InputWrapper>
</template>
<script>
<script setup>
import { inputProps, useFormInput } from './useFormInput.js'
import InputWrapper from './components/InputWrapper.vue'
import { getCurrentInstance } from 'vue'
import { DatePicker } from 'v-calendar'
import 'v-calendar/dist/style.css'
import { format } from 'date-fns'
import { tailwindcssPaletteGenerator } from '~/lib/colors.js'
export default {
name: 'DateInput',
components: { InputWrapper },
mixins: [],
const props = defineProps({
...inputProps,
withTime: { type: Boolean, default: false },
dateRange: { type: Boolean, default: false },
disablePastDates: { type: Boolean, default: false },
disableFutureDates: { type: Boolean, default: false },
dateFormat: { type: String, default: 'dd/MM/yyyy' },
outputDateFormat: { type: String, default: 'yyyy-MM-dd\'T\'HH:mm:ssXXX' },
isDark: { type: Boolean, default: false }
})
props: {
...inputProps,
withTime: { type: Boolean, default: false },
dateRange: { type: Boolean, default: false },
disablePastDates: { type: Boolean, default: false },
disableFutureDates: { type: Boolean, default: false }
const input = useFormInput(props, getCurrentInstance())
const fromDate = ref(null)
const toDate = ref(null)
const datepicker = ref(null)
const pickerOpen = ref(false)
const twColors = computed(() => {
return tailwindcssPaletteGenerator(props.color).primary
})
const modeledValue = computed({
get () {
return props.dateRange ? { start: fromDate.value, end: toDate.value } : fromDate.value
},
setup (props, context) {
return {
...useFormInput(props, context)
set (value) {
if (props.dateRange) {
fromDate.value = format(value.start, props.outputDateFormat)
toDate.value = format(value.end, props.outputDateFormat)
} else {
fromDate.value = format(value, props.outputDateFormat)
}
},
}
})
data: () => ({
fromDate: null,
toDate: null
}),
const inputClasses = computed(() => {
const classes = [props.theme.DateInput.input, 'w-full']
if (props.disabled) {
classes.push('!cursor-not-allowed dark:!bg-gray-600 !bg-gray-200')
}
if (input.hasError.value) {
computed: {
inputClasses () {
let str = 'border border-gray-300 dark:bg-notion-dark-light dark:border-gray-600 dark:placeholder-gray-500 dark:text-gray-300 flex-1 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-opacity-100 placeholder-gray-400 px-4 py-2 rounded-lg shadow-sm text-base text-black text-gray-700'
str += this.dateRange ? ' w-50' : ' w-full'
str += this.disabled ? ' !cursor-not-allowed !bg-gray-200' : ''
return str
},
useTime () {
return this.withTime && !this.dateRange
},
setMinDate () {
if (this.disablePastDates) {
return new Date().toISOString().split('T')[0]
}
return false
},
setMaxDate () {
if (this.disableFutureDates) {
return new Date().toISOString().split('T')[0]
}
return false
}
},
classes.push('!ring-red-500 !ring-2 !border-transparent')
}
return classes.join(' ')
})
watch: {
color: {
handler () {
this.setInputColor()
},
immediate: true
},
fromDate: {
handler (val) {
if (this.dateRange) {
if (!Array.isArray(this.compVal)) {
this.compVal = []
}
this.compVal[0] = this.dateToUTC(val)
} else {
this.compVal = this.dateToUTC(val)
}
},
immediate: false
},
toDate: {
handler (val) {
if (this.dateRange) {
if (!Array.isArray(this.compVal)) {
this.compVal = [null]
}
this.compVal[1] = this.dateToUTC(val)
} else {
this.compVal = null
}
},
immediate: false
}
},
mounted () {
if (this.compVal) {
if (Array.isArray(this.compVal)) {
this.fromDate = this.compVal[0] ?? null
this.toDate = this.compVal[1] ?? null
} else {
this.fromDate = this.dateToLocal(this.compVal)
}
}
this.setInputColor()
},
methods: {
/**
* Pressing enter won't submit form
* @param event
* @returns {boolean}
*/
onEnterPress (event) {
event.preventDefault()
return false
},
setInputColor () {
if (this.$refs.datepicker) {
const dateInput = this.$refs.datepicker.$el.getElementsByTagName('input')[0]
dateInput.style.setProperty('--tw-ring-color', this.color)
}
},
dateToUTC (val) {
if (!val) {
return null
}
if (!this.useTime) {
return val
}
return new Date(val).toISOString()
},
dateToLocal (val) {
if (!val) {
return null
}
const dateObj = new Date(val)
let dateStr = dateObj.getFullYear() + '-' +
String(dateObj.getMonth() + 1).padStart(2, '0') + '-' +
String(dateObj.getDate()).padStart(2, '0')
if (this.useTime) {
dateStr += 'T' + String(dateObj.getHours()).padStart(2, '0') + ':' +
String(dateObj.getMinutes()).padStart(2, '0')
}
return dateStr
const minDate = computed(() => {
if (props.disablePastDates) {
return new Date()
}
return undefined
})
const maxDate = computed(() => {
if (props.disableFutureDates) {
return new Date()
}
return undefined
})
const handleCompValChange = () => {
if (input.compVal.value) {
if (Array.isArray(input.compVal.value)) {
fromDate.value = input.compVal.value[0] ?? null
toDate.value = input.compVal.value[1] ?? null
} else {
fromDate.value = input.compVal.value
}
}
}
const setInputColor = () => {
if (datepicker.value) {
const dateInput = datepicker.value.$el.getElementsByTagName('input')[0]
dateInput.style.setProperty('--tw-ring-color', props.color)
}
}
const clear = () => {
fromDate.value = null
toDate.value = null
pickerOpen.value = false
}
const formattedDate = (value) => {
if (props.withTime) {
try {
return format(new Date(value), props.dateFormat + ' HH:mm')
} catch (e) {
return ''
}
}
try {
return format(new Date(value), props.dateFormat)
} catch (e) {
return ''
}
}
const formattedDatePreview = computed(() => {
if (!fromDate.value) return ''
if (props.dateRange) {
if (!toDate.value) return formattedDate(fromDate.value)
return `${formattedDate(fromDate.value)} - ${formattedDate(toDate.value)}`
}
return formattedDate(fromDate.value)
})
watch(() => props.color, () => {
setInputColor()
}, { immediate: true })
watch(() => props.dateRange, () => {
fromDate.value = null
toDate.value = null
}, { immediate: true })
watch(() => fromDate.value, (val) => {
if (props.dateRange) {
if (!Array.isArray(input.compVal.value)) input.compVal.value = []
input.compVal.value[0] = val
} else {
input.compVal.value = val
}
}, { immediate: false })
watch(() => toDate.value, (val) => {
if (props.dateRange) {
if (!Array.isArray(input.compVal.value)) input.compVal.value = [null]
input.compVal.value[1] = val
} else {
input.compVal.value = null
}
}, { immediate: false })
watch(() => input.compVal.value, (val, oldVal) => {
if (!oldVal) handleCompValChange()
}, { immediate: false })
onMounted(() => {
handleCompValChange()
setInputColor()
})
</script>
<style>
.vc-form-color {
--vc-accent-50: v-bind('twColors[50]');
--vc-accent-100: v-bind('twColors[100]');
--vc-accent-200: v-bind('twColors[200]');
--vc-accent-300: v-bind('twColors[300]');
--vc-accent-400: v-bind('twColors[400]');
--vc-accent-500: v-bind('twColors[500]');
--vc-accent-600: v-bind('twColors[600]');
--vc-accent-700: v-bind('twColors[700]');
--vc-accent-800: v-bind('twColors[800]');
--vc-accent-900: v-bind('twColors[900]');
}
</style>

View File

@@ -338,9 +338,8 @@ export default {
}
if (this.isPublicFormPage && this.form.editable_submissions) {
const urlParam = new URLSearchParams(window.location.search)
if (urlParam && urlParam.get('submission_id')) {
this.form.submission_id = urlParam.get('submission_id')
if (useRoute().query?.submission_id) {
this.form.submission_id = useRoute().query?.submission_id
const data = await this.getSubmissionData()
if (data !== null && data) {
this.dataForm = useForm(data)
@@ -353,15 +352,7 @@ export default {
if (pendingData !== null && pendingData && Object.keys(this.pendingSubmission.get()).length !== 0) {
this.fields.forEach((field) => {
if (field.type === 'date' && field.prefill_today === true) { // For Prefill with 'today'
const dateObj = new Date()
let currentDate = dateObj.getFullYear() + '-' +
String(dateObj.getMonth() + 1).padStart(2, '0') + '-' +
String(dateObj.getDate()).padStart(2, '0')
if (field.with_time === true) {
currentDate += 'T' + String(dateObj.getHours()).padStart(2, '0') + ':' +
String(dateObj.getMinutes()).padStart(2, '0')
}
pendingData[field.id] = currentDate
pendingData[field.id] = new Date().toISOString()
}
})
this.dataForm = useForm(pendingData)
@@ -395,15 +386,7 @@ export default {
// Array url prefills
formData[field.id] = urlPrefill.getAll(field.id + '[]')
} else if (field.type === 'date' && field.prefill_today === true) { // For Prefill with 'today'
const dateObj = new Date()
let currentDate = dateObj.getFullYear() + '-' +
String(dateObj.getMonth() + 1).padStart(2, '0') + '-' +
String(dateObj.getDate()).padStart(2, '0')
if (field.with_time === true) {
currentDate += 'T' + String(dateObj.getHours()).padStart(2, '0') + ':' +
String(dateObj.getMinutes()).padStart(2, '0')
}
formData[field.id] = currentDate
formData[field.id] = new Date().toISOString()
} else { // Default prefill if any
formData[field.id] = field.prefill
}

View File

@@ -56,6 +56,7 @@
<script>
import { computed } from 'vue'
import FormLogicPropertyResolver from "~/lib/forms/FormLogicPropertyResolver.js"
import { darkModeEnabled } from '~/lib/forms/public-page.js'
import { default as _has } from 'lodash/has'
export default {
@@ -176,6 +177,9 @@ export default {
},
fieldSideBarOpened() {
return this.adminPreview && (this.form && this.selectedFieldIndex !== null) ? (this.form.properties[this.selectedFieldIndex] && this.showEditFieldSidebar) : false
},
isDark () {
return this.form.dark_mode === 'dark' || this.form.dark_mode === 'auto' && darkModeEnabled()
}
},
@@ -245,7 +249,8 @@ export default {
uppercaseLabels: this.form.uppercase_labels == 1 || this.form.uppercase_labels == true,
theme: this.theme,
maxCharLimit: (field.max_char_limit) ? parseInt(field.max_char_limit) : 2000,
showCharLimit: field.show_char_limit || false
showCharLimit: field.show_char_limit || false,
isDark: this.isDark
}
if (['select', 'multi_select'].includes(field.type)) {
@@ -261,9 +266,11 @@ export default {
inputProperties.allowCreation = (field.allow_creation === true)
inputProperties.searchable = (inputProperties.options.length > 4)
} else if (field.type === 'date') {
inputProperties.dateFormat = field.date_format
if (field.with_time) {
inputProperties.withTime = true
} else if (field.date_range) {
}
if (field.date_range) {
inputProperties.dateRange = true
}
if (field.disable_past_dates) {

View File

@@ -8,18 +8,12 @@
<p class="text-gray-400 mb-2 text-xs">
Exclude this field or make it required.
</p>
<v-checkbox v-model="field.hidden" class="mb-3" :name="field.id + '_hidden'"
@update:model-value="onFieldHiddenChange">
Hidden
</v-checkbox>
<v-checkbox v-model="field.required" class="mb-3" :name="field.id + '_required'"
@update:model-value="onFieldRequiredChange">
Required
</v-checkbox>
<v-checkbox v-model="field.disabled" class="mb-3" :name="field.id + '_disabled'"
@update:model-value="onFieldDisabledChange">
Disabled
</v-checkbox>
<toggle-switch-input :form="field" name="required" label="Required"
@update:model-value="onFieldRequiredChange"/>
<toggle-switch-input :form="field" name="hidden" label="Hidden"
@update:model-value="onFieldHiddenChange"/>
<toggle-switch-input :form="field" name="disabled" label="Disabled"
@update:model-value="onFieldDisabledChange"/>
</div>
<!-- Checkbox -->
@@ -119,38 +113,53 @@
<h3 class="font-semibold block text-lg">
Date Options
</h3>
<v-checkbox v-model="field.date_range" class="mt-3" :name="field.id + '_date_range'"
@update:model-value="onFieldDateRangeChange">
Date Range
</v-checkbox>
<p class="text-gray-400 mb-3 text-xs">
Adds an end date. This cannot be used with the time option yet.
</p>
<v-checkbox v-model="field.with_time" :name="field.id + '_with_time'" @update:model-value="onFieldWithTimeChange">
Date with time
</v-checkbox>
<p class="text-gray-400 mb-3 text-xs">
Include time. Or not. This cannot be used with the date range option yet.
</p>
<select-input v-if="field.with_time" name="timezone" class="mt-3" :form="field" :options="timezonesOptions"
label="Timezone" :searchable="true" help="Make sure to select correct timezone. Leave blank otherwise." />
<v-checkbox v-model="field.prefill_today" name="prefill_today" @update:model-value="onFieldPrefillTodayChange">
Prefill with 'today'
</v-checkbox>
<p class="text-gray-400 mb-3 text-xs">
if enabled we will pre-fill this field with the current date
</p>
<v-checkbox v-model="field.disable_past_dates" name="disable_past_dates" class="mb-3"
@update:model-value="onFieldDisablePastDatesChange">
Disable past dates
</v-checkbox>
<v-checkbox v-model="field.disable_future_dates" name="disable_future_dates" class="mb-3"
@update:model-value="onFieldDisableFutureDatesChange">
Disable future dates
</v-checkbox>
<toggle-switch-input
:form="field"
class="mt-3"
name="date_range"
label="End date"
@update:model-value="onFieldDateRangeChange"
/>
<toggle-switch-input
:form="field"
name="prefill_today"
label="Prefill with 'today'"
@update:model-value="onFieldPrefillTodayChange"
/>
<toggle-switch-input
:form="field"
name="disable_past_dates"
label="Disable past dates"
@update:model-value="onFieldDisablePastDatesChange"
/>
<toggle-switch-input
:form="field"
name="disable_future_dates"
label="Disable future dates"
@update:model-value="onFieldDisableFutureDatesChange"
/>
<toggle-switch-input
:form="field"
name="with_time"
label="Include time"
/>
<select-input
v-if="field.with_time"
name="timezone"
class="mt-4"
:form="field"
:options="timezonesOptions"
label="Timezone"
:searchable="true"
help="Make sure to select the same timezone you're using in Notion. Leave blank otherwise."
/>
<flat-select-input
name="date_format"
class="mt-4"
:form="field"
:options="dateFormatOptions"
label="Date format"
/>
</div>
<!-- select/multiselect Options -->
@@ -268,7 +277,7 @@
<!-- Help -->
<rich-text-area-input name="help" class="mt-3" :form="field" :editor-toolbar="editorToolbarCustom"
label="Field Help" help="Your field help will be shown below/above the field, just like this message."
label="Field Help" help="Your field help will be shown below/above the field, just like this text."
:help-position="field.help_position" />
<select-input name="help_position" class="mt-3" :options="[
{ name: 'Below input', value: 'below_input' },
@@ -316,6 +325,7 @@ import timezones from '~/data/timezones.json'
import countryCodes from '~/data/country_codes.json'
import CountryFlag from 'vue-country-flag-next'
import FormBlockLogicEditor from '../../components/form-logic-components/FormBlockLogicEditor.vue'
import { format } from 'date-fns'
import { default as _has } from 'lodash/has'
export default {
@@ -370,6 +380,15 @@ export default {
}
})
},
dateFormatOptions () {
const date = new Date()
return ['dd/MM/yyyy', 'MM-dd-yyyy'].map(dateFormat => {
return {
name: format(date, dateFormat),
value: dateFormat
}
})
},
displayBasedOnAdvanced() {
if (this.field.generates_uuid || this.field.generates_auto_increment_id) {
return false
@@ -445,16 +464,9 @@ export default {
onFieldDateRangeChange(val) {
this.field.date_range = val
if (this.field.date_range) {
this.field.with_time = false
this.field.prefill_today = false
}
},
onFieldWithTimeChange(val) {
this.field.with_time = val
if (this.field.with_time) {
this.field.date_range = false
}
},
onFieldGenUIdChange(val) {
this.field.generates_uuid = val
if (this.field.generates_uuid) {
@@ -554,6 +566,9 @@ export default {
url: {
max_char_limit: 2000
},
date: {
date_format: this.dateFormatOptions[0].value
}
}
if (this.field.type in defaultFieldValues) {
Object.keys(defaultFieldValues[this.field.type]).forEach(key => {

View File

@@ -1,17 +1,23 @@
<template>
<span v-if="valueIsObject">
<template v-if="value[0]">{{ value[0] }}</template>
<template v-if="value[1]"><b>to</b> {{ value[1] }}</template>
<template v-if="value[0]">{{ formattedDate(value[0]) }}</template>
<template v-if="value[1]"><b class="mx-2">to</b>{{ formattedDate(value[1]) }}</template>
</span>
<span v-else>
{{ value }}
{{ formattedDate(value) }}
</span>
</template>
<script>
import { format } from 'date-fns'
import { default as _has } from 'lodash/has'
export default {
components: {},
props: {
property: {
required: true
},
value: {
required: true
}
@@ -30,6 +36,22 @@ export default {
mounted () {
},
methods: {
formattedDate(val) {
if (!val) return ''
const dateFormat = _has(this.property, 'date_format') ? this.property.date_format : 'dd/MM/yyyy'
if (this.property?.with_time) {
try {
return format(new Date(val), dateFormat + ' HH:mm')
} catch (e) {
return ''
}
}
try {
return format(new Date(val), dateFormat)
} catch (e) {
return ''
}
}
}
}
</script>