@budibase/bbui 3.41.3 → 3.43.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 +1132 -41417
- package/package.json +3 -3
- package/src/Button/Button.svelte +10 -1
- package/src/Drawer/Drawer.svelte +2 -2
- package/src/FancyForm/FancyInput.svelte +1 -1
- package/src/Form/Core/DatePicker/DatePicker.svelte +14 -4
- package/src/Form/Core/DatePicker/DatePickerPopoverContents.svelte +26 -17
- package/src/Form/Core/DatePicker/NumberInput.svelte +11 -6
- package/src/Form/Core/DatePicker/TimePicker.svelte +8 -4
- package/src/Form/Core/DatePicker/utils.spec.ts +53 -0
- package/src/Form/Core/DatePicker/utils.ts +53 -55
- 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/Dropzone.svelte +2 -0
- package/src/Form/TimeField.svelte +15 -5
- package/src/Modal/Modal.svelte +2 -2
- package/src/Switcher/Switcher.svelte +20 -5
- package/src/Tooltip/AbsTooltip.svelte +26 -1
- package/src/helpers.test.ts +57 -1
- package/src/helpers.ts +42 -5
- package/src/index.ts +1 -1
- package/src/styles.d.ts +2 -0
- package/src/types/events.ts +5 -0
- package/src/types/index.ts +1 -0
- package/src/utils/ids.test.ts +13 -0
- package/src/utils/ids.ts +5 -0
|
@@ -3,16 +3,19 @@
|
|
|
3
3
|
import AbsTooltip from "../Tooltip/AbsTooltip.svelte"
|
|
4
4
|
import ActionButton from "../ActionButton/ActionButton.svelte"
|
|
5
5
|
|
|
6
|
-
export let leftIcon: string
|
|
6
|
+
export let leftIcon: string | undefined = undefined
|
|
7
7
|
export let leftNotificationTooltip: string | undefined = undefined
|
|
8
8
|
export let leftNotificationCount: number | undefined = undefined
|
|
9
9
|
export let leftText: string
|
|
10
|
-
export let rightIcon: string
|
|
10
|
+
export let rightIcon: string | undefined = undefined
|
|
11
11
|
export let rightNotificationTooltip: string | undefined = undefined
|
|
12
12
|
export let rightNotificationCount: number | undefined = undefined
|
|
13
13
|
export let rightText: string
|
|
14
14
|
export let selected: "left" | "right" = "left"
|
|
15
15
|
export let disabled = false
|
|
16
|
+
export let leftDisabled = false
|
|
17
|
+
export let rightDisabled = false
|
|
18
|
+
export let size: "S" | "M" | "L" = "M"
|
|
16
19
|
|
|
17
20
|
const dispatch = createEventDispatcher<{
|
|
18
21
|
left: void
|
|
@@ -21,7 +24,7 @@
|
|
|
21
24
|
</script>
|
|
22
25
|
|
|
23
26
|
<div class="view-mode-toggle" class:disabled>
|
|
24
|
-
<div class="group">
|
|
27
|
+
<div class="group size-{size}">
|
|
25
28
|
<div class="wrapper">
|
|
26
29
|
{#if leftNotificationTooltip && leftNotificationCount}
|
|
27
30
|
<AbsTooltip text={leftNotificationTooltip}>
|
|
@@ -39,7 +42,8 @@
|
|
|
39
42
|
<ActionButton
|
|
40
43
|
icon={leftIcon}
|
|
41
44
|
quiet
|
|
42
|
-
{disabled}
|
|
45
|
+
disabled={disabled || leftDisabled}
|
|
46
|
+
{size}
|
|
43
47
|
selected={selected === "left"}
|
|
44
48
|
on:click={() => {
|
|
45
49
|
selected = "left"
|
|
@@ -67,7 +71,8 @@
|
|
|
67
71
|
<ActionButton
|
|
68
72
|
icon={rightIcon}
|
|
69
73
|
quiet
|
|
70
|
-
{disabled}
|
|
74
|
+
disabled={disabled || rightDisabled}
|
|
75
|
+
{size}
|
|
71
76
|
selected={selected === "right"}
|
|
72
77
|
on:click={() => {
|
|
73
78
|
selected = "right"
|
|
@@ -101,6 +106,16 @@
|
|
|
101
106
|
.left :global(*) {
|
|
102
107
|
border-radius: 10px 0 0 10px;
|
|
103
108
|
}
|
|
109
|
+
/* A radius sized for M crops the label on a shorter button */
|
|
110
|
+
.group.size-S {
|
|
111
|
+
border-radius: 8px;
|
|
112
|
+
}
|
|
113
|
+
.size-S .right :global(*) {
|
|
114
|
+
border-radius: 0 6px 6px 0;
|
|
115
|
+
}
|
|
116
|
+
.size-S .left :global(*) {
|
|
117
|
+
border-radius: 6px 0 0 6px;
|
|
118
|
+
}
|
|
104
119
|
.wrapper {
|
|
105
120
|
position: relative;
|
|
106
121
|
}
|
|
@@ -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/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import "./bbui.css"
|
|
|
2
2
|
|
|
3
3
|
// Constants
|
|
4
4
|
export * from "./constants"
|
|
5
|
+
export { generateId } from "./utils/ids"
|
|
5
6
|
|
|
6
7
|
// Form components
|
|
7
8
|
export { default as Checkbox } from "./Form/Checkbox.svelte"
|
|
@@ -122,5 +123,4 @@ export { createNotificationStore, notifications } from "./Stores/notifications"
|
|
|
122
123
|
|
|
123
124
|
// Helpers
|
|
124
125
|
export * as Helpers from "./helpers"
|
|
125
|
-
|
|
126
126
|
export type * from "./types"
|
package/src/styles.d.ts
ADDED
package/src/types/index.ts
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { generateId } from "./ids"
|
|
3
|
+
|
|
4
|
+
const ID_PATTERN = /^[0-9a-zA-Z_-]+$/
|
|
5
|
+
|
|
6
|
+
describe("generateId", () => {
|
|
7
|
+
it("uses the configured length and character set", () => {
|
|
8
|
+
const ids = Array.from({ length: 100 }, generateId)
|
|
9
|
+
|
|
10
|
+
expect(ids.every(id => id.length === 9)).toBe(true)
|
|
11
|
+
expect(ids.every(id => ID_PATTERN.test(id))).toBe(true)
|
|
12
|
+
})
|
|
13
|
+
})
|