add time formats

pull/641/head
Pete Matsyburka 2 weeks ago
parent 82500c2d7d
commit c7afe33c72

@ -161,9 +161,7 @@ module Api
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter) submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
end end
submitter.values = submitter.values.transform_values do |v| submitter.values = Submitters::SubmitValues.replace_current_date_placeholders(submitter)
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
end
end end
submitter submitter

@ -527,7 +527,8 @@ export default {
try { try {
return this.formatDate( return this.formatDate(
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue), this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY') this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'),
{ withTimePlaceholders: this.modelValue === '{{date}}' }
) )
} catch { } catch {
return this.modelValue return this.modelValue
@ -646,36 +647,55 @@ export default {
return number return number
} }
}, },
formatDate (date, format) { formatDate (date, format, { withTimePlaceholders = false } = {}) {
const monthFormats = { const monthFormats = { M: 'numeric', MM: '2-digit', MMM: 'short', MMMM: 'long' }
M: 'numeric', const dayFormats = { D: 'numeric', DD: '2-digit' }
MM: '2-digit', const yearFormats = { YYYY: 'numeric', YYY: 'numeric', YY: '2-digit' }
MMM: 'short', const hourFormats = { H: 'numeric', HH: '2-digit', h: 'numeric', hh: '2-digit' }
MMMM: 'long' const minuteFormats = { m: 'numeric', mm: '2-digit' }
} const secondFormats = { s: 'numeric', ss: '2-digit' }
const hasTime = /[HhAasz]/.test(format)
const dayFormats = { const opts = {
D: 'numeric', day: dayFormats[format.match(/D+/)],
DD: '2-digit' month: monthFormats[format.match(/M+/)],
year: yearFormats[format.match(/Y+/)]
} }
const yearFormats = { if (format.match(/H+/)) { opts.hour = hourFormats[format.match(/H+/)[0]]; opts.hour12 = false }
YYYY: 'numeric', if (format.match(/h+/)) { opts.hour = hourFormats[format.match(/h+/)[0]]; opts.hour12 = true }
YYY: 'numeric', if (/[Aa]/.test(format) && opts.hour12 === undefined) opts.hour12 = true
YY: '2-digit' if (format.match(/m+/)) opts.minute = minuteFormats[format.match(/m+/)[0]]
if (format.match(/s+/)) opts.second = secondFormats[format.match(/s+/)[0]]
if (/z/.test(format)) opts.timeZoneName = 'short'
if (!hasTime) opts.timeZone = 'UTC'
const partTypes = {
M: 'month',
D: 'day',
Y: 'year',
H: 'hour',
h: 'hour',
m: 'minute',
s: 'second',
z: 'timeZoneName',
A: 'dayPeriod',
a: 'dayPeriod'
} }
const parts = new Intl.DateTimeFormat([], { const parts = new Intl.DateTimeFormat([], opts).formatToParts(date)
day: dayFormats[format.match(/D+/)],
month: monthFormats[format.match(/M+/)], return format.replace(/MMMM|MMM|MM|M|DD|D|YYYY|YYY|YY|HH|hh|H|h|mm|m|ss|s|A|a|z/g, (token) => {
year: yearFormats[format.match(/Y+/)], if (withTimePlaceholders && /^(HH|hh|H|h|mm|m|ss|s|A|a)$/.test(token)) return '--'
timeZone: 'UTC'
}).formatToParts(date) const value = parts.find((p) => p.type === partTypes[token[0]])?.value
return format if (token === 'A') return (value || '').toUpperCase()
.replace(/D+/, parts.find((p) => p.type === 'day').value) if (token === 'a') return (value || '').toLowerCase()
.replace(/M+/, parts.find((p) => p.type === 'month').value)
.replace(/Y+/, parts.find((p) => p.type === 'year').value) return value
})
}, },
updateMultipleSelectValue (value) { updateMultipleSelectValue (value) {
if (this.modelValue?.includes(value)) { if (this.modelValue?.includes(value)) {

@ -56,12 +56,18 @@
class="base-input !text-2xl text-center w-full" class="base-input !text-2xl text-center w-full"
:required="field.required" :required="field.required"
:aria-describedby="field.description ? field.uuid + '-desc' : undefined" :aria-describedby="field.description ? field.uuid + '-desc' : undefined"
type="date" :type="inputType"
:name="`values[${field.uuid}]`" :name="formatType === 'datetime' ? undefined : `values[${field.uuid}]`"
@keydown.enter="onEnter" @keydown.enter="onEnter"
@focus="$emit('focus')" @focus="$emit('focus')"
@paste="onPaste" @paste="onPaste"
> >
<input
v-if="formatType === 'datetime'"
type="hidden"
:name="`values[${field.uuid}]`"
:value="modelValue"
>
</div> </div>
</div> </div>
</template> </template>
@ -97,14 +103,19 @@ export default {
}, },
emits: ['update:model-value', 'focus', 'submit'], emits: ['update:model-value', 'focus', 'submit'],
computed: { computed: {
dateNowString () { formatType () {
const today = new Date() const format = this.field.preferences?.format || ''
const yyyy = today.getFullYear() if (/[HhAasz]/.test(format)) return 'datetime'
const mm = String(today.getMonth() + 1).padStart(2, '0') if (format && !/[Dd]/.test(format)) return 'month'
const dd = String(today.getDate()).padStart(2, '0')
return `${yyyy}-${mm}-${dd}` return 'date'
},
inputType () {
return { datetime: 'datetime-local', month: 'month', date: 'date' }[this.formatType]
},
dateNowString () {
return this.formatDateValue(new Date())
}, },
validationMin () { validationMin () {
if (this.field.validation?.min) { if (this.field.validation?.min) {
@ -121,6 +132,8 @@ export default {
} }
}, },
withToday () { withToday () {
if (this.formatType === 'datetime') return false
const todayDate = new Date().setHours(0, 0, 0, 0) const todayDate = new Date().setHours(0, 0, 0, 0)
if (this.validationMin) { if (this.validationMin) {
@ -137,9 +150,25 @@ export default {
}, },
value: { value: {
set (value) { set (value) {
if (this.formatType === 'datetime' && value) {
const d = new Date(value)
if (!isNaN(d)) {
this.$emit('update:model-value', d.toISOString())
return
}
}
this.$emit('update:model-value', value) this.$emit('update:model-value', value)
}, },
get () { get () {
if (this.formatType === 'datetime') {
const d = new Date(this.modelValue)
return isNaN(d) ? '' : this.formatDateValue(d)
}
return this.modelValue return this.modelValue
} }
} }
@ -163,20 +192,32 @@ export default {
const parsedDate = new Date(pasteData) const parsedDate = new Date(pasteData)
if (!isNaN(parsedDate)) { if (isNaN(parsedDate)) return
const inputEl = this.$refs.input
inputEl.valueAsDate = new Date(parsedDate.getTime() - parsedDate.getTimezoneOffset() * 60000)
inputEl.dispatchEvent(new Event('input', { bubbles: true })) this.setInputValue(parsedDate)
}
}, },
setCurrentDate () { setCurrentDate () {
this.setInputValue(new Date())
},
setInputValue (date) {
const inputEl = this.$refs.input const inputEl = this.$refs.input
inputEl.valueAsDate = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000) if (this.formatType === 'date') {
inputEl.valueAsDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000)
} else {
inputEl.value = this.formatDateValue(date)
}
inputEl.dispatchEvent(new Event('input', { bubbles: true })) inputEl.dispatchEvent(new Event('input', { bubbles: true }))
},
formatDateValue (date) {
const pad = (n) => String(n).padStart(2, '0')
const ymd = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
if (this.formatType === 'month') return ymd.slice(0, 7)
if (this.formatType === 'datetime') return `${ymd}T${pad(date.getHours())}:${pad(date.getMinutes())}`
return ymd
} }
} }
} }

@ -137,7 +137,7 @@
<span <span
v-else-if="field.default_value === '{{date}}'" v-else-if="field.default_value === '{{date}}'"
> >
{{ t('signing_date') }} {{ /[HhAasz]/.test(field.preferences?.format || '') ? t('signing_date_and_time') : t('signing_date') }}
</span> </span>
<div <div
v-else-if="field.type === 'cells' && field.default_value" v-else-if="field.type === 'cells' && field.default_value"

@ -669,6 +669,7 @@ export default {
locale: this.locale, locale: this.locale,
baseFetch: this.baseFetch, baseFetch: this.baseFetch,
fieldTypes: this.fieldTypes, fieldTypes: this.fieldTypes,
dateFormats: this.dateFormats,
backgroundColor: this.backgroundColor, backgroundColor: this.backgroundColor,
withPhone: this.withPhone, withPhone: this.withPhone,
withVerification: this.withVerification, withVerification: this.withVerification,
@ -823,6 +824,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
dateFormats: {
type: Array,
required: false,
default: () => []
},
defaultSubmitters: { defaultSubmitters: {
type: Array, type: Array,
required: false, required: false,
@ -1030,6 +1036,8 @@ export default {
return isMobileSafariIos || /android|iphone|ipad/i.test(navigator.userAgent) return isMobileSafariIos || /android|iphone|ipad/i.test(navigator.userAgent)
}, },
defaultDateFormat () { defaultDateFormat () {
if (this.dateFormats.length) return this.dateFormats[0]
const isUsBrowser = Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') const isUsBrowser = Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US')
const isUsTimezone = new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()).match(/\s(?:CST|CDT|PST|PDT|EST|EDT)$/) const isUsTimezone = new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()).match(/\s(?:CST|CDT|PST|PDT|EST|EDT)$/)

@ -119,7 +119,7 @@
@change="[schema.format = $event.target.value, save()]" @change="[schema.format = $event.target.value, save()]"
> >
<option <option
v-for="format in dateFormats" v-for="format in availableDateFormats"
:key="format" :key="format"
:value="format" :value="format"
>{{ formatDate(new Date(), format) }}</option> >{{ formatDate(new Date(), format) }}</option>
@ -248,7 +248,7 @@ export default {
FieldType, FieldType,
IconSettings IconSettings
}, },
inject: ['t', 'save', 'backgroundColor'], inject: ['t', 'save', 'backgroundColor', 'dateFormats'],
provide () { provide () {
return { return {
fieldTypes: ['text', 'number', 'date', 'checkbox', 'radio', 'select'] fieldTypes: ['text', 'number', 'date', 'checkbox', 'radio', 'select']
@ -318,20 +318,23 @@ export default {
'space' 'space'
] ]
}, },
dateFormats () { availableDateFormats () {
const formats = [ const formats = this.dateFormats.length
'MM/DD/YYYY', ? [...this.dateFormats]
'DD/MM/YYYY', : [
'YYYY-MM-DD', 'MM/DD/YYYY',
'DD-MM-YYYY', 'DD/MM/YYYY',
'DD.MM.YYYY', 'YYYY-MM-DD',
'MMM D, YYYY', 'DD-MM-YYYY',
'MMMM D, YYYY', 'DD.MM.YYYY',
'D MMM YYYY', 'MMM D, YYYY',
'D MMMM YYYY' 'MMMM D, YYYY',
] 'MMMM YYYY',
'D MMM YYYY',
'D MMMM YYYY'
]
if (Intl.DateTimeFormat().resolvedOptions().timeZone?.includes('Seoul') || navigator.language?.startsWith('ko')) { if (!this.dateFormats.length && (Intl.DateTimeFormat().resolvedOptions().timeZone?.includes('Seoul') || navigator.language?.startsWith('ko'))) {
formats.push('YYYY년 MM월 DD일') formats.push('YYYY년 MM월 DD일')
} }
@ -401,18 +404,47 @@ export default {
formatDate (date, format) { formatDate (date, format) {
const monthFormats = { M: 'numeric', MM: '2-digit', MMM: 'short', MMMM: 'long' } const monthFormats = { M: 'numeric', MM: '2-digit', MMM: 'short', MMMM: 'long' }
const dayFormats = { D: 'numeric', DD: '2-digit' } const dayFormats = { D: 'numeric', DD: '2-digit' }
const yearFormats = { YYYY: 'numeric', YY: '2-digit' } const yearFormats = { YYYY: 'numeric', YYY: 'numeric', YY: '2-digit' }
const hourFormats = { H: 'numeric', HH: '2-digit', h: 'numeric', hh: '2-digit' }
const minuteFormats = { m: 'numeric', mm: '2-digit' }
const secondFormats = { s: 'numeric', ss: '2-digit' }
const parts = new Intl.DateTimeFormat([], { const opts = {
day: dayFormats[format.match(/D+/)], day: dayFormats[format.match(/D+/)],
month: monthFormats[format.match(/M+/)], month: monthFormats[format.match(/M+/)],
year: yearFormats[format.match(/Y+/)] year: yearFormats[format.match(/Y+/)]
}).formatToParts(date) }
if (format.match(/H+/)) { opts.hour = hourFormats[format.match(/H+/)[0]]; opts.hour12 = false }
if (format.match(/h+/)) { opts.hour = hourFormats[format.match(/h+/)[0]]; opts.hour12 = true }
if (/[Aa]/.test(format) && opts.hour12 === undefined) opts.hour12 = true
if (format.match(/m+/)) opts.minute = minuteFormats[format.match(/m+/)[0]]
if (format.match(/s+/)) opts.second = secondFormats[format.match(/s+/)[0]]
if (/z/.test(format)) opts.timeZoneName = 'short'
const partTypes = {
M: 'month',
D: 'day',
Y: 'year',
H: 'hour',
h: 'hour',
m: 'minute',
s: 'second',
z: 'timeZoneName',
A: 'dayPeriod',
a: 'dayPeriod'
}
return format const parts = new Intl.DateTimeFormat([], opts).formatToParts(date)
.replace(/D+/, parts.find((p) => p.type === 'day').value)
.replace(/M+/, parts.find((p) => p.type === 'month').value) return format.replace(/MMMM|MMM|MM|M|DD|D|YYYY|YYY|YY|HH|hh|H|h|mm|m|ss|s|A|a|z/g, (token) => {
.replace(/Y+/, parts.find((p) => p.type === 'year').value) const value = parts.find((p) => p.type === partTypes[token[0]])?.value
if (token === 'A') return (value || '').toUpperCase()
if (token === 'a') return (value || '').toLowerCase()
return value
})
}, },
closeDropdown () { closeDropdown () {
this.$el.getRootNode().activeElement.blur() this.$el.getRootNode().activeElement.blur()

@ -345,7 +345,7 @@ export default {
IconMathFunction, IconMathFunction,
FieldType FieldType
}, },
inject: ['template', 'backgroundColor', 'selectedAreasRef', 't', 'locale', 'getFieldTypeIndex'], inject: ['template', 'backgroundColor', 'selectedAreasRef', 't', 'locale', 'getFieldTypeIndex', 'dateFormats'],
props: { props: {
field: { field: {
type: Object, type: Object,
@ -428,7 +428,8 @@ export default {
if (this.field.type === 'date') { if (this.field.type === 'date') {
this.field.preferences.format ||= this.field.preferences.format ||=
({ 'de-DE': 'DD.MM.YYYY' }[this.locale] || ((Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') || new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()).match(/\s(?:CST|CDT|PST|PDT|EST|EDT)$/)) ? 'MM/DD/YYYY' : 'DD/MM/YYYY')) this.dateFormats[0] ||
({ 'de-DE': 'DD.MM.YYYY' }[this.locale] || ((Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') || new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()).match(/\s(?:CST|CDT|PST|PDT|EST|EDT)$/)) ? 'MM/DD/YYYY' : 'DD/MM/YYYY'))
} }
}, },
methods: { methods: {

@ -514,7 +514,7 @@ export default {
ContextSubmenu, ContextSubmenu,
ContextModal ContextModal
}, },
inject: ['t', 'getFieldTypeIndex', 'template', 'withCustomFields', 'currencies'], inject: ['t', 'getFieldTypeIndex', 'template', 'withCustomFields', 'currencies', 'dateFormats'],
props: { props: {
contextMenu: { contextMenu: {
type: Object, type: Object,
@ -580,7 +580,7 @@ export default {
fieldNames: FieldType.computed.fieldNames, fieldNames: FieldType.computed.fieldNames,
fieldLabels: FieldType.computed.fieldLabels, fieldLabels: FieldType.computed.fieldLabels,
validationOptions: FieldSettings.computed.validations, validationOptions: FieldSettings.computed.validations,
dateFormats: FieldSettings.computed.dateFormats, availableDateFormats: FieldSettings.computed.availableDateFormats,
numberFormats: FieldSettings.computed.numberFormats, numberFormats: FieldSettings.computed.numberFormats,
prefillableFieldTypes: FieldSettings.computed.prefillableFieldTypes, prefillableFieldTypes: FieldSettings.computed.prefillableFieldTypes,
verificationMethods: FieldSettings.computed.verificationMethods, verificationMethods: FieldSettings.computed.verificationMethods,
@ -686,7 +686,7 @@ export default {
}, },
formatOptions () { formatOptions () {
switch (this.field.type) { switch (this.field.type) {
case 'date': return this.dateFormats.map(f => ({ value: f, label: this.formatDate(new Date(), f) })) case 'date': return this.availableDateFormats.map(f => ({ value: f, label: this.formatDate(new Date(), f) }))
case 'number': return this.numberFormats.map(f => ({ value: f, label: this.formatNumber(123456789.567, f) })) case 'number': return this.numberFormats.map(f => ({ value: f, label: this.formatNumber(123456789.567, f) }))
case 'signature': return this.signatureFormats.map(f => ({ value: f, label: this.t(f) })) case 'signature': return this.signatureFormats.map(f => ({ value: f, label: this.t(f) }))
default: return [] default: return []

@ -290,7 +290,7 @@
@change="$emit('save')" @change="$emit('save')"
> >
<option <option
v-for="format in dateFormats" v-for="format in availableDateFormats"
:key="format" :key="format"
:value="format" :value="format"
> >
@ -610,7 +610,7 @@ export default {
IconTypography, IconTypography,
IconX IconX
}, },
inject: ['template', 't'], inject: ['template', 't', 'dateFormats'],
props: { props: {
field: { field: {
type: Object, type: Object,
@ -701,20 +701,23 @@ export default {
'space' 'space'
] ]
}, },
dateFormats () { availableDateFormats () {
const formats = [ const formats = this.dateFormats.length
'MM/DD/YYYY', ? [...this.dateFormats]
'DD/MM/YYYY', : [
'YYYY-MM-DD', 'MM/DD/YYYY',
'DD-MM-YYYY', 'DD/MM/YYYY',
'DD.MM.YYYY', 'YYYY-MM-DD',
'MMM D, YYYY', 'DD-MM-YYYY',
'MMMM D, YYYY', 'DD.MM.YYYY',
'D MMM YYYY', 'MMM D, YYYY',
'D MMMM YYYY' 'MMMM D, YYYY',
] 'MMMM YYYY',
'D MMM YYYY',
'D MMMM YYYY'
]
if (Intl.DateTimeFormat().resolvedOptions().timeZone?.includes('Seoul') || navigator.language?.startsWith('ko')) { if (!this.dateFormats.length && (Intl.DateTimeFormat().resolvedOptions().timeZone?.includes('Seoul') || navigator.language?.startsWith('ko'))) {
formats.push('YYYY년 MM월 DD일') formats.push('YYYY년 MM월 DD일')
} }
@ -819,33 +822,49 @@ export default {
return pattern?.match(/^\.{(?<min>\d+),(?<max>\d+)?}$/)?.groups || null return pattern?.match(/^\.{(?<min>\d+),(?<max>\d+)?}$/)?.groups || null
}, },
formatDate (date, format) { formatDate (date, format) {
const monthFormats = { const monthFormats = { M: 'numeric', MM: '2-digit', MMM: 'short', MMMM: 'long' }
M: 'numeric', const dayFormats = { D: 'numeric', DD: '2-digit' }
MM: '2-digit', const yearFormats = { YYYY: 'numeric', YYY: 'numeric', YY: '2-digit' }
MMM: 'short', const hourFormats = { H: 'numeric', HH: '2-digit', h: 'numeric', hh: '2-digit' }
MMMM: 'long' const minuteFormats = { m: 'numeric', mm: '2-digit' }
} const secondFormats = { s: 'numeric', ss: '2-digit' }
const dayFormats = { const opts = {
D: 'numeric', day: dayFormats[format.match(/D+/)],
DD: '2-digit' month: monthFormats[format.match(/M+/)],
year: yearFormats[format.match(/Y+/)]
} }
const yearFormats = { if (format.match(/H+/)) { opts.hour = hourFormats[format.match(/H+/)[0]]; opts.hour12 = false }
YYYY: 'numeric', if (format.match(/h+/)) { opts.hour = hourFormats[format.match(/h+/)[0]]; opts.hour12 = true }
YY: '2-digit' if (/[Aa]/.test(format) && opts.hour12 === undefined) opts.hour12 = true
if (format.match(/m+/)) opts.minute = minuteFormats[format.match(/m+/)[0]]
if (format.match(/s+/)) opts.second = secondFormats[format.match(/s+/)[0]]
if (/z/.test(format)) opts.timeZoneName = 'short'
const partTypes = {
M: 'month',
D: 'day',
Y: 'year',
H: 'hour',
h: 'hour',
m: 'minute',
s: 'second',
z: 'timeZoneName',
A: 'dayPeriod',
a: 'dayPeriod'
} }
const parts = new Intl.DateTimeFormat([], { const parts = new Intl.DateTimeFormat([], opts).formatToParts(date)
day: dayFormats[format.match(/D+/)],
month: monthFormats[format.match(/M+/)],
year: yearFormats[format.match(/Y+/)]
}).formatToParts(date)
return format return format.replace(/MMMM|MMM|MM|M|DD|D|YYYY|YYY|YY|HH|hh|H|h|mm|m|ss|s|A|a|z/g, (token) => {
.replace(/D+/, parts.find((p) => p.type === 'day').value) const value = parts.find((p) => p.type === partTypes[token[0]])?.value
.replace(/M+/, parts.find((p) => p.type === 'month').value)
.replace(/Y+/, parts.find((p) => p.type === 'year').value) if (token === 'A') return (value || '').toUpperCase()
if (token === 'a') return (value || '').toLowerCase()
return value
})
} }
} }
} }

@ -70,6 +70,7 @@ const en = {
sign_yourself: 'Sign Yourself', sign_yourself: 'Sign Yourself',
set_signing_date: 'Set signing date', set_signing_date: 'Set signing date',
signing_date: 'Signing Date', signing_date: 'Signing Date',
signing_date_and_time: 'Signing Date and Time',
send: 'Send', send: 'Send',
remove: 'Remove', remove: 'Remove',
edit: 'Edit', edit: 'Edit',
@ -271,6 +272,7 @@ const es = {
with_logo: 'Con logotipo', with_logo: 'Con logotipo',
description: 'Descripción', description: 'Descripción',
signing_date: 'Fecha de Firma', signing_date: 'Fecha de Firma',
signing_date_and_time: 'Fecha y Hora de Firma',
display_title: 'Título de visualización', display_title: 'Título de visualización',
unchecked: 'No marcado', unchecked: 'No marcado',
price: 'Precio', price: 'Precio',
@ -508,6 +510,7 @@ const it = {
sign_yourself: 'Firma te stesso', sign_yourself: 'Firma te stesso',
set_signing_date: 'Imposta data di firma', set_signing_date: 'Imposta data di firma',
signing_date: 'Data di firma', signing_date: 'Data di firma',
signing_date_and_time: 'Data e ora di firma',
send: 'Invia', send: 'Invia',
remove: 'Rimuovi', remove: 'Rimuovi',
edit: 'Modifica', edit: 'Modifica',
@ -710,6 +713,7 @@ const pt = {
description: 'Descrição', description: 'Descrição',
display_title: 'Título de exibição', display_title: 'Título de exibição',
signing_date: 'Data da Assinatura', signing_date: 'Data da Assinatura',
signing_date_and_time: 'Data e Hora da Assinatura',
unchecked: 'Não marcado', unchecked: 'Não marcado',
price: 'Preço', price: 'Preço',
equal: 'Igual', equal: 'Igual',
@ -946,6 +950,7 @@ const fr = {
sign_yourself: 'Signer vous-même', sign_yourself: 'Signer vous-même',
set_signing_date: 'Définir la date de signature', set_signing_date: 'Définir la date de signature',
signing_date: 'Date de signature', signing_date: 'Date de signature',
signing_date_and_time: 'Date et heure de signature',
send: 'Envoyer', send: 'Envoyer',
remove: 'Supprimer', remove: 'Supprimer',
edit: 'Modifier', edit: 'Modifier',
@ -1165,6 +1170,7 @@ const de = {
sign_yourself: 'Selbst unterschreiben', sign_yourself: 'Selbst unterschreiben',
set_signing_date: 'Unterzeichnungsdatum festlegen', set_signing_date: 'Unterzeichnungsdatum festlegen',
signing_date: 'Unterzeichnungsdatum', signing_date: 'Unterzeichnungsdatum',
signing_date_and_time: 'Unterzeichnungsdatum und -uhrzeit',
send: 'Senden', send: 'Senden',
remove: 'Entfernen', remove: 'Entfernen',
edit: 'Bearbeiten', edit: 'Bearbeiten',
@ -1384,6 +1390,7 @@ const nl = {
sign_yourself: 'Zelf ondertekenen', sign_yourself: 'Zelf ondertekenen',
set_signing_date: 'Ondertekeningsdatum instellen', set_signing_date: 'Ondertekeningsdatum instellen',
signing_date: 'Ondertekeningsdatum', signing_date: 'Ondertekeningsdatum',
signing_date_and_time: 'Ondertekeningsdatum en -tijd',
send: 'Verzenden', send: 'Verzenden',
remove: 'Verwijderen', remove: 'Verwijderen',
edit: 'Bewerken', edit: 'Bewerken',

@ -73,8 +73,8 @@
<% elsif field['type'] == 'date' %> <% elsif field['type'] == 'date' %>
<autosize-field></autosize-field> <autosize-field></autosize-field>
<div class="flex w-full px-0.5 <%= valign == 'top' ? 'items-start' : (valign == 'bottom' ? 'items-end' : 'items-center') %>"> <div class="flex w-full px-0.5 <%= valign == 'top' ? 'items-start' : (valign == 'bottom' ? 'items-end' : 'items-center') %>">
<% value = Time.current.in_time_zone(local_assigns[:timezone]).to_date.to_s if value == '{{date}}' %> <% tz = local_assigns[:with_submitter_timezone] ? (submitter.timezone.presence || local_assigns[:timezone]) : local_assigns[:timezone] %>
<div class="w-full"><%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), local_assigns[:locale]) %></div> <div class="w-full"><%= value == '{{date}}' ? TimeUtils.format_date_preview(field.dig('preferences', 'format'), local_assigns[:locale], tz) : TimeUtils.format_date_string(value, field.dig('preferences', 'format'), local_assigns[:locale], timezone: tz) %></div>
</div> </div>
<% elsif field['type'] == 'number' %> <% elsif field['type'] == 'number' %>
<autosize-field></autosize-field> <autosize-field></autosize-field>

@ -281,7 +281,7 @@
<% if field['type'] == 'number' %> <% if field['type'] == 'number' %>
<% value = NumberUtils.format_number(value, field.dig('preferences', 'format')) %> <% value = NumberUtils.format_number(value, field.dig('preferences', 'format')) %>
<% elsif field['type'] == 'date' %> <% elsif field['type'] == 'date' %>
<% value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), @submission.account.locale) %> <% value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), @submission.account.locale, timezone: (with_submitter_timezone ? (submitter.timezone.presence || @submission.account.timezone) : @submission.account.timezone)) %>
<% end %> <% end %>
<% if (mask = field.dig('preferences', 'mask').presence) %> <% if (mask = field.dig('preferences', 'mask').presence) %>
<% if signed_in? && can?(:read, @submission) %> <% if signed_in? && can?(:read, @submission) %>

@ -410,9 +410,7 @@ module Submissions
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter) submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
end end
submitter.values = submitter.values.transform_values do |v| submitter.values = Submitters::SubmitValues.replace_current_date_placeholders(submitter)
v == '{{date}}' ? Time.current.in_time_zone(submitter.submission.account.timezone).to_date.to_s : v
end
submitter submitter
end end

@ -415,7 +415,8 @@ module Submissions
composer.formatted_text_box([{ text: value.to_s.titleize }], padding: [0, 0, 10, 0]) composer.formatted_text_box([{ text: value.to_s.titleize }], padding: [0, 0, 10, 0])
else else
if field['type'] == 'date' if field['type'] == 'date'
value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), account.locale) value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), account.locale,
timezone:)
end end
value = NumberUtils.format_number(value, field.dig('preferences', 'format')) if field['type'] == 'number' value = NumberUtils.format_number(value, field.dig('preferences', 'format')) if field['type'] == 'number'

@ -652,7 +652,10 @@ module Submissions
end end
else else
if field['type'] == 'date' if field['type'] == 'date'
value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), locale) timezone = submitter.account.timezone
timezone = submitter.timezone || submitter.account.timezone if with_submitter_timezone
value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), locale, timezone:)
end end
value = NumberUtils.format_number(value, field.dig('preferences', 'format')) if field['type'] == 'number' value = NumberUtils.format_number(value, field.dig('preferences', 'format')) if field['type'] == 'number'

@ -103,17 +103,43 @@ module Submitters
end end
def normalize_date(field, value) def normalize_date(field, value)
if value.is_a?(Integer) format = field.dig('preferences', 'format')
if TimeUtils.format_with_time?(format)
normalize_date_time(value, format)
elsif TimeUtils.month_only_format?(format)
normalize_date_month(value, format)
elsif value.is_a?(Integer)
Time.zone.at(value.to_s.first(10).to_i).to_date.to_s Time.zone.at(value.to_s.first(10).to_i).to_date.to_s
elsif value.gsub(/\w/, '0') == field.dig('preferences', 'format').to_s.gsub(/\w/, '0') elsif value.gsub(/\w/, '0') == format.to_s.gsub(/\w/, '0')
TimeUtils.parse_date_string(value, field.dig('preferences', 'format')).to_s TimeUtils.parse_date_string(value, format).to_s
else else
Date.parse(value).to_s Date.parse(value).to_s
end end
rescue Date::Error rescue ArgumentError
value value
end end
def normalize_date_time(value, format)
if value.is_a?(Integer)
Time.zone.at(value.to_s.first(10).to_i).utc.iso8601
elsif value.to_s.match?(/T\d{2}:\d{2}/)
Time.iso8601(value).utc.iso8601
else
TimeUtils.parse_date_string(value, format).utc.iso8601
end
end
def normalize_date_month(value, format)
if value.is_a?(Integer)
Time.zone.at(value.to_s.first(10).to_i).strftime('%Y-%m')
elsif value.to_s.match?(/\A\d{4}-\d{2}\z/)
value
else
TimeUtils.parse_date_string(value, format).strftime('%Y-%m')
end
end
def fetch_fields(template, submitter_name: nil, for_submitter: nil) def fetch_fields(template, submitter_name: nil, for_submitter: nil)
if submitter_name && !for_submitter if submitter_name && !for_submitter
submitter = submitter =

@ -86,9 +86,7 @@ module Submitters
submitter.values = maybe_remove_condition_values(submitter, required_field_uuids_acc:) submitter.values = maybe_remove_condition_values(submitter, required_field_uuids_acc:)
end end
submitter.values = submitter.values.transform_values do |v| submitter.values = replace_current_date_placeholders(submitter)
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
end
required_field_uuids_acc.each do |uuid| required_field_uuids_acc.each do |uuid|
next if submitter.values[uuid].present? next if submitter.values[uuid].present?
@ -194,7 +192,7 @@ module Submitters
next if value.blank? next if value.blank?
acc[field['uuid']] = template_default_value_for_submitter(value, submitter, with_time: true) acc[field['uuid']] = template_default_value_for_submitter(value, submitter, field:, with_time: true)
end end
default_values.compact_blank.merge(submitter.values) default_values.compact_blank.merge(submitter.values)
@ -248,7 +246,20 @@ module Submitters
0 0
end end
def template_default_value_for_submitter(value, submitter, with_time: false) def replace_current_date_placeholders(submitter)
submitter.values.each_with_object({}) do |(uuid, v), acc|
acc[uuid] =
if v == '{{date}}'
field = submitter.submission.fields_uuid_index[uuid]
TimeUtils.current_date_value(field&.dig('preferences', 'format'), submitter.account.timezone)
else
v
end
end
end
def template_default_value_for_submitter(value, submitter, with_time: false, field: nil)
return if value.blank? return if value.blank?
return if submitter.blank? return if submitter.blank?
@ -257,7 +268,8 @@ module Submitters
replace_default_variables(value, replace_default_variables(value,
submitter.attributes.merge('role' => role), submitter.attributes.merge('role' => role),
submitter.submission, submitter.submission,
with_time:) with_time:,
field:)
end end
def maybe_remove_condition_values(submitter, required_field_uuids_acc: nil) def maybe_remove_condition_values(submitter, required_field_uuids_acc: nil)
@ -394,7 +406,7 @@ module Submitters
end end
# rubocop:enable Metrics # rubocop:enable Metrics
def replace_default_variables(value, attrs, submission, with_time: false) def replace_default_variables(value, attrs, submission, with_time: false, field: nil)
return value if value.in?([true, false]) || value.is_a?(Numeric) || value.is_a?(Array) return value if value.in?([true, false]) || value.is_a?(Numeric) || value.is_a?(Array)
return if value.blank? return if value.blank?
@ -412,7 +424,7 @@ module Submitters
when 'hour', 'minute', 'day', 'month', 'year' when 'hour', 'minute', 'day', 'month', 'year'
with_time ? Time.current.in_time_zone(submission.account.timezone).strftime(STRFTIME_MAP[key]) : e with_time ? Time.current.in_time_zone(submission.account.timezone).strftime(STRFTIME_MAP[key]) : e
when 'date' when 'date'
with_time ? Time.current.in_time_zone(submission.account.timezone).to_date.to_s : e with_time ? TimeUtils.current_date_value(field&.dig('preferences', 'format'), submission.account.timezone) : e
when 'role', 'email', 'phone', 'name' when 'role', 'email', 'phone', 'name'
attrs[key] || e attrs[key] || e
else else

@ -19,6 +19,47 @@ module TimeUtils
'YY' => '%y' 'YY' => '%y'
}.freeze }.freeze
HOUR_FORMATS = {
'H' => '%-H',
'HH' => '%H',
'h' => '%-I',
'hh' => '%I'
}.freeze
MINUTE_FORMATS = {
'm' => '%-M',
'mm' => '%M'
}.freeze
SECOND_FORMATS = {
's' => '%-S',
'ss' => '%S'
}.freeze
AMPM_FORMATS = {
'A' => '%p',
'a' => '%P'
}.freeze
TIMEZONE_FORMATS = {
'z' => '%Z'
}.freeze
TIME_FORMATS = HOUR_FORMATS.merge(MINUTE_FORMATS)
.merge(SECOND_FORMATS)
.merge(AMPM_FORMATS)
.freeze
ALL_FORMATS = MONTH_FORMATS.merge(DAY_FORMATS)
.merge(YEAR_FORMATS)
.merge(TIME_FORMATS)
.merge(TIMEZONE_FORMATS)
.freeze
TOKEN_REGEX = /MMMM|MMM|MM|M|DD|D|YYYY|YYY|YY|HH|hh|H|h|mm|m|ss|s|A|a|z/
MONTH_ONLY_VALUE_REGEX = /\A\d{4}-\d{2}\z/
DEFAULT_DATE_FORMAT_US = 'MM/DD/YYYY' DEFAULT_DATE_FORMAT_US = 'MM/DD/YYYY'
DEFAULT_DATE_FORMAT = 'DD/MM/YYYY' DEFAULT_DATE_FORMAT = 'DD/MM/YYYY'
@ -42,27 +83,60 @@ module TimeUtils
end end
end end
def parse_date_string(string, pattern) def format_with_time?(format)
pattern = pattern.sub(/Y+/, YEAR_FORMATS) format.to_s.match?(/[HhAasz]/)
.sub(/M+/, MONTH_FORMATS) end
.sub(/D+/, DAY_FORMATS)
Date.strptime(string, pattern) def month_only_format?(format)
format.to_s.present? && !format.to_s.match?(/[DdHhAasz]/)
end end
def format_date_string(string, format, locale) def format_date_preview(format, locale, timezone)
date = Date.parse(string.to_s) return '' if format.blank?
format = format.upcase if format format = format.upcase unless format_with_time?(format)
preview_pattern = format.gsub(TOKEN_REGEX) { |token| TIME_FORMATS.key?(token) ? '--' : ALL_FORMATS[token] }
I18n.l(Time.current.in_time_zone(timezone.presence || Time.zone.name), format: preview_pattern, locale:)
end
def current_date_value(format, timezone)
tz = timezone.presence || Time.zone.name
if format_with_time?(format)
Time.current.utc.iso8601
elsif month_only_format?(format)
Time.current.in_time_zone(tz).strftime('%Y-%m')
else
Time.current.in_time_zone(tz).to_date.to_s
end
end
def parse_date_string(string, pattern)
with_time = format_with_time?(pattern)
pattern = pattern.upcase unless with_time
pattern = pattern.gsub(TOKEN_REGEX, ALL_FORMATS)
with_time ? Time.zone.strptime(string, pattern) : Date.strptime(string, pattern)
end
def format_date_string(string, format, locale, timezone: nil)
format = format.upcase if format && !format_with_time?(format)
format ||= locale.to_s.ends_with?('US') ? DEFAULT_DATE_FORMAT_US : DEFAULT_DATE_FORMAT format ||= locale.to_s.ends_with?('US') ? DEFAULT_DATE_FORMAT_US : DEFAULT_DATE_FORMAT
i18n_format = format.sub(/D+/) { DAY_FORMATS[format[/D+/]] } date =
.sub(/M+/) { MONTH_FORMATS[format[/M+/]] } if format_with_time?(format)
.sub(/Y+/) { YEAR_FORMATS[format[/Y+/]] } Time.iso8601(string.to_s).in_time_zone(timezone.presence || Time.zone.name)
elsif string.to_s.match?(MONTH_ONLY_VALUE_REGEX)
year, month = string.to_s.split('-').map(&:to_i)
Date.new(year, month, 1)
else
Date.parse(string.to_s)
end
I18n.l(date, format: i18n_format, locale:) I18n.l(date, format: format.gsub(TOKEN_REGEX, ALL_FORMATS), locale:)
rescue Date::Error rescue ArgumentError
string string
end end
end end

Loading…
Cancel
Save