@budibase/bbui 3.41.3 → 3.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bbui.mjs +444 -388
- package/package.json +2 -2
- package/src/Button/Button.svelte +10 -1
- package/src/FancyForm/FancyInput.svelte +1 -1
- package/src/Form/Core/DatePicker/DatePicker.svelte +14 -4
- package/src/Form/Core/DatePicker/DatePickerPopoverContents.svelte +23 -16
- package/src/Form/Core/DatePicker/NumberInput.svelte +11 -6
- package/src/Form/Core/DatePicker/TimePicker.svelte +8 -4
- package/src/Form/Core/DateRangePicker.svelte +2 -2
- package/src/Form/Core/TextField.svelte +1 -1
- package/src/Form/DatePicker.svelte +2 -0
- package/src/Form/TimeField.svelte +15 -5
- package/src/Tooltip/AbsTooltip.svelte +26 -1
- package/src/helpers.test.ts +57 -1
- package/src/helpers.ts +42 -5
- package/src/styles.d.ts +2 -0
- package/src/types/events.ts +5 -0
- package/src/types/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/bbui",
|
|
3
3
|
"description": "A UI solution used in the different Budibase projects.",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "v3.42.0",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"module": "dist/bbui.mjs",
|
|
7
7
|
"exports": {
|
|
@@ -108,5 +108,5 @@
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
},
|
|
111
|
-
"gitHead": "
|
|
111
|
+
"gitHead": "180c04c6852aa7acf0402d53dc909eab764d0251"
|
|
112
112
|
}
|
package/src/Button/Button.svelte
CHANGED
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
export let active = false
|
|
24
24
|
export let tooltip: string | null = ""
|
|
25
25
|
export let tooltipPosition: TooltipPosition = TooltipPosition.Top
|
|
26
|
+
export let calculateTooltipWidth:
|
|
27
|
+
| ((target: Element) => number | undefined)
|
|
28
|
+
| undefined = undefined
|
|
26
29
|
export let newStyles = true
|
|
27
30
|
export let id: string | undefined = undefined
|
|
28
31
|
export let ref: HTMLButtonElement | undefined = undefined
|
|
@@ -32,10 +35,16 @@
|
|
|
32
35
|
$: tooltipText = tooltip ?? ""
|
|
33
36
|
</script>
|
|
34
37
|
|
|
35
|
-
<AbsTooltip
|
|
38
|
+
<AbsTooltip
|
|
39
|
+
text={tooltipText}
|
|
40
|
+
position={tooltipPosition}
|
|
41
|
+
disabledTarget={disabled && !!tooltipText}
|
|
42
|
+
calculateWidth={calculateTooltipWidth}
|
|
43
|
+
>
|
|
36
44
|
<button
|
|
37
45
|
{id}
|
|
38
46
|
{type}
|
|
47
|
+
{disabled}
|
|
39
48
|
bind:this={ref}
|
|
40
49
|
class:spectrum-Button--cta={cta}
|
|
41
50
|
class:spectrum-Button--primary={primary}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export interface DatePickerApi {
|
|
3
|
+
open: () => void
|
|
4
|
+
close: () => void
|
|
5
|
+
}
|
|
6
|
+
</script>
|
|
7
|
+
|
|
1
8
|
<script lang="ts" generics="V">
|
|
2
9
|
import "@spectrum-css/calendar/dist/index-vars.css"
|
|
3
10
|
import "@spectrum-css/inputgroup/dist/index-vars.css"
|
|
@@ -12,18 +19,19 @@
|
|
|
12
19
|
import { getLocaleStartDayOfWeek, type Weekday } from "./utils"
|
|
13
20
|
import { resolveTranslationGroup } from "@budibase/shared-core"
|
|
14
21
|
|
|
15
|
-
export let id = null
|
|
22
|
+
export let id: string | null = null
|
|
16
23
|
export let disabled = false
|
|
17
24
|
export let readonly = false
|
|
18
|
-
export let error = null
|
|
25
|
+
export let error: string | false | null | undefined = null
|
|
19
26
|
export let enableTime = true
|
|
20
27
|
export let value: V | null = null
|
|
21
28
|
export let placeholder: string | null = null
|
|
22
29
|
export let timeOnly = false
|
|
30
|
+
export let setTimeTo: string | undefined = undefined
|
|
23
31
|
export let ignoreTimezones = false
|
|
24
32
|
export let useKeyboardShortcuts = true
|
|
25
|
-
export let appendTo = undefined
|
|
26
|
-
export let api = null
|
|
33
|
+
export let appendTo: string | undefined = undefined
|
|
34
|
+
export let api: DatePickerApi | null = null
|
|
27
35
|
export let align: PopoverAlignment = PopoverAlignment.Left
|
|
28
36
|
const browserStartDayOfWeek = getLocaleStartDayOfWeek()
|
|
29
37
|
export let startDayOfWeek: Weekday | undefined = undefined
|
|
@@ -37,6 +45,7 @@
|
|
|
37
45
|
|
|
38
46
|
$: parsedValue = parseDate(value as string | dayjs.Dayjs | null, {
|
|
39
47
|
enableTime,
|
|
48
|
+
setTimeTo,
|
|
40
49
|
})
|
|
41
50
|
|
|
42
51
|
const onOpen = () => {
|
|
@@ -88,6 +97,7 @@
|
|
|
88
97
|
{ignoreTimezones}
|
|
89
98
|
{enableTime}
|
|
90
99
|
{timeOnly}
|
|
100
|
+
{setTimeTo}
|
|
91
101
|
startDayOfWeek={resolvedStartDayOfWeek}
|
|
92
102
|
{calendarLabels}
|
|
93
103
|
value={parsedValue}
|
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
import dayjs from "dayjs"
|
|
3
|
-
import TimePicker from "./TimePicker.svelte"
|
|
4
|
-
import Calendar from "./Calendar.svelte"
|
|
5
|
-
import ActionButton from "../../../ActionButton/ActionButton.svelte"
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import dayjs, { type Dayjs } from "dayjs"
|
|
6
3
|
import { createEventDispatcher, onMount } from "svelte"
|
|
7
|
-
import { stringifyDate } from "../../../helpers"
|
|
8
4
|
import { resolveTranslationGroup } from "@budibase/shared-core"
|
|
5
|
+
import ActionButton from "../../../ActionButton/ActionButton.svelte"
|
|
6
|
+
import { stringifyDate } from "../../../helpers"
|
|
7
|
+
import Calendar from "./Calendar.svelte"
|
|
8
|
+
import TimePicker from "./TimePicker.svelte"
|
|
9
|
+
import type { Weekday } from "./utils"
|
|
9
10
|
|
|
10
11
|
export let useKeyboardShortcuts = true
|
|
11
|
-
export let ignoreTimezones
|
|
12
|
-
export let enableTime
|
|
13
|
-
export let timeOnly
|
|
14
|
-
export let
|
|
15
|
-
export let
|
|
12
|
+
export let ignoreTimezones = false
|
|
13
|
+
export let enableTime = true
|
|
14
|
+
export let timeOnly = false
|
|
15
|
+
export let setTimeTo: string | undefined = undefined
|
|
16
|
+
export let value: Dayjs | null | undefined = null
|
|
17
|
+
export let startDayOfWeek: Weekday = "Monday"
|
|
16
18
|
export let calendarLabels = resolveTranslationGroup("calendar")
|
|
17
19
|
|
|
18
|
-
const dispatch = createEventDispatcher()
|
|
19
|
-
let calendar
|
|
20
|
+
const dispatch = createEventDispatcher<{ change: string | null }>()
|
|
21
|
+
let calendar: { setDate: (date: Dayjs) => void } | undefined
|
|
20
22
|
|
|
21
23
|
$: showCalendar = !timeOnly
|
|
22
24
|
$: showTime = enableTime || timeOnly
|
|
@@ -27,14 +29,19 @@
|
|
|
27
29
|
handleChange(now)
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
const handleChange = date => {
|
|
32
|
+
const handleChange = (date: Dayjs | null | undefined) => {
|
|
31
33
|
dispatch(
|
|
32
34
|
"change",
|
|
33
|
-
stringifyDate(date
|
|
35
|
+
stringifyDate(date ?? null, {
|
|
36
|
+
enableTime,
|
|
37
|
+
timeOnly,
|
|
38
|
+
ignoreTimezones,
|
|
39
|
+
setTimeTo,
|
|
40
|
+
})
|
|
34
41
|
)
|
|
35
42
|
}
|
|
36
43
|
|
|
37
|
-
const clearDateOnBackspace = event => {
|
|
44
|
+
const clearDateOnBackspace = (event: KeyboardEvent) => {
|
|
38
45
|
// Ignore if we're typing a value
|
|
39
46
|
if (document.activeElement?.tagName.toLowerCase() === "input") {
|
|
40
47
|
return
|
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
export let value
|
|
3
|
-
export let min = undefined
|
|
4
|
-
export let max = undefined
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export let value: string | number | undefined = undefined
|
|
3
|
+
export let min: string | number | undefined = undefined
|
|
4
|
+
export let max: string | number | undefined = undefined
|
|
5
5
|
export let hideArrows = false
|
|
6
|
-
export let width = undefined
|
|
6
|
+
export let width: number | undefined = undefined
|
|
7
7
|
export let type = "number"
|
|
8
8
|
export let disabled = false
|
|
9
9
|
export let readonly = false
|
|
10
|
+
export let step: string | number | undefined = undefined
|
|
10
11
|
|
|
11
12
|
$: style = width ? `width:${width}px;` : ""
|
|
12
13
|
|
|
13
|
-
const selectAll = event =>
|
|
14
|
+
const selectAll = (event: MouseEvent) => {
|
|
15
|
+
const input = event.currentTarget as HTMLInputElement
|
|
16
|
+
input.select()
|
|
17
|
+
}
|
|
14
18
|
</script>
|
|
15
19
|
|
|
16
20
|
<input
|
|
@@ -22,6 +26,7 @@
|
|
|
22
26
|
{max}
|
|
23
27
|
{disabled}
|
|
24
28
|
{readonly}
|
|
29
|
+
{step}
|
|
25
30
|
on:click={selectAll}
|
|
26
31
|
on:change
|
|
27
32
|
on:input
|
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
import NumberInput from "./NumberInput.svelte"
|
|
5
5
|
import { createEventDispatcher } from "svelte"
|
|
6
6
|
|
|
7
|
-
export let value: Dayjs | undefined
|
|
7
|
+
export let value: Dayjs | null | undefined
|
|
8
8
|
export let disableClearing = false
|
|
9
9
|
export let disabled = false
|
|
10
10
|
export let readonly = false
|
|
11
|
+
export let showSeconds = false
|
|
11
12
|
|
|
12
13
|
const dispatch = createEventDispatcher<{ change: Dayjs | undefined }>()
|
|
13
14
|
|
|
14
|
-
$: displayValue = value?.format("HH:mm")
|
|
15
|
+
$: displayValue = value?.format(showSeconds ? "HH:mm:ss" : "HH:mm")
|
|
15
16
|
|
|
16
17
|
const handleChange = async (e: Event) => {
|
|
17
18
|
if (disabled || readonly) {
|
|
@@ -28,10 +29,12 @@
|
|
|
28
29
|
return
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
const [hour, minute] = target.value
|
|
32
|
+
const [hour, minute, second = 0] = target.value
|
|
33
|
+
.split(":")
|
|
34
|
+
.map(x => parseInt(x))
|
|
32
35
|
dispatch(
|
|
33
36
|
"change",
|
|
34
|
-
(value || dayjs()).hour(hour).minute(minute).second(
|
|
37
|
+
(value || dayjs()).hour(hour).minute(minute).second(second).millisecond(0)
|
|
35
38
|
)
|
|
36
39
|
}
|
|
37
40
|
</script>
|
|
@@ -43,6 +46,7 @@
|
|
|
43
46
|
value={displayValue}
|
|
44
47
|
{disabled}
|
|
45
48
|
{readonly}
|
|
49
|
+
step={showSeconds ? 1 : undefined}
|
|
46
50
|
on:input={handleChange}
|
|
47
51
|
on:change={handleChange}
|
|
48
52
|
/>
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
const onChangeFrom = (utc: string) => {
|
|
42
|
+
const onChangeFrom = (utc: string | null) => {
|
|
43
43
|
// Preserve the time if its editable
|
|
44
44
|
const fromDate = utc
|
|
45
45
|
? enableTime
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
dispatch("change", [fromDate, toDate])
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
const onChangeTo = (utc: string) => {
|
|
58
|
+
const onChangeTo = (utc: string | null) => {
|
|
59
59
|
// Preserve the time if its editable
|
|
60
60
|
const toDate = utc
|
|
61
61
|
? enableTime
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import "@spectrum-css/textfield/dist/index-vars.css"
|
|
3
3
|
import { createEventDispatcher, onMount, tick } from "svelte"
|
|
4
4
|
import type { FullAutoFill } from "svelte/elements"
|
|
5
|
-
import type { UIEvent } from "
|
|
5
|
+
import type { UIEvent } from "../../types"
|
|
6
6
|
|
|
7
7
|
export let value: V | null = null
|
|
8
8
|
export let placeholder: string | undefined = undefined
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
export let error = undefined
|
|
14
14
|
export let enableTime = true
|
|
15
15
|
export let timeOnly = false
|
|
16
|
+
export let setTimeTo: string | undefined = undefined
|
|
16
17
|
export let placeholder: string | null = null
|
|
17
18
|
export let appendTo = undefined
|
|
18
19
|
export let ignoreTimezones = false
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
{placeholder}
|
|
37
38
|
{enableTime}
|
|
38
39
|
{timeOnly}
|
|
40
|
+
{setTimeTo}
|
|
39
41
|
{appendTo}
|
|
40
42
|
{ignoreTimezones}
|
|
41
43
|
{calendarLabels}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
id?: string
|
|
17
17
|
name?: string
|
|
18
18
|
disableClearing?: boolean
|
|
19
|
+
showSeconds?: boolean
|
|
19
20
|
onchange?: (value: string | undefined) => void
|
|
20
21
|
}
|
|
21
22
|
</script>
|
|
@@ -39,33 +40,41 @@
|
|
|
39
40
|
id = undefined,
|
|
40
41
|
name = undefined,
|
|
41
42
|
disableClearing = false,
|
|
43
|
+
showSeconds = false,
|
|
42
44
|
onchange,
|
|
43
45
|
}: Props = $props()
|
|
44
46
|
|
|
45
|
-
const FALLBACK_TIME = "00:00"
|
|
47
|
+
const FALLBACK_TIME = $derived(showSeconds ? "00:00:00" : "00:00")
|
|
46
48
|
|
|
47
49
|
const parseValue = (time: TimeFieldValue) => {
|
|
48
50
|
if (!time) {
|
|
49
51
|
return undefined
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
const
|
|
54
|
+
const parts = time.split(":")
|
|
55
|
+
if (parts.length !== (showSeconds ? 3 : 2)) {
|
|
56
|
+
return undefined
|
|
57
|
+
}
|
|
58
|
+
const [hour, minute, second = 0] = parts.map(part => Number(part))
|
|
53
59
|
if (
|
|
54
60
|
!Number.isInteger(hour) ||
|
|
55
61
|
!Number.isInteger(minute) ||
|
|
62
|
+
!Number.isInteger(second) ||
|
|
56
63
|
hour < 0 ||
|
|
57
64
|
hour > 23 ||
|
|
58
65
|
minute < 0 ||
|
|
59
|
-
minute > 59
|
|
66
|
+
minute > 59 ||
|
|
67
|
+
second < 0 ||
|
|
68
|
+
second > 59
|
|
60
69
|
) {
|
|
61
70
|
return undefined
|
|
62
71
|
}
|
|
63
72
|
|
|
64
|
-
return dayjs().hour(hour).minute(minute).second(
|
|
73
|
+
return dayjs().hour(hour).minute(minute).second(second).millisecond(0)
|
|
65
74
|
}
|
|
66
75
|
|
|
67
76
|
const handleChange = (event: CustomEvent<Dayjs | undefined>) => {
|
|
68
|
-
const nextValue = event.detail?.format("HH:mm")
|
|
77
|
+
const nextValue = event.detail?.format(showSeconds ? "HH:mm:ss" : "HH:mm")
|
|
69
78
|
if (!nextValue && disableClearing) {
|
|
70
79
|
return
|
|
71
80
|
}
|
|
@@ -104,6 +113,7 @@
|
|
|
104
113
|
{disableClearing}
|
|
105
114
|
{disabled}
|
|
106
115
|
{readonly}
|
|
116
|
+
{showSeconds}
|
|
107
117
|
on:change={handleChange}
|
|
108
118
|
/>
|
|
109
119
|
{#if name}
|
|
@@ -11,12 +11,17 @@
|
|
|
11
11
|
export let fixed: boolean = false
|
|
12
12
|
export let color: string | undefined = undefined
|
|
13
13
|
export let noWrap: boolean = false
|
|
14
|
+
export let disabledTarget: boolean = false
|
|
15
|
+
export let calculateWidth:
|
|
16
|
+
| ((target: Element) => number | undefined)
|
|
17
|
+
| undefined = undefined
|
|
14
18
|
export let offset: number = 0
|
|
15
19
|
|
|
16
20
|
let wrapper: HTMLElement | undefined
|
|
17
21
|
let hovered = false
|
|
18
22
|
let left: number | undefined
|
|
19
23
|
let top: number | undefined
|
|
24
|
+
let tooltipWidth: number | undefined
|
|
20
25
|
let visible = false
|
|
21
26
|
let timeout: ReturnType<typeof setTimeout> | undefined
|
|
22
27
|
let interval: ReturnType<typeof setInterval> | undefined
|
|
@@ -38,9 +43,11 @@
|
|
|
38
43
|
if (!node) {
|
|
39
44
|
left = undefined
|
|
40
45
|
top = undefined
|
|
46
|
+
tooltipWidth = undefined
|
|
41
47
|
return
|
|
42
48
|
}
|
|
43
49
|
const bounds = node.getBoundingClientRect()
|
|
50
|
+
tooltipWidth = calculateWidth?.(node)
|
|
44
51
|
|
|
45
52
|
// Determine where to render tooltip based on position prop
|
|
46
53
|
if (position === TooltipPosition.Top) {
|
|
@@ -82,6 +89,7 @@
|
|
|
82
89
|
<div
|
|
83
90
|
bind:this={wrapper}
|
|
84
91
|
class="abs-tooltip"
|
|
92
|
+
class:disabled-target={disabledTarget}
|
|
85
93
|
on:focus={null}
|
|
86
94
|
on:mouseover={() => (hovered = true)}
|
|
87
95
|
on:mouseleave={() => (hovered = false)}
|
|
@@ -94,7 +102,13 @@
|
|
|
94
102
|
<span
|
|
95
103
|
class="spectrum-Tooltip spectrum-Tooltip--{type} spectrum-Tooltip--{position} is-open"
|
|
96
104
|
class:noWrap
|
|
97
|
-
|
|
105
|
+
class:calculated-width={tooltipWidth != null}
|
|
106
|
+
style:left={`${left}px`}
|
|
107
|
+
style:top={`${top}px`}
|
|
108
|
+
style:--tooltip-width={tooltipWidth != null
|
|
109
|
+
? `${tooltipWidth}px`
|
|
110
|
+
: undefined}
|
|
111
|
+
style={tooltipStyle}
|
|
98
112
|
transition:fade|local={{ duration: 130 }}
|
|
99
113
|
>
|
|
100
114
|
<span class="spectrum-Tooltip-label">{text}</span>
|
|
@@ -108,6 +122,12 @@
|
|
|
108
122
|
display: contents;
|
|
109
123
|
pointer-events: all;
|
|
110
124
|
}
|
|
125
|
+
.abs-tooltip.disabled-target {
|
|
126
|
+
display: inline-block;
|
|
127
|
+
}
|
|
128
|
+
.abs-tooltip.disabled-target :global(:disabled) {
|
|
129
|
+
pointer-events: none;
|
|
130
|
+
}
|
|
111
131
|
.spectrum-Tooltip.noWrap .spectrum-Tooltip-label {
|
|
112
132
|
width: max-content;
|
|
113
133
|
}
|
|
@@ -121,6 +141,11 @@
|
|
|
121
141
|
top 130ms ease-out,
|
|
122
142
|
left 130ms ease-out;
|
|
123
143
|
}
|
|
144
|
+
.spectrum-Tooltip.calculated-width {
|
|
145
|
+
width: var(--tooltip-width);
|
|
146
|
+
max-width: none;
|
|
147
|
+
max-inline-size: none;
|
|
148
|
+
}
|
|
124
149
|
.spectrum-Tooltip-label {
|
|
125
150
|
display: -webkit-box;
|
|
126
151
|
-webkit-line-clamp: 3;
|
package/src/helpers.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from "vitest"
|
|
2
|
-
import
|
|
2
|
+
import dayjs from "dayjs"
|
|
3
|
+
import { parseDate, parseTime, stringifyDate, uuid } from "./helpers"
|
|
3
4
|
|
|
4
5
|
describe("uuid", () => {
|
|
5
6
|
afterEach(() => {
|
|
@@ -27,3 +28,58 @@ describe("uuid", () => {
|
|
|
27
28
|
expect(Math.random).toHaveBeenCalled()
|
|
28
29
|
})
|
|
29
30
|
})
|
|
31
|
+
|
|
32
|
+
describe("date helpers", () => {
|
|
33
|
+
const selectedDate = dayjs("2026-08-05T10:15:30.456")
|
|
34
|
+
|
|
35
|
+
it("keeps date-only values unchanged when no time is configured", () => {
|
|
36
|
+
expect(
|
|
37
|
+
stringifyDate(selectedDate, { enableTime: false, setTimeTo: "" })
|
|
38
|
+
).toBe("2026-08-05")
|
|
39
|
+
expect(stringifyDate(null, { setTimeTo: "23:59:59" })).toBeNull()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it("applies a configured time to timezone-aware date selections", () => {
|
|
43
|
+
const expected = selectedDate
|
|
44
|
+
.hour(23)
|
|
45
|
+
.minute(59)
|
|
46
|
+
.second(59)
|
|
47
|
+
.millisecond(0)
|
|
48
|
+
.toISOString()
|
|
49
|
+
|
|
50
|
+
expect(
|
|
51
|
+
stringifyDate(selectedDate, {
|
|
52
|
+
enableTime: false,
|
|
53
|
+
setTimeTo: "23:59:59",
|
|
54
|
+
})
|
|
55
|
+
).toBe(expected)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("applies a configured time without an offset when ignoring timezones", () => {
|
|
59
|
+
expect(
|
|
60
|
+
stringifyDate(selectedDate, {
|
|
61
|
+
enableTime: false,
|
|
62
|
+
ignoreTimezones: true,
|
|
63
|
+
setTimeTo: "23:59:59",
|
|
64
|
+
})
|
|
65
|
+
).toBe("2026-08-05T23:59:59.000")
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("treats malformed configured times as unset", () => {
|
|
69
|
+
expect(
|
|
70
|
+
stringifyDate(selectedDate, {
|
|
71
|
+
enableTime: false,
|
|
72
|
+
setTimeTo: "24:00:00",
|
|
73
|
+
})
|
|
74
|
+
).toBe("2026-08-05")
|
|
75
|
+
expect(parseTime("12:30")).toBeNull()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it("parses stored datetime values without discarding their time", () => {
|
|
79
|
+
const value = "2026-08-05T23:59:59.000Z"
|
|
80
|
+
|
|
81
|
+
expect(
|
|
82
|
+
parseDate(value, { enableTime: false, setTimeTo: "23:59:59" })?.valueOf()
|
|
83
|
+
).toBe(dayjs(value).valueOf())
|
|
84
|
+
})
|
|
85
|
+
})
|
package/src/helpers.ts
CHANGED
|
@@ -119,11 +119,28 @@ export const copyToClipboard = (value: any): Promise<void> => {
|
|
|
119
119
|
})
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
export const parseTime = (
|
|
123
|
+
value: string | undefined
|
|
124
|
+
): { hour: number; minute: number; second: number } | null => {
|
|
125
|
+
const match = /^(\d{2}):(\d{2}):(\d{2})$/.exec(value || "")
|
|
126
|
+
if (!match) {
|
|
127
|
+
return null
|
|
128
|
+
}
|
|
129
|
+
const [, hourValue, minuteValue, secondValue] = match
|
|
130
|
+
const hour = Number(hourValue)
|
|
131
|
+
const minute = Number(minuteValue)
|
|
132
|
+
const second = Number(secondValue)
|
|
133
|
+
if (hour > 23 || minute > 59 || second > 59) {
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
return { hour, minute, second }
|
|
137
|
+
}
|
|
138
|
+
|
|
122
139
|
// Parse a date value. This is usually an ISO string, but can be a
|
|
123
140
|
// bunch of different formats and shapes depending on schema flags.
|
|
124
141
|
export const parseDate = (
|
|
125
142
|
value: string | dayjs.Dayjs | null,
|
|
126
|
-
{ enableTime = true }
|
|
143
|
+
{ enableTime = true, setTimeTo }: { enableTime?: boolean; setTimeTo?: string }
|
|
127
144
|
): dayjs.Dayjs | null => {
|
|
128
145
|
// If empty then invalid
|
|
129
146
|
if (!value) {
|
|
@@ -138,7 +155,7 @@ export const parseDate = (
|
|
|
138
155
|
}
|
|
139
156
|
|
|
140
157
|
// If date only, check for cases where we received a UTC string
|
|
141
|
-
else if (!enableTime && value.endsWith("Z")) {
|
|
158
|
+
else if (!enableTime && !parseTime(setTimeTo) && value.endsWith("Z")) {
|
|
142
159
|
value = value.split("Z")[0]
|
|
143
160
|
}
|
|
144
161
|
}
|
|
@@ -159,16 +176,36 @@ export const parseDate = (
|
|
|
159
176
|
// schema flags
|
|
160
177
|
export const stringifyDate = (
|
|
161
178
|
value: null | dayjs.Dayjs,
|
|
162
|
-
{
|
|
179
|
+
{
|
|
180
|
+
enableTime = true,
|
|
181
|
+
timeOnly = false,
|
|
182
|
+
ignoreTimezones = false,
|
|
183
|
+
setTimeTo,
|
|
184
|
+
}: {
|
|
185
|
+
enableTime?: boolean
|
|
186
|
+
timeOnly?: boolean
|
|
187
|
+
ignoreTimezones?: boolean
|
|
188
|
+
setTimeTo?: string
|
|
189
|
+
} = {}
|
|
163
190
|
): string | null => {
|
|
164
191
|
if (!value) {
|
|
165
192
|
return null
|
|
166
193
|
}
|
|
167
194
|
|
|
195
|
+
const configuredTime = !enableTime && !timeOnly ? parseTime(setTimeTo) : null
|
|
196
|
+
if (configuredTime) {
|
|
197
|
+
value = value
|
|
198
|
+
.hour(configuredTime.hour)
|
|
199
|
+
.minute(configuredTime.minute)
|
|
200
|
+
.second(configuredTime.second)
|
|
201
|
+
.millisecond(0)
|
|
202
|
+
}
|
|
203
|
+
const serializeTime = enableTime || !!configuredTime
|
|
204
|
+
|
|
168
205
|
// Time only fields always ignore timezones, otherwise they make no sense.
|
|
169
206
|
// For non-timezone-aware fields, create an ISO 8601 timestamp of the exact
|
|
170
207
|
// time picked, without timezone
|
|
171
|
-
const offsetForTimezone = (
|
|
208
|
+
const offsetForTimezone = (serializeTime && ignoreTimezones) || timeOnly
|
|
172
209
|
if (offsetForTimezone) {
|
|
173
210
|
// Ensure we use the correct offset for the date
|
|
174
211
|
const referenceDate = value.toDate()
|
|
@@ -183,7 +220,7 @@ export const stringifyDate = (
|
|
|
183
220
|
|
|
184
221
|
// For date-only fields, construct a manual timestamp string without a time
|
|
185
222
|
// or time zone
|
|
186
|
-
else if (!
|
|
223
|
+
else if (!serializeTime) {
|
|
187
224
|
const year = value.year()
|
|
188
225
|
const month = `${value.month() + 1}`.padStart(2, "0")
|
|
189
226
|
const day = `${value.date()}`.padStart(2, "0")
|
package/src/styles.d.ts
ADDED