mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-19 08:01:25 +00:00
fix(frontend): improve time picker (#7893)
* clean layout * remove useless toggle * improve layout * nit * Fix input type seconds * convert secondInputs to svelte 5 * redesign seconds input * Adapt layout where time input is used * nit
This commit is contained in:
@@ -667,10 +667,17 @@
|
||||
|
||||
<div class="flex space-x-1">
|
||||
{#if inputCat == 'number'}
|
||||
{#if extra['min'] != undefined && extra['max'] != undefined}
|
||||
{#if extra['seconds'] !== undefined}
|
||||
<div class="w-full">
|
||||
<SecondsInput
|
||||
bind:seconds={value}
|
||||
onfocus={bubble('focus')}
|
||||
{defaultValue}
|
||||
clearable={extra['clearable'] !== false}
|
||||
/>
|
||||
</div>
|
||||
{:else if extra['min'] != undefined && extra['max'] != undefined}
|
||||
<Range bind:value min={extra['min']} max={extra['max']} {defaultValue} />
|
||||
{:else if extra['seconds'] !== undefined}
|
||||
<SecondsInput bind:seconds={value} on:focus />
|
||||
{:else if extra?.currency}
|
||||
<CurrencyInput
|
||||
inputClasses={{
|
||||
|
||||
@@ -919,6 +919,7 @@
|
||||
? 60 * 60 * 24 * 30
|
||||
: undefined}
|
||||
bind:seconds={$values[setting.key]}
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
{:else if setting.fieldType == 'select'}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import Label from './Label.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import { selectOptions } from './apps/editor/component'
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
import TextInput from './text_input/TextInput.svelte'
|
||||
|
||||
@@ -11,83 +12,80 @@
|
||||
max: number | undefined
|
||||
currency: string | undefined
|
||||
currencyLocale: string | undefined
|
||||
seconds: boolean | undefined
|
||||
}
|
||||
|
||||
let {
|
||||
min = $bindable(),
|
||||
max = $bindable(),
|
||||
currency = $bindable(),
|
||||
currencyLocale = $bindable()
|
||||
currencyLocale = $bindable(),
|
||||
seconds = $bindable()
|
||||
}: Props = $props()
|
||||
|
||||
let minChecked: boolean = $state(min != undefined)
|
||||
let maxChecked: boolean = $state(max != undefined)
|
||||
let mode: string | undefined = $state(seconds ? 'seconds' : currency ? 'currency' : undefined)
|
||||
|
||||
function onModeChange(newMode: string | undefined) {
|
||||
mode = newMode
|
||||
if (newMode === 'seconds') {
|
||||
seconds = true
|
||||
currency = undefined
|
||||
currencyLocale = undefined
|
||||
} else if (newMode === 'currency') {
|
||||
seconds = undefined
|
||||
} else {
|
||||
seconds = undefined
|
||||
currency = undefined
|
||||
currencyLocale = undefined
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<Label label="Min" class="w-full col-span-1">
|
||||
{#snippet header()}
|
||||
<Tooltip light small>
|
||||
Set a minimum value for the number. If both min and max are set, the input will render as
|
||||
a range slider.
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<Toggle
|
||||
bind:checked={minChecked}
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
min = 0
|
||||
} else {
|
||||
min = undefined
|
||||
}
|
||||
}}
|
||||
options={{ right: 'Enabled' }}
|
||||
size="xs"
|
||||
/>
|
||||
{/snippet}
|
||||
<TextInput
|
||||
inputProps={{ type: 'number', disabled: !minChecked }}
|
||||
bind:value={() => min?.toString(), (v) => (min = v ? parseInt(v) : undefined)}
|
||||
/>
|
||||
</Label>
|
||||
<div class="grid grid-cols-2 gap-4 p-4 border rounded-md">
|
||||
<Label label="Min" class="w-full">
|
||||
{#snippet header()}
|
||||
<Tooltip light small>
|
||||
Set a minimum value for the number. If both min and max are set, the input will render as a
|
||||
range slider.
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
<TextInput
|
||||
inputProps={{ type: 'number' }}
|
||||
bind:value={() => min?.toString(), (v) => (min = v ? parseInt(v) : undefined)}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Max" class="w-full col-span-1 ">
|
||||
{#snippet header()}
|
||||
<Tooltip light small>
|
||||
Set a maximum value for the number. If both min and max are set, the input will render as
|
||||
a range slider.
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<Toggle
|
||||
bind:checked={maxChecked}
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
max = 42
|
||||
} else {
|
||||
max = undefined
|
||||
}
|
||||
}}
|
||||
options={{ right: 'Enabled' }}
|
||||
size="xs"
|
||||
/>
|
||||
{/snippet}
|
||||
<TextInput
|
||||
inputProps={{ type: 'number', disabled: !maxChecked }}
|
||||
bind:value={() => max?.toString(), (v) => (max = v ? parseInt(v) : undefined)}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Label label="Currency">
|
||||
{#snippet header()}
|
||||
<Tooltip light small>
|
||||
Select a currency to display the number in. If a currency is selected, you can also select
|
||||
a locale to format the number according to that locale.
|
||||
</Tooltip>
|
||||
<Label label="Max" class="w-full">
|
||||
{#snippet header()}
|
||||
<Tooltip light small>
|
||||
Set a maximum value for the number. If both min and max are set, the input will render as a
|
||||
range slider.
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
<TextInput
|
||||
inputProps={{ type: 'number' }}
|
||||
bind:value={() => max?.toString(), (v) => (max = v ? parseInt(v) : undefined)}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Format" class="w-full col-span-2">
|
||||
{#snippet header()}
|
||||
<Tooltip light small>Display the number as a currency or as a duration in seconds.</Tooltip>
|
||||
{/snippet}
|
||||
<ToggleButtonGroup
|
||||
selected={mode ?? 'none'}
|
||||
onSelected={(v) => onModeChange(v === 'none' ? undefined : v)}
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton value="none" label="None" {item} size="sm" />
|
||||
<ToggleButton value="currency" label="Currency" {item} size="sm" />
|
||||
<ToggleButton value="seconds" label="Seconds" {item} size="sm" />
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</Label>
|
||||
|
||||
{#if mode === 'currency'}
|
||||
<Label label="Currency" class="w-full">
|
||||
<Select
|
||||
bind:value={
|
||||
() => currency,
|
||||
@@ -101,6 +99,7 @@
|
||||
clearable
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Currency locale" class="w-full">
|
||||
<Select
|
||||
bind:value={currencyLocale}
|
||||
@@ -110,5 +109,5 @@
|
||||
clearable
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1323,9 +1323,7 @@
|
||||
/>
|
||||
{#if script.cache_ttl}
|
||||
<div class="text-2xs text-secondary">How long to keep the cache valid</div>
|
||||
<div class="-mt-5">
|
||||
<SecondsInput bind:seconds={script.cache_ttl} />
|
||||
</div>
|
||||
<SecondsInput bind:seconds={script.cache_ttl} />
|
||||
<Toggle
|
||||
size="2xs"
|
||||
bind:checked={
|
||||
|
||||
@@ -341,6 +341,7 @@
|
||||
{/if}
|
||||
{:else if kind == 'none'}
|
||||
{#if !noExtra}
|
||||
<div class="mt-2"></div>
|
||||
<Label label="Min textarea rows">
|
||||
<TextInput
|
||||
inputProps={{ type: 'number' }}
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
right: 'Cache the results for each possible inputs'
|
||||
}}
|
||||
/>
|
||||
<div class="mb-4">
|
||||
<div class="mt-6">
|
||||
<span class="text-xs font-bold">How long to keep cache valid</span>
|
||||
|
||||
{#if cache_ttl}
|
||||
|
||||
@@ -1,36 +1,60 @@
|
||||
<script lang="ts">
|
||||
type TimeUnit = number | undefined
|
||||
import { untrack } from 'svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import {
|
||||
inputBaseClass,
|
||||
inputBorderClass,
|
||||
inputSizeClasses
|
||||
} from '../../text_input/TextInput.svelte'
|
||||
import type { ButtonType } from '../button/model'
|
||||
import CloseButton from '../CloseButton.svelte'
|
||||
|
||||
const ONE_DAY_IN_SECONDS = 86400 as const
|
||||
const ONE_HOUR_IN_SECONDS = 3600 as const
|
||||
const ONE_MINUTE_IN_SECONDS = 60 as const
|
||||
|
||||
export let seconds = 0
|
||||
export let hideDisplay = false
|
||||
export let disabled = false
|
||||
export let max: number | undefined = undefined
|
||||
const segmentClass =
|
||||
'no-default-style !bg-transparent border-none !w-7 text-right p-0 text-xs font-medium focus:ring-0 focus:outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none'
|
||||
|
||||
let day: TimeUnit = undefined
|
||||
let hour: TimeUnit = undefined
|
||||
let min: TimeUnit = undefined
|
||||
let sec: TimeUnit = undefined
|
||||
interface Props {
|
||||
seconds?: number
|
||||
disabled?: boolean
|
||||
max?: number | undefined
|
||||
size?: ButtonType.UnifiedSize
|
||||
clearable?: boolean
|
||||
defaultValue?: number | undefined
|
||||
onfocus?: (e: FocusEvent) => void
|
||||
}
|
||||
|
||||
$: convertSecondsToTime(seconds)
|
||||
let {
|
||||
seconds = $bindable(),
|
||||
disabled = false,
|
||||
max = undefined,
|
||||
size = 'md',
|
||||
clearable = false,
|
||||
defaultValue = undefined,
|
||||
onfocus
|
||||
}: Props = $props()
|
||||
|
||||
function convertSecondsToTime(seconds) {
|
||||
day = Math.floor(seconds / ONE_DAY_IN_SECONDS)
|
||||
seconds -= day * ONE_DAY_IN_SECONDS
|
||||
day = day || undefined
|
||||
let day: number = $state(0)
|
||||
let hour: number = $state(0)
|
||||
let min: number = $state(0)
|
||||
let sec: number = $state(0)
|
||||
|
||||
hour = Math.floor(seconds / ONE_HOUR_IN_SECONDS)
|
||||
seconds -= hour * ONE_HOUR_IN_SECONDS
|
||||
hour = hour || undefined
|
||||
let containerFocused = $state(false)
|
||||
let hasValue = $derived(seconds != null)
|
||||
|
||||
min = Math.floor(seconds / ONE_MINUTE_IN_SECONDS)
|
||||
seconds -= min * ONE_MINUTE_IN_SECONDS
|
||||
min = min || undefined
|
||||
function convertSecondsToTime(s: number) {
|
||||
day = Math.floor(s / ONE_DAY_IN_SECONDS)
|
||||
s -= day * ONE_DAY_IN_SECONDS
|
||||
|
||||
sec = seconds || undefined
|
||||
hour = Math.floor(s / ONE_HOUR_IN_SECONDS)
|
||||
s -= hour * ONE_HOUR_IN_SECONDS
|
||||
|
||||
min = Math.floor(s / ONE_MINUTE_IN_SECONDS)
|
||||
s -= min * ONE_MINUTE_IN_SECONDS
|
||||
|
||||
sec = s
|
||||
}
|
||||
|
||||
function convertUnitsToSeconds() {
|
||||
@@ -44,70 +68,172 @@
|
||||
seconds = max
|
||||
}
|
||||
}
|
||||
|
||||
function clamp(value: number, maxVal: number) {
|
||||
return Math.max(0, Math.min(maxVal, value))
|
||||
}
|
||||
|
||||
function handleKeydown(
|
||||
e: KeyboardEvent,
|
||||
getter: () => number,
|
||||
setter: (v: number) => void,
|
||||
maxVal: number
|
||||
) {
|
||||
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
setter(clamp((getter() || 0) + (e.key === 'ArrowUp' ? 1 : -1), maxVal))
|
||||
convertUnitsToSeconds()
|
||||
}
|
||||
}
|
||||
|
||||
function handleInput(e: Event, setter: (v: number) => void, maxVal: number) {
|
||||
const input = e.currentTarget as HTMLInputElement
|
||||
const clamped = clamp(parseInt(input.value) || 0, maxVal)
|
||||
setter(clamped)
|
||||
input.value = String(clamped)
|
||||
convertUnitsToSeconds()
|
||||
}
|
||||
|
||||
function handleFocus(e: FocusEvent) {
|
||||
containerFocused = true
|
||||
if (seconds == null) {
|
||||
seconds = 0
|
||||
}
|
||||
onfocus?.(e)
|
||||
}
|
||||
|
||||
function handleBlur() {
|
||||
containerFocused = false
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const s = seconds
|
||||
untrack(() => convertSecondsToTime(s ?? 0))
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="flex flex-wrap gap-x-4">
|
||||
{#if !hideDisplay}
|
||||
<input
|
||||
value={seconds == null || seconds == undefined
|
||||
? 'Not set'
|
||||
: disabled
|
||||
? ''
|
||||
: seconds + ' second' + (seconds === 1 ? '' : 's')}
|
||||
{disabled}
|
||||
readonly
|
||||
type="text"
|
||||
class="max-w-[248px] bg-gray-50 mb-2 mt-6"
|
||||
/>
|
||||
{/if}
|
||||
<div class="flex flex-wrap items-center gap-2 text-xs font-medium">
|
||||
<div class="flex items-center gap-2">
|
||||
<label>
|
||||
Sec
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center gap-1">
|
||||
<div
|
||||
class={twMerge(
|
||||
inputBaseClass,
|
||||
inputSizeClasses[size],
|
||||
inputBorderClass({ forceFocus: containerFocused }),
|
||||
'flex items-center gap-0.5 !w-fit'
|
||||
)}
|
||||
>
|
||||
<div class="flex items-baseline">
|
||||
<input
|
||||
type="number"
|
||||
class="!w-14"
|
||||
class={segmentClass}
|
||||
value={hasValue ? day || 0 : ''}
|
||||
placeholder="_ _"
|
||||
min="0"
|
||||
{disabled}
|
||||
bind:value={sec}
|
||||
on:change={convertUnitsToSeconds}
|
||||
on:focus
|
||||
oninput={(e) => handleInput(e, (v) => (day = v), Infinity)}
|
||||
onkeydown={(e) =>
|
||||
handleKeydown(
|
||||
e,
|
||||
() => day,
|
||||
(v) => (day = v),
|
||||
Infinity
|
||||
)}
|
||||
onfocus={handleFocus}
|
||||
onblur={handleBlur}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Min
|
||||
<span class="text-secondary text-2xs inline-grid [&>*]:col-start-1 [&>*]:row-start-1"
|
||||
><span class="invisible">days</span><span>{day && day > 1 ? 'days' : 'day'}</span></span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-baseline">
|
||||
<input
|
||||
type="number"
|
||||
class="!w-14"
|
||||
class={segmentClass}
|
||||
value={hasValue ? hour || 0 : ''}
|
||||
placeholder="_ _"
|
||||
min="0"
|
||||
max="23"
|
||||
{disabled}
|
||||
bind:value={min}
|
||||
on:change={convertUnitsToSeconds}
|
||||
on:focus
|
||||
oninput={(e) => handleInput(e, (v) => (hour = v), 23)}
|
||||
onkeydown={(e) =>
|
||||
handleKeydown(
|
||||
e,
|
||||
() => hour,
|
||||
(v) => (hour = v),
|
||||
23
|
||||
)}
|
||||
onfocus={handleFocus}
|
||||
onblur={handleBlur}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label>
|
||||
Hour
|
||||
<input
|
||||
type="number"
|
||||
class="!w-14"
|
||||
{disabled}
|
||||
bind:value={hour}
|
||||
on:change={convertUnitsToSeconds}
|
||||
on:focus
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Day
|
||||
<input
|
||||
type="number"
|
||||
class="!w-14"
|
||||
{disabled}
|
||||
bind:value={day}
|
||||
on:change={convertUnitsToSeconds}
|
||||
on:focus
|
||||
/>
|
||||
</label>
|
||||
<span class="text-secondary text-2xs inline-grid [&>*]:col-start-1 [&>*]:row-start-1"
|
||||
><span class="invisible">hrs</span><span>{hour && hour > 1 ? 'hrs' : 'hr'}</span></span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-baseline">
|
||||
<input
|
||||
type="number"
|
||||
class={segmentClass}
|
||||
value={hasValue ? min || 0 : ''}
|
||||
placeholder="_ _"
|
||||
min="0"
|
||||
max="59"
|
||||
{disabled}
|
||||
oninput={(e) => handleInput(e, (v) => (min = v), 59)}
|
||||
onkeydown={(e) =>
|
||||
handleKeydown(
|
||||
e,
|
||||
() => min,
|
||||
(v) => (min = v),
|
||||
59
|
||||
)}
|
||||
onfocus={handleFocus}
|
||||
onblur={handleBlur}
|
||||
/>
|
||||
<span class="text-secondary text-2xs inline-grid [&>*]:col-start-1 [&>*]:row-start-1"
|
||||
><span class="invisible">mins</span><span>{min && min > 1 ? 'mins' : 'min'}</span></span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-baseline">
|
||||
<input
|
||||
type="number"
|
||||
class={segmentClass}
|
||||
value={hasValue ? sec || 0 : ''}
|
||||
placeholder="_ _"
|
||||
min="0"
|
||||
max="59"
|
||||
{disabled}
|
||||
oninput={(e) => handleInput(e, (v) => (sec = v), 59)}
|
||||
onkeydown={(e) =>
|
||||
handleKeydown(
|
||||
e,
|
||||
() => sec,
|
||||
(v) => (sec = v),
|
||||
59
|
||||
)}
|
||||
onfocus={handleFocus}
|
||||
onblur={handleBlur}
|
||||
/>
|
||||
<span class="text-secondary text-2xs inline-grid [&>*]:col-start-1 [&>*]:row-start-1"
|
||||
><span class="invisible">secs</span><span>{sec && sec > 1 ? 'secs' : 'sec'}</span></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{#if clearable && !disabled && seconds != null && seconds !== defaultValue}
|
||||
<CloseButton
|
||||
class="bg-transparent text-secondary hover:text-primary"
|
||||
noBg
|
||||
small
|
||||
on:close={() => {
|
||||
seconds = defaultValue
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{#if seconds != null}
|
||||
<span class="text-2xs text-hint">
|
||||
= {seconds.toLocaleString()} second{seconds === 1 ? '' : 's'}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-2xs text-hint"> no time set </span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -46,9 +46,7 @@
|
||||
/>
|
||||
{#if flowModule.cache_ttl}
|
||||
<Label label="How long to keep cache valid">
|
||||
<div class="-mt-5">
|
||||
<SecondsInput bind:seconds={flowModule.cache_ttl} />
|
||||
</div>
|
||||
<SecondsInput bind:seconds={flowModule.cache_ttl} />
|
||||
</Label>
|
||||
<Toggle
|
||||
size="2xs"
|
||||
|
||||
@@ -1195,6 +1195,7 @@
|
||||
<SecondsInput
|
||||
disabled={!$enterpriseLicense}
|
||||
bind:seconds={flowModule.value.concurrency_time_window_s}
|
||||
clearable
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Custom concurrency key (optional)">
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
argName="sleep"
|
||||
{schema}
|
||||
{previousModuleId}
|
||||
argExtra={{ seconds: true }}
|
||||
argExtra={{ seconds: true, clearable: false }}
|
||||
bind:editor
|
||||
/>
|
||||
</PropPickerWrapper>
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
>max</button
|
||||
>
|
||||
</div>
|
||||
<div class="text-xs font-bold !mt-2">Delay</div>
|
||||
<div class="text-xs font-bold !mt-4">Delay</div>
|
||||
<SecondsInput bind:seconds={flowModuleRetry.constant.seconds} />
|
||||
{/if}
|
||||
{:else if delayType === 'exponential'}
|
||||
|
||||
@@ -211,9 +211,7 @@
|
||||
{#if flowStore.val.value.cache_ttl}
|
||||
<div class="flex gap-x-4 flex-col gap-1 mt-2" transition:slide={{ duration: 120 }}>
|
||||
<div class="text-2xs text-secondary">How long to keep the cache valid</div>
|
||||
<div class="-mt-5">
|
||||
<SecondsInput bind:seconds={flowStore.val.value.cache_ttl} />
|
||||
</div>
|
||||
<SecondsInput bind:seconds={flowStore.val.value.cache_ttl} />
|
||||
<Toggle
|
||||
size="2xs"
|
||||
bind:checked={
|
||||
|
||||
@@ -249,6 +249,7 @@
|
||||
bind:max={extra['max']}
|
||||
bind:currency={extra['currency']}
|
||||
bind:currencyLocale={extra['currencyLocale']}
|
||||
bind:seconds={extra['seconds']}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user