feat(frontend): date select component (#4064)

* feat(frontend): add date select component

* feat(frontend): add date select component

* feat(frontend): add missing tooltip

* feat(frontend): add support for locale

* feat(frontend): correct number of days depending on month and year + fix output format

* feat(frontend): Fix display

* feat(frontend): Fix initial dimension

* feat(frontend): Fix outputs

* feat(frontend): clear and set the outputs depending on what field is enabled

* feat(frontend): improve code
This commit is contained in:
Faton Ramadani
2024-07-11 13:47:11 +02:00
committed by GitHub
parent c67170e88f
commit 3b4a376208
5 changed files with 384 additions and 1 deletions
@@ -0,0 +1,313 @@
<script lang="ts">
import { getContext } from 'svelte'
import { initConfig, initOutput } from '../../editor/appUtils'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { initCss } from '../../utils'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import { parseISO, format as formatDateFns } from 'date-fns'
import { components } from '../../editor/component'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
import Select from '../../svelte-select/lib/Select.svelte'
import { SELECT_INPUT_DEFAULT_STYLE } from '$lib/defaults'
import DarkModeObserver from '$lib/components/DarkModeObserver.svelte'
import { twMerge } from 'tailwind-merge'
import { enUS, fr, de, pt, ja } from 'date-fns/locale'
export let id: string
export let configuration: RichConfigurations
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export let customCss: ComponentCustomCSS<'dateselectcomponent'> | undefined = undefined
export let render: boolean
const { app, worldStore, componentControl } = getContext<AppViewerContext>('AppViewerContext')
let resolvedConfig = initConfig(
components['dateselectcomponent'].initialData.configuration,
configuration
)
let value: string | undefined = undefined
$componentControl[id] = {
setValue(nvalue: string) {
value = nvalue
}
}
let outputs = initOutput($worldStore, id, {
day: undefined as number | undefined,
month: undefined as number | undefined,
year: undefined as number | undefined
})
$: !value && handleDefault(resolvedConfig.defaultValue)
function getLocale(locale: string = 'en-US') {
const localeMapping: { [key: string]: Locale } = {
'en-US': enUS,
'en-GB': enUS,
'en-IE': enUS,
'de-DE': de,
'fr-FR': fr,
'br-FR': fr,
'ja-JP': ja,
'pt-TL': pt,
'fr-CA': fr,
'en-CA': enUS
}
return localeMapping[resolvedConfig?.locale as string] || enUS
}
function handleDefault(defaultValue: string | undefined) {
value = defaultValue
if (value) {
const date = parseISO(value)
const locale = getLocale(resolvedConfig.locale)
selectedDay = String(date.getDate())
selectedMonth = formatDateFns(date, 'MMMM', { locale })
selectedYear = String(date.getFullYear())
if (resolvedConfig?.enableDay) {
outputs.day.set(Number(selectedDay))
}
if (resolvedConfig?.enableMonth) {
outputs.month.set(date.getMonth() + 1)
}
if (resolvedConfig?.enableYear) {
outputs.year.set(Number(selectedYear))
}
}
}
let css = initCss($app.css?.dateinputcomponent, customCss)
let darkMode: boolean = false
let selectedDay: string | undefined = undefined
let selectedMonth: string | undefined = undefined
let selectedYear: string | undefined = undefined
$: monthItems = computeMonthItems(resolvedConfig?.locale)
function updateOutputs(enableDay?: boolean, enableMonth?: boolean, enableYear?: boolean) {
if (enableDay) {
outputs.day.set(Number(selectedDay))
} else {
outputs.day.set(undefined)
}
if (enableMonth) {
const monthIndex = monthItems.findIndex((item) => item.label === selectedMonth)
outputs.month.set(monthIndex + 1)
} else {
outputs.month.set(undefined)
}
if (enableYear) {
outputs.year.set(Number(selectedYear))
} else {
outputs.year.set(undefined)
}
}
$: updateOutputs(resolvedConfig.enableDay, resolvedConfig.enableMonth, resolvedConfig.enableYear)
function computeMonthItems(locale: string = 'en-US') {
return [
{
label: formatDateFns(new Date(2000, 0, 1), 'MMMM', { locale: getLocale(locale) }),
value: 'January'
},
{
label: formatDateFns(new Date(2000, 1, 1), 'MMMM', { locale: getLocale(locale) }),
value: 'February'
},
{
label: formatDateFns(new Date(2000, 2, 1), 'MMMM', { locale: getLocale(locale) }),
value: 'March'
},
{
label: formatDateFns(new Date(2000, 3, 1), 'MMMM', { locale: getLocale(locale) }),
value: 'April'
},
{
label: formatDateFns(new Date(2000, 4, 1), 'MMMM', { locale: getLocale(locale) }),
value: 'May'
},
{
label: formatDateFns(new Date(2000, 5, 1), 'MMMM', { locale: getLocale(locale) }),
value: 'June'
},
{
label: formatDateFns(new Date(2000, 6, 1), 'MMMM', { locale: getLocale(locale) }),
value: 'July'
},
{
label: formatDateFns(new Date(2000, 7, 1), 'MMMM', { locale: getLocale(locale) }),
value: 'August'
},
{
label: formatDateFns(new Date(2000, 8, 1), 'MMMM', { locale: getLocale(locale) }),
value: 'September'
},
{
label: formatDateFns(new Date(2000, 9, 1), 'MMMM', { locale: getLocale(locale) }),
value: 'October'
},
{
label: formatDateFns(new Date(2000, 10, 1), 'MMMM', { locale: getLocale(locale) }),
value: 'November'
},
{
label: formatDateFns(new Date(2000, 11, 1), 'MMMM', { locale: getLocale(locale) }),
value: 'December'
}
]
}
function computeDayPerMonth(selectedMonth: string | undefined, selectedYear: string | undefined) {
if (!selectedMonth || !selectedYear) {
return 31
}
const monthIndex =
[
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
].indexOf(selectedMonth) + 1
const daysInMonth = new Date(Number(selectedYear), monthIndex, 0).getDate()
return daysInMonth
}
</script>
{#each Object.keys(components['dateselectcomponent'].initialData.configuration) as key (key)}
<ResolveConfig
{id}
{key}
bind:resolvedConfig={resolvedConfig[key]}
configuration={configuration[key]}
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.dateselectcomponent}
/>
{/each}
<DarkModeObserver bind:darkMode />
<InitializeComponent {id} />
<AlignWrapper {render} {verticalAlignment}>
<div
class={twMerge(
'w-full',
resolvedConfig?.orientation === 'horizontal'
? 'flex flex-row gap-2 '
: 'flex gap-2 flex-col',
css?.container?.class
)}
style={css?.container?.style}
>
{#if resolvedConfig?.enableDay}
<div
class={twMerge('grow', resolvedConfig?.orientation === 'horizontal' ? 'w-1/4' : 'w-full')}
>
<Select
portal={false}
value={selectedDay}
on:change={(e) => {
selectedDay = e.detail.value
outputs.day.set(Number(selectedDay))
}}
on:clear={() => {
selectedDay = ''
outputs.day.set(undefined)
}}
items={Array.from({ length: computeDayPerMonth(selectedMonth, selectedYear) }, (_, i) => {
return { label: String(i + 1), value: String(i + 1) }
})}
class={twMerge('text-clip min-w-0', css?.input?.class, 'wm-date-select')}
containerStyles={(darkMode
? SELECT_INPUT_DEFAULT_STYLE.containerStylesDark
: SELECT_INPUT_DEFAULT_STYLE.containerStyles) + css?.input?.style}
inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles}
placeholder="Pick a day"
/>
</div>
{/if}
{#if resolvedConfig?.enableMonth}
<div
class={twMerge('grow', resolvedConfig?.orientation === 'horizontal' ? 'w-1/2' : 'w-full')}
>
<Select
portal={false}
value={selectedMonth}
on:change={(e) => {
selectedMonth = e.detail.value
outputs.month.set(monthItems.findIndex((item) => item.value === selectedMonth) + 1)
}}
on:clear={() => {
selectedMonth = ''
outputs.month.set(undefined)
}}
items={monthItems}
placeholder="Pick a month"
class={twMerge('text-clip min-w-0', css?.input?.class, 'wm-date-select')}
containerStyles={(darkMode
? SELECT_INPUT_DEFAULT_STYLE.containerStylesDark
: SELECT_INPUT_DEFAULT_STYLE.containerStyles) + css?.input?.style}
clearable
/>
</div>
{/if}
{#if resolvedConfig?.enableYear}
<div
class={twMerge('grow', resolvedConfig?.orientation === 'horizontal' ? 'w-1/4' : 'w-full')}
>
<Select
portal={false}
value={selectedYear}
on:change={(e) => {
selectedYear = e.detail.value
outputs.year.set(Number(selectedYear))
}}
on:clear={() => {
selectedYear = ''
outputs.year.set(undefined)
}}
items={Array.from({ length: 201 }, (_, i) => `${1900 + i}`)}
placeholder="Pick a year"
inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles}
class={twMerge('text-clip min-w-0', css?.input?.class, 'wm-date-select')}
containerStyles={(darkMode
? SELECT_INPUT_DEFAULT_STYLE.containerStylesDark
: SELECT_INPUT_DEFAULT_STYLE.containerStyles) + css?.input?.style}
clearable
/>
</div>
{/if}
</div>
</AlignWrapper>
@@ -76,6 +76,7 @@
import AppSliderInputs from '../../components/inputs/AppSliderInputs.svelte'
import AppNumberInput from '../../components/inputs/AppNumberInput.svelte'
import AppNavbar from '../../components/display/AppNavbar.svelte'
import AppDateSelect from '../../components/inputs/AppDateSelect.svelte'
export let component: AppComponent
export let selected: boolean
@@ -838,6 +839,14 @@
navbarItems={component.navbarItems}
{render}
/>
{:else if component.type === 'dateselectcomponent'}
<AppDateSelect
id={component.id}
configuration={component.configuration}
customCss={component.customCss}
verticalAlignment={component.verticalAlignment}
{render}
/>
{/if}
</div>
</div>
@@ -276,6 +276,8 @@ export type NavBarComponent = BaseComponent<'navbarcomponent'> & {
navbarItems: NavbarItem[]
}
export type DateSelectComponent = BaseComponent<'dateselectcomponent'>
export type TypedComponent =
| DBExplorerComponent
| DisplayComponent
@@ -351,6 +353,7 @@ export type TypedComponent =
| AggridInfiniteComponentEe
| MultiSelectComponentV2
| NavBarComponent
| DateSelectComponent
export type AppComponent = BaseAppComponent & TypedComponent
@@ -3938,6 +3941,60 @@ See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm'
}
}
}
},
dateselectcomponent: {
name: 'Date Select',
icon: Calendar,
documentationLink: `${documentationBaseUrl}/date_select`,
dims: '3:4-6:4' as AppComponentDimensions,
customCss: {
container: { class: '', style: '' },
input: { class: '', style: '' }
},
initialData: {
componentInput: undefined,
verticalAlignment: 'center',
configuration: {
enableDay: {
type: 'static',
value: true,
fieldType: 'boolean'
},
enableMonth: {
type: 'static',
value: true,
fieldType: 'boolean'
},
enableYear: {
type: 'static',
value: true,
fieldType: 'boolean'
},
defaultValue: {
type: 'static',
value: undefined,
fieldType: 'date'
},
orientation: {
type: 'static',
value: 'horizontal',
fieldType: 'select',
selectOptions: [
{ value: 'horizontal', label: 'Horizontal' },
{ value: 'vertical', label: 'Vertical' }
]
},
locale: {
type: 'static',
value: 'en-US',
fieldType: 'select',
selectOptions: selectOptions.localeOptions,
tooltip: 'Format on the month names'
}
}
}
}
} as const
@@ -53,7 +53,8 @@ const inputs: ComponentSet = {
'resourceselectcomponent',
'multiselectcomponentv2',
'selecttabcomponent',
'selectstepcomponent'
'selectstepcomponent',
'dateselectcomponent'
]
} as const
@@ -789,5 +789,8 @@ export const quickStyleProperties: Record<
},
navbarcomponent: {
container: containerDefaultProps
},
dateselectcomponent: {
input: inputDefaultProps
}
}