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)
end
submitter.values = submitter.values.transform_values do |v|
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
end
submitter.values = Submitters::SubmitValues.replace_current_date_placeholders(submitter)
end
submitter

@ -527,7 +527,8 @@ export default {
try {
return this.formatDate(
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 {
return this.modelValue
@ -646,36 +647,55 @@ export default {
return number
}
},
formatDate (date, format) {
const monthFormats = {
M: 'numeric',
MM: '2-digit',
MMM: 'short',
MMMM: 'long'
}
formatDate (date, format, { withTimePlaceholders = false } = {}) {
const monthFormats = { M: 'numeric', MM: '2-digit', MMM: 'short', MMMM: 'long' }
const dayFormats = { D: 'numeric', DD: '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 hasTime = /[HhAasz]/.test(format)
const dayFormats = {
D: 'numeric',
DD: '2-digit'
const opts = {
day: dayFormats[format.match(/D+/)],
month: monthFormats[format.match(/M+/)],
year: yearFormats[format.match(/Y+/)]
}
const yearFormats = {
YYYY: 'numeric',
YYY: 'numeric',
YY: '2-digit'
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'
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([], {
day: dayFormats[format.match(/D+/)],
month: monthFormats[format.match(/M+/)],
year: yearFormats[format.match(/Y+/)],
timeZone: 'UTC'
}).formatToParts(date)
const parts = new Intl.DateTimeFormat([], opts).formatToParts(date)
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) => {
if (withTimePlaceholders && /^(HH|hh|H|h|mm|m|ss|s|A|a)$/.test(token)) return '--'
const value = parts.find((p) => p.type === partTypes[token[0]])?.value
return format
.replace(/D+/, parts.find((p) => p.type === 'day').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
})
},
updateMultipleSelectValue (value) {
if (this.modelValue?.includes(value)) {

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

@ -669,6 +669,7 @@ export default {
locale: this.locale,
baseFetch: this.baseFetch,
fieldTypes: this.fieldTypes,
dateFormats: this.dateFormats,
backgroundColor: this.backgroundColor,
withPhone: this.withPhone,
withVerification: this.withVerification,
@ -823,6 +824,11 @@ export default {
required: false,
default: () => []
},
dateFormats: {
type: Array,
required: false,
default: () => []
},
defaultSubmitters: {
type: Array,
required: false,
@ -1030,6 +1036,8 @@ export default {
return isMobileSafariIos || /android|iphone|ipad/i.test(navigator.userAgent)
},
defaultDateFormat () {
if (this.dateFormats.length) return this.dateFormats[0]
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)$/)

@ -119,7 +119,7 @@
@change="[schema.format = $event.target.value, save()]"
>
<option
v-for="format in dateFormats"
v-for="format in availableDateFormats"
:key="format"
:value="format"
>{{ formatDate(new Date(), format) }}</option>
@ -248,7 +248,7 @@ export default {
FieldType,
IconSettings
},
inject: ['t', 'save', 'backgroundColor'],
inject: ['t', 'save', 'backgroundColor', 'dateFormats'],
provide () {
return {
fieldTypes: ['text', 'number', 'date', 'checkbox', 'radio', 'select']
@ -318,8 +318,10 @@ export default {
'space'
]
},
dateFormats () {
const formats = [
availableDateFormats () {
const formats = this.dateFormats.length
? [...this.dateFormats]
: [
'MM/DD/YYYY',
'DD/MM/YYYY',
'YYYY-MM-DD',
@ -327,11 +329,12 @@ export default {
'DD.MM.YYYY',
'MMM D, 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일')
}
@ -401,18 +404,47 @@ export default {
formatDate (date, format) {
const monthFormats = { M: 'numeric', MM: '2-digit', MMM: 'short', MMMM: 'long' }
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+/)],
month: monthFormats[format.match(/M+/)],
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
.replace(/D+/, parts.find((p) => p.type === 'day').value)
.replace(/M+/, parts.find((p) => p.type === 'month').value)
.replace(/Y+/, parts.find((p) => p.type === 'year').value)
const parts = new Intl.DateTimeFormat([], opts).formatToParts(date)
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) => {
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 () {
this.$el.getRootNode().activeElement.blur()

@ -345,7 +345,7 @@ export default {
IconMathFunction,
FieldType
},
inject: ['template', 'backgroundColor', 'selectedAreasRef', 't', 'locale', 'getFieldTypeIndex'],
inject: ['template', 'backgroundColor', 'selectedAreasRef', 't', 'locale', 'getFieldTypeIndex', 'dateFormats'],
props: {
field: {
type: Object,
@ -428,6 +428,7 @@ export default {
if (this.field.type === 'date') {
this.field.preferences.format ||=
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'))
}
},

@ -514,7 +514,7 @@ export default {
ContextSubmenu,
ContextModal
},
inject: ['t', 'getFieldTypeIndex', 'template', 'withCustomFields', 'currencies'],
inject: ['t', 'getFieldTypeIndex', 'template', 'withCustomFields', 'currencies', 'dateFormats'],
props: {
contextMenu: {
type: Object,
@ -580,7 +580,7 @@ export default {
fieldNames: FieldType.computed.fieldNames,
fieldLabels: FieldType.computed.fieldLabels,
validationOptions: FieldSettings.computed.validations,
dateFormats: FieldSettings.computed.dateFormats,
availableDateFormats: FieldSettings.computed.availableDateFormats,
numberFormats: FieldSettings.computed.numberFormats,
prefillableFieldTypes: FieldSettings.computed.prefillableFieldTypes,
verificationMethods: FieldSettings.computed.verificationMethods,
@ -686,7 +686,7 @@ export default {
},
formatOptions () {
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 'signature': return this.signatureFormats.map(f => ({ value: f, label: this.t(f) }))
default: return []

@ -290,7 +290,7 @@
@change="$emit('save')"
>
<option
v-for="format in dateFormats"
v-for="format in availableDateFormats"
:key="format"
:value="format"
>
@ -610,7 +610,7 @@ export default {
IconTypography,
IconX
},
inject: ['template', 't'],
inject: ['template', 't', 'dateFormats'],
props: {
field: {
type: Object,
@ -701,8 +701,10 @@ export default {
'space'
]
},
dateFormats () {
const formats = [
availableDateFormats () {
const formats = this.dateFormats.length
? [...this.dateFormats]
: [
'MM/DD/YYYY',
'DD/MM/YYYY',
'YYYY-MM-DD',
@ -710,11 +712,12 @@ export default {
'DD.MM.YYYY',
'MMM D, 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일')
}
@ -819,33 +822,49 @@ export default {
return pattern?.match(/^\.{(?<min>\d+),(?<max>\d+)?}$/)?.groups || null
},
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 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 dayFormats = {
D: 'numeric',
DD: '2-digit'
const opts = {
day: dayFormats[format.match(/D+/)],
month: monthFormats[format.match(/M+/)],
year: yearFormats[format.match(/Y+/)]
}
const yearFormats = {
YYYY: 'numeric',
YY: '2-digit'
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'
}
const parts = new Intl.DateTimeFormat([], {
day: dayFormats[format.match(/D+/)],
month: monthFormats[format.match(/M+/)],
year: yearFormats[format.match(/Y+/)]
}).formatToParts(date)
const parts = new Intl.DateTimeFormat([], opts).formatToParts(date)
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) => {
const value = parts.find((p) => p.type === partTypes[token[0]])?.value
return format
.replace(/D+/, parts.find((p) => p.type === 'day').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',
set_signing_date: 'Set signing date',
signing_date: 'Signing Date',
signing_date_and_time: 'Signing Date and Time',
send: 'Send',
remove: 'Remove',
edit: 'Edit',
@ -271,6 +272,7 @@ const es = {
with_logo: 'Con logotipo',
description: 'Descripción',
signing_date: 'Fecha de Firma',
signing_date_and_time: 'Fecha y Hora de Firma',
display_title: 'Título de visualización',
unchecked: 'No marcado',
price: 'Precio',
@ -508,6 +510,7 @@ const it = {
sign_yourself: 'Firma te stesso',
set_signing_date: 'Imposta data di firma',
signing_date: 'Data di firma',
signing_date_and_time: 'Data e ora di firma',
send: 'Invia',
remove: 'Rimuovi',
edit: 'Modifica',
@ -710,6 +713,7 @@ const pt = {
description: 'Descrição',
display_title: 'Título de exibição',
signing_date: 'Data da Assinatura',
signing_date_and_time: 'Data e Hora da Assinatura',
unchecked: 'Não marcado',
price: 'Preço',
equal: 'Igual',
@ -946,6 +950,7 @@ const fr = {
sign_yourself: 'Signer vous-même',
set_signing_date: 'Définir la date de signature',
signing_date: 'Date de signature',
signing_date_and_time: 'Date et heure de signature',
send: 'Envoyer',
remove: 'Supprimer',
edit: 'Modifier',
@ -1165,6 +1170,7 @@ const de = {
sign_yourself: 'Selbst unterschreiben',
set_signing_date: 'Unterzeichnungsdatum festlegen',
signing_date: 'Unterzeichnungsdatum',
signing_date_and_time: 'Unterzeichnungsdatum und -uhrzeit',
send: 'Senden',
remove: 'Entfernen',
edit: 'Bearbeiten',
@ -1384,6 +1390,7 @@ const nl = {
sign_yourself: 'Zelf ondertekenen',
set_signing_date: 'Ondertekeningsdatum instellen',
signing_date: 'Ondertekeningsdatum',
signing_date_and_time: 'Ondertekeningsdatum en -tijd',
send: 'Verzenden',
remove: 'Verwijderen',
edit: 'Bewerken',

@ -73,8 +73,8 @@
<% elsif field['type'] == 'date' %>
<autosize-field></autosize-field>
<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}}' %>
<div class="w-full"><%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), local_assigns[:locale]) %></div>
<% tz = local_assigns[:with_submitter_timezone] ? (submitter.timezone.presence || local_assigns[:timezone]) : local_assigns[:timezone] %>
<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>
<% elsif field['type'] == 'number' %>
<autosize-field></autosize-field>

@ -281,7 +281,7 @@
<% if field['type'] == 'number' %>
<% value = NumberUtils.format_number(value, field.dig('preferences', 'format')) %>
<% 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 %>
<% if (mask = field.dig('preferences', 'mask').presence) %>
<% if signed_in? && can?(:read, @submission) %>

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

@ -415,7 +415,8 @@ module Submissions
composer.formatted_text_box([{ text: value.to_s.titleize }], padding: [0, 0, 10, 0])
else
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
value = NumberUtils.format_number(value, field.dig('preferences', 'format')) if field['type'] == 'number'

@ -652,7 +652,10 @@ module Submissions
end
else
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
value = NumberUtils.format_number(value, field.dig('preferences', 'format')) if field['type'] == 'number'

@ -103,17 +103,43 @@ module Submitters
end
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
elsif value.gsub(/\w/, '0') == field.dig('preferences', 'format').to_s.gsub(/\w/, '0')
TimeUtils.parse_date_string(value, field.dig('preferences', 'format')).to_s
elsif value.gsub(/\w/, '0') == format.to_s.gsub(/\w/, '0')
TimeUtils.parse_date_string(value, format).to_s
else
Date.parse(value).to_s
end
rescue Date::Error
rescue ArgumentError
value
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)
if submitter_name && !for_submitter
submitter =

@ -86,9 +86,7 @@ module Submitters
submitter.values = maybe_remove_condition_values(submitter, required_field_uuids_acc:)
end
submitter.values = submitter.values.transform_values do |v|
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
end
submitter.values = replace_current_date_placeholders(submitter)
required_field_uuids_acc.each do |uuid|
next if submitter.values[uuid].present?
@ -194,7 +192,7 @@ module Submitters
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
default_values.compact_blank.merge(submitter.values)
@ -248,7 +246,20 @@ module Submitters
0
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 submitter.blank?
@ -257,7 +268,8 @@ module Submitters
replace_default_variables(value,
submitter.attributes.merge('role' => role),
submitter.submission,
with_time:)
with_time:,
field:)
end
def maybe_remove_condition_values(submitter, required_field_uuids_acc: nil)
@ -394,7 +406,7 @@ module Submitters
end
# 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 if value.blank?
@ -412,7 +424,7 @@ module Submitters
when 'hour', 'minute', 'day', 'month', 'year'
with_time ? Time.current.in_time_zone(submission.account.timezone).strftime(STRFTIME_MAP[key]) : e
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'
attrs[key] || e
else

@ -19,6 +19,47 @@ module TimeUtils
'YY' => '%y'
}.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 = 'DD/MM/YYYY'
@ -42,27 +83,60 @@ module TimeUtils
end
end
def parse_date_string(string, pattern)
pattern = pattern.sub(/Y+/, YEAR_FORMATS)
.sub(/M+/, MONTH_FORMATS)
.sub(/D+/, DAY_FORMATS)
def format_with_time?(format)
format.to_s.match?(/[HhAasz]/)
end
def month_only_format?(format)
format.to_s.present? && !format.to_s.match?(/[DdHhAasz]/)
end
def format_date_preview(format, locale, timezone)
return '' if format.blank?
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
Date.strptime(string, pattern)
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 format_date_string(string, format, locale)
date = Date.parse(string.to_s)
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)
format = format.upcase if format
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
i18n_format = format.sub(/D+/) { DAY_FORMATS[format[/D+/]] }
.sub(/M+/) { MONTH_FORMATS[format[/M+/]] }
.sub(/Y+/) { YEAR_FORMATS[format[/Y+/]] }
date =
if format_with_time?(format)
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:)
rescue Date::Error
I18n.l(date, format: format.gsub(TOKEN_REGEX, ALL_FORMATS), locale:)
rescue ArgumentError
string
end
end

Loading…
Cancel
Save