1
0
Fork 0
mirror of synced 2024-06-29 11:31:06 +12:00

Merge branch 'master' into fix/isBetterSample-null-reference

This commit is contained in:
Adria Navarro 2024-04-26 16:12:55 +02:00 committed by GitHub
commit c133d329af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 2038 additions and 993 deletions

View file

@ -92,8 +92,6 @@ jobs:
test-libraries:
runs-on: ubuntu-latest
env:
REUSE_CONTAINERS: true
steps:
- name: Checkout repo
uses: actions/checkout@v4
@ -150,8 +148,6 @@ jobs:
test-server:
runs-on: budi-tubby-tornado-quad-core-150gb
env:
REUSE_CONTAINERS: true
steps:
- name: Checkout repo
uses: actions/checkout@v4

View file

@ -1,16 +1,49 @@
import { GenericContainer, Wait } from "testcontainers"
import {
GenericContainer,
Wait,
getContainerRuntimeClient,
} from "testcontainers"
import { ContainerInfo } from "dockerode"
import path from "path"
import lockfile from "proper-lockfile"
async function getBudibaseContainers() {
const client = await getContainerRuntimeClient()
const conatiners = await client.container.list()
return conatiners.filter(
container =>
container.Labels["com.budibase"] === "true" &&
container.Labels["org.testcontainers"] === "true"
)
}
async function killContainers(containers: ContainerInfo[]) {
const client = await getContainerRuntimeClient()
for (const container of containers) {
const c = client.container.getById(container.Id)
await c.kill()
await c.remove()
}
}
export default async function setup() {
const lockPath = path.resolve(__dirname, "globalSetup.ts")
if (process.env.REUSE_CONTAINERS) {
// If you run multiple tests at the same time, it's possible for the CouchDB
// shared container to get started multiple times despite having an
// identical reuse hash. To avoid that, we do a filesystem-based lock so
// that only one globalSetup.ts is running at a time.
lockfile.lockSync(lockPath)
}
// If you run multiple tests at the same time, it's possible for the CouchDB
// shared container to get started multiple times despite having an
// identical reuse hash. To avoid that, we do a filesystem-based lock so
// that only one globalSetup.ts is running at a time.
lockfile.lockSync(lockPath)
// Remove any containers that are older than 24 hours. This is to prevent
// containers getting full volumes or accruing any other problems from being
// left up for very long periods of time.
const threshold = new Date(Date.now() - 1000 * 60 * 60 * 24)
const containers = (await getBudibaseContainers()).filter(container => {
const created = new Date(container.Created * 1000)
return created < threshold
})
await killContainers(containers)
try {
let couchdb = new GenericContainer("budibase/couchdb:v3.2.1-sqs")
@ -28,20 +61,16 @@ export default async function setup() {
target: "/opt/couchdb/etc/local.d/test-couchdb.ini",
},
])
.withLabels({ "com.budibase": "true" })
.withReuse()
.withWaitStrategy(
Wait.forSuccessfulCommand(
"curl http://budibase:budibase@localhost:5984/_up"
).withStartupTimeout(20000)
)
if (process.env.REUSE_CONTAINERS) {
couchdb = couchdb.withReuse()
}
await couchdb.start()
} finally {
if (process.env.REUSE_CONTAINERS) {
lockfile.unlockSync(lockPath)
}
lockfile.unlockSync(lockPath)
}
}

View file

@ -59,7 +59,8 @@
"dev:all": "yarn run kill-all && lerna run --stream dev",
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built",
"dev:docker": "./scripts/devDocker.sh",
"test": "REUSE_CONTAINERS=1 lerna run --concurrency 1 --stream test --stream",
"test": "lerna run --concurrency 1 --stream test --stream",
"test:containers:kill": "./scripts/killTestcontainers.sh",
"lint:eslint": "eslint packages --max-warnings=0",
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"",
"lint": "yarn run lint:eslint && yarn run lint:prettier",

View file

@ -28,7 +28,11 @@ function getTestcontainers(): ContainerInfo[] {
.split("\n")
.filter(x => x.length > 0)
.map(x => JSON.parse(x) as ContainerInfo)
.filter(x => x.Labels.includes("org.testcontainers=true"))
.filter(
x =>
x.Labels.includes("org.testcontainers=true") &&
x.Labels.includes("com.budibase=true")
)
}
export function getContainerByImage(image: string) {

View file

@ -21,7 +21,7 @@
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^11.2.1",
"postcss": "^8.2.9",
"rollup": "^4.9.6",
"rollup": "^2.45.2",
"rollup-plugin-postcss": "^4.0.0",
"rollup-plugin-svelte": "^7.1.0",
"rollup-plugin-terser": "^7.0.2"
@ -43,6 +43,7 @@
"@spectrum-css/avatar": "3.0.2",
"@spectrum-css/button": "3.0.1",
"@spectrum-css/buttongroup": "3.0.2",
"@spectrum-css/calendar": "3.2.7",
"@spectrum-css/checkbox": "3.0.2",
"@spectrum-css/dialog": "3.0.1",
"@spectrum-css/divider": "1.0.3",

View file

@ -38,7 +38,15 @@
<div use:getAnchor on:click={openMenu}>
<slot name="control" />
</div>
<Popover bind:this={dropdown} {anchor} {align} {portalTarget} on:open on:close>
<Popover
bind:this={dropdown}
{anchor}
{align}
{portalTarget}
resizable={false}
on:open
on:close
>
<Menu>
<slot />
</Menu>

View file

@ -1,7 +1,13 @@
const ignoredClasses = [
".flatpickr-calendar",
".spectrum-Popover",
".download-js-link",
".flatpickr-calendar",
".spectrum-Menu",
".date-time-popover",
]
const conditionallyIgnoredClasses = [
".spectrum-Underlay",
".drawer-wrapper",
".spectrum-Popover",
]
let clickHandlers = []
@ -9,6 +15,9 @@ let clickHandlers = []
* Handle a body click event
*/
const handleClick = event => {
// Treat right clicks (context menu events) as normal clicks
const eventType = event.type === "contextmenu" ? "click" : event.type
// Ignore click if this is an ignored class
if (event.target.closest('[data-ignore-click-outside="true"]')) {
return
@ -21,26 +30,23 @@ const handleClick = event => {
// Process handlers
clickHandlers.forEach(handler => {
// Check that we're the right kind of click event
if (handler.allowedType && eventType !== handler.allowedType) {
return
}
// Check that the click isn't inside the target
if (handler.element.contains(event.target)) {
return
}
// Ignore clicks for modals, unless the handler is registered from a modal
const sourceInModal = handler.anchor.closest(".spectrum-Underlay") != null
const clickInModal = event.target.closest(".spectrum-Underlay") != null
if (clickInModal && !sourceInModal) {
return
}
// Ignore clicks for drawers, unless the handler is registered from a drawer
const sourceInDrawer = handler.anchor.closest(".drawer-wrapper") != null
const clickInDrawer = event.target.closest(".drawer-wrapper") != null
if (clickInDrawer && !sourceInDrawer) {
return
}
if (handler.allowedType && event.type !== handler.allowedType) {
return
// Ignore clicks for certain classes unless we're nested inside them
for (let className of conditionallyIgnoredClasses) {
const sourceInside = handler.anchor.closest(className) != null
const clickInside = event.target.closest(className) != null
if (clickInside && !sourceInside) {
return
}
}
handler.callback?.(event)
@ -48,6 +54,7 @@ const handleClick = event => {
}
document.documentElement.addEventListener("click", handleClick, true)
document.documentElement.addEventListener("mousedown", handleClick, true)
document.documentElement.addEventListener("contextmenu", handleClick, true)
/**
* Adds or updates a click handler

View file

@ -1,3 +1,22 @@
/**
* Valid alignment options are
* - left
* - right
* - left-outside
* - right-outside
**/
// Strategies are defined as [Popover]To[Anchor].
// They can apply for both horizontal and vertical alignment.
const Strategies = {
StartToStart: "StartToStart", // e.g. left alignment
EndToEnd: "EndToEnd", // e.g. right alignment
StartToEnd: "StartToEnd", // e.g. right-outside alignment
EndToStart: "EndToStart", // e.g. left-outside alignment
MidPoint: "MidPoint", // centers relative to midpoints
ScreenEdge: "ScreenEdge", // locks to screen edge
}
export default function positionDropdown(element, opts) {
let resizeObserver
let latestOpts = opts
@ -19,6 +38,8 @@ export default function positionDropdown(element, opts) {
useAnchorWidth,
offset = 5,
customUpdate,
resizable,
wrap,
} = opts
if (!anchor) {
return
@ -27,56 +48,159 @@ export default function positionDropdown(element, opts) {
// Compute bounds
const anchorBounds = anchor.getBoundingClientRect()
const elementBounds = element.getBoundingClientRect()
const winWidth = window.innerWidth
const winHeight = window.innerHeight
const screenOffset = 8
let styles = {
maxHeight: null,
minWidth,
maxWidth,
maxHeight,
minWidth: useAnchorWidth ? anchorBounds.width : minWidth,
maxWidth: useAnchorWidth ? anchorBounds.width : maxWidth,
left: null,
top: null,
}
// Ignore all our logic for custom logic
if (typeof customUpdate === "function") {
styles = customUpdate(anchorBounds, elementBounds, {
...styles,
offset: opts.offset,
})
} else {
// Determine vertical styles
if (align === "right-outside" || align === "left-outside") {
styles.top =
anchorBounds.top + anchorBounds.height / 2 - elementBounds.height / 2
styles.maxHeight = maxHeight
if (styles.top + elementBounds.height > window.innerHeight) {
styles.top = window.innerHeight - elementBounds.height
}
} else if (
window.innerHeight - anchorBounds.bottom <
(maxHeight || 100)
) {
styles.top = anchorBounds.top - elementBounds.height - offset
styles.maxHeight = maxHeight || 240
} else {
styles.top = anchorBounds.bottom + offset
styles.maxHeight =
maxHeight || window.innerHeight - anchorBounds.bottom - 20
}
// Otherwise position ourselves as normal
else {
// Checks if we overflow off the screen. We only report that we overflow
// when the alternative dimension is larger than the one we are checking.
const doesXOverflow = () => {
const overflows = styles.left + elementBounds.width > winWidth
return overflows && anchorBounds.left > winWidth - anchorBounds.right
}
const doesYOverflow = () => {
const overflows = styles.top + elementBounds.height > winHeight
return overflows && anchorBounds.top > winHeight - anchorBounds.bottom
}
// Determine horizontal styles
if (!maxWidth && useAnchorWidth) {
styles.maxWidth = anchorBounds.width
// Applies a dynamic max height constraint if appropriate
const applyMaxHeight = height => {
if (!styles.maxHeight && resizable) {
styles.maxHeight = height
}
}
if (useAnchorWidth) {
styles.minWidth = anchorBounds.width
// Applies the X strategy to our styles
const applyXStrategy = strategy => {
switch (strategy) {
case Strategies.StartToStart:
default:
styles.left = anchorBounds.left
break
case Strategies.EndToEnd:
styles.left = anchorBounds.right - elementBounds.width
break
case Strategies.StartToEnd:
styles.left = anchorBounds.right + offset
break
case Strategies.EndToStart:
styles.left = anchorBounds.left - elementBounds.width - offset
break
case Strategies.MidPoint:
styles.left =
anchorBounds.left +
anchorBounds.width / 2 -
elementBounds.width / 2
break
case Strategies.ScreenEdge:
styles.left = winWidth - elementBounds.width - screenOffset
break
}
}
// Applies the Y strategy to our styles
const applyYStrategy = strategy => {
switch (strategy) {
case Strategies.StartToStart:
styles.top = anchorBounds.top
applyMaxHeight(winHeight - anchorBounds.top - screenOffset)
break
case Strategies.EndToEnd:
styles.top = anchorBounds.bottom - elementBounds.height
applyMaxHeight(anchorBounds.bottom - screenOffset)
break
case Strategies.StartToEnd:
default:
styles.top = anchorBounds.bottom + offset
applyMaxHeight(winHeight - anchorBounds.bottom - screenOffset)
break
case Strategies.EndToStart:
styles.top = anchorBounds.top - elementBounds.height - offset
applyMaxHeight(anchorBounds.top - screenOffset)
break
case Strategies.MidPoint:
styles.top =
anchorBounds.top +
anchorBounds.height / 2 -
elementBounds.height / 2
break
case Strategies.ScreenEdge:
styles.top = winHeight - elementBounds.height - screenOffset
applyMaxHeight(winHeight - 2 * screenOffset)
break
}
}
// Determine X strategy
if (align === "right") {
styles.left =
anchorBounds.left + anchorBounds.width - elementBounds.width
applyXStrategy(Strategies.EndToEnd)
} else if (align === "right-outside") {
styles.left = anchorBounds.right + offset
applyXStrategy(Strategies.StartToEnd)
} else if (align === "left-outside") {
styles.left = anchorBounds.left - elementBounds.width - offset
applyXStrategy(Strategies.EndToStart)
} else {
styles.left = anchorBounds.left
applyXStrategy(Strategies.StartToStart)
}
// Determine Y strategy
if (align === "right-outside" || align === "left-outside") {
applyYStrategy(Strategies.MidPoint)
} else {
applyYStrategy(Strategies.StartToEnd)
}
// Handle screen overflow
if (doesXOverflow()) {
// Swap left to right
if (align === "left") {
applyXStrategy(Strategies.EndToEnd)
}
// Swap right-outside to left-outside
else if (align === "right-outside") {
applyXStrategy(Strategies.EndToStart)
}
}
if (doesYOverflow()) {
// If wrapping, lock to the bottom of the screen and also reposition to
// the side to not block the anchor
if (wrap) {
applyYStrategy(Strategies.MidPoint)
if (doesYOverflow()) {
applyYStrategy(Strategies.ScreenEdge)
}
applyXStrategy(Strategies.StartToEnd)
if (doesXOverflow()) {
applyXStrategy(Strategies.EndToStart)
}
}
// Othewise invert as normal
else {
// If using an outside strategy then lock to the bottom of the screen
if (align === "left-outside" || align === "right-outside") {
applyYStrategy(Strategies.ScreenEdge)
}
// Otherwise flip above
else {
applyYStrategy(Strategies.EndToStart)
}
}
}
}

View file

@ -1,268 +0,0 @@
<script>
import Flatpickr from "svelte-flatpickr"
import "flatpickr/dist/flatpickr.css"
import "@spectrum-css/inputgroup/dist/index-vars.css"
import "@spectrum-css/textfield/dist/index-vars.css"
import "@spectrum-css/picker/dist/index-vars.css"
import { createEventDispatcher } from "svelte"
import { uuid } from "../../helpers"
export let id = null
export let disabled = false
export let readonly = false
export let enableTime = true
export let value = null
export let placeholder = null
export let appendTo = undefined
export let timeOnly = false
export let ignoreTimezones = false
export let time24hr = false
export let range = false
export let flatpickr
export let useKeyboardShortcuts = true
const dispatch = createEventDispatcher()
const flatpickrId = `${uuid()}-wrapper`
let open = false
let flatpickrOptions
// Another classic flatpickr issue. Errors were randomly being thrown due to
// flatpickr internal code. Making sure that "destroy" is a valid function
// fixes it. The sooner we remove flatpickr the better.
$: {
if (flatpickr && !flatpickr.destroy) {
flatpickr.destroy = () => {}
}
}
const resolveTimeStamp = timestamp => {
let maskedDate = new Date(`0-${timestamp}`)
if (maskedDate instanceof Date && !isNaN(maskedDate.getTime())) {
return maskedDate
} else {
return null
}
}
$: flatpickrOptions = {
element: `#${flatpickrId}`,
enableTime: timeOnly || enableTime || false,
noCalendar: timeOnly || false,
altInput: true,
time_24hr: time24hr || false,
altFormat: timeOnly ? "H:i" : enableTime ? "F j Y, H:i" : "F j, Y",
wrap: true,
mode: range ? "range" : "single",
appendTo,
disableMobile: "true",
onReady: () => {
let timestamp = resolveTimeStamp(value)
if (timeOnly && timestamp) {
dispatch("change", timestamp.toISOString())
}
},
onOpen: () => dispatch("open"),
onClose: () => dispatch("close"),
}
$: redrawOptions = {
timeOnly,
enableTime,
time24hr,
disabled,
}
const handleChange = event => {
const [dates] = event.detail
const noTimezone = enableTime && !timeOnly && ignoreTimezones
let newValue = dates[0]
if (newValue) {
newValue = newValue.toISOString()
}
// If time only set date component to 2000-01-01
if (timeOnly) {
newValue = `2000-01-01T${newValue.split("T")[1]}`
}
// For date-only fields, construct a manual timestamp string without a time
// or time zone
else if (!enableTime) {
const year = dates[0].getFullYear()
const month = `${dates[0].getMonth() + 1}`.padStart(2, "0")
const day = `${dates[0].getDate()}`.padStart(2, "0")
newValue = `${year}-${month}-${day}T00:00:00.000`
}
// For non-timezone-aware fields, create an ISO 8601 timestamp of the exact
// time picked, without timezone
else if (noTimezone) {
const offset = dates[0].getTimezoneOffset() * 60000
newValue = new Date(dates[0].getTime() - offset)
.toISOString()
.slice(0, -1)
}
if (range) {
dispatch("change", event.detail)
} else {
dispatch("change", newValue)
}
}
const clearDateOnBackspace = event => {
if (["Backspace", "Clear", "Delete"].includes(event.key)) {
dispatch("change", "")
flatpickr.close()
}
}
const onOpen = () => {
open = true
if (useKeyboardShortcuts) {
document.addEventListener("keyup", clearDateOnBackspace)
}
}
const onClose = () => {
open = false
if (useKeyboardShortcuts) {
document.removeEventListener("keyup", clearDateOnBackspace)
}
// Manually blur all input fields since flatpickr creates a second
// duplicate input field.
// We need to blur both because the focus styling does not get properly
// applied.
const els = document.querySelectorAll(`#${flatpickrId} input`)
els.forEach(el => el.blur())
}
const parseDate = val => {
if (!val) {
return null
}
let date
let time
// it is a string like 00:00:00, just time
let ts = resolveTimeStamp(val)
if (timeOnly && ts) {
date = ts
} else if (val instanceof Date) {
// Use real date obj if already parsed
date = val
} else if (isNaN(val)) {
// Treat as date string of some sort
date = new Date(val)
} else {
// Treat as numerical timestamp
date = new Date(parseInt(val))
}
time = date.getTime()
if (isNaN(time)) {
return null
}
// By rounding to the nearest second we avoid locking up in an endless
// loop in the builder, caused by potentially enriching {{ now }} to every
// millisecond.
return new Date(Math.floor(time / 1000) * 1000)
}
</script>
{#key redrawOptions}
<Flatpickr
bind:flatpickr
value={range ? value : parseDate(value)}
on:open={onOpen}
on:close={onClose}
options={flatpickrOptions}
on:change={handleChange}
element={`#${flatpickrId}`}
>
<div
id={flatpickrId}
class:is-disabled={disabled || readonly}
class="flatpickr spectrum-InputGroup spectrum-Datepicker"
class:is-focused={open}
aria-readonly="false"
aria-required="false"
aria-haspopup="true"
>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
on:click={flatpickr?.open}
class="spectrum-Textfield spectrum-InputGroup-textfield"
class:is-disabled={disabled}
>
<input
{disabled}
{readonly}
data-input
type="text"
class="spectrum-Textfield-input spectrum-InputGroup-input"
class:is-disabled={disabled}
{placeholder}
{id}
{value}
/>
</div>
<button
type="button"
class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button"
tabindex="-1"
class:is-disabled={disabled}
on:click={flatpickr?.open}
>
<svg
class="spectrum-Icon spectrum-Icon--sizeM"
focusable="false"
aria-hidden="true"
aria-label="Calendar"
>
<use xlink:href="#spectrum-icon-18-Calendar" />
</svg>
</button>
</div>
</Flatpickr>
{/key}
{#if open}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="overlay" on:mousedown|self={flatpickr?.close} />
{/if}
<style>
.spectrum-Textfield-input {
pointer-events: none;
}
.spectrum-Textfield:not(.is-disabled):hover {
cursor: pointer;
}
.flatpickr {
width: 100%;
overflow: hidden;
}
.flatpickr .spectrum-Textfield {
width: 100%;
}
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 999;
max-height: 100%;
}
:global(.flatpickr-calendar) {
font-family: var(--font-sans);
}
.is-disabled {
pointer-events: none !important;
}
</style>

View file

@ -0,0 +1,249 @@
<script>
import { cleanInput } from "./utils"
import Select from "../../Select.svelte"
import dayjs from "dayjs"
import NumberInput from "./NumberInput.svelte"
import { createEventDispatcher } from "svelte"
export let value
const dispatch = createEventDispatcher()
const DaysOfWeek = [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
]
const MonthsOfYear = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
]
const now = dayjs()
let calendarDate
$: calendarDate = dayjs(value || dayjs()).startOf("month")
$: mondays = getMondays(calendarDate)
const getMondays = monthStart => {
if (!monthStart?.isValid()) {
return []
}
let monthEnd = monthStart.endOf("month")
let calendarStart = monthStart.startOf("week")
const numWeeks = Math.ceil((monthEnd.diff(calendarStart, "day") + 1) / 7)
let mondays = []
for (let i = 0; i < numWeeks; i++) {
mondays.push(calendarStart.add(i, "weeks"))
}
return mondays
}
const handleCalendarYearChange = e => {
calendarDate = calendarDate.year(parseInt(e.target.value))
}
const handleDateChange = date => {
const base = value || now
dispatch(
"change",
base.year(date.year()).month(date.month()).date(date.date())
)
}
export const setDate = date => {
calendarDate = date
}
const cleanYear = cleanInput({ max: 9999, pad: 0, fallback: now.year() })
</script>
<div class="spectrum-Calendar">
<div class="spectrum-Calendar-header">
<div
class="spectrum-Calendar-title"
aria-live="assertive"
aria-atomic="true"
>
<div class="month-selector">
<Select
autoWidth
placeholder={null}
options={MonthsOfYear.map((m, idx) => ({ label: m, value: idx }))}
value={calendarDate.month()}
on:change={e => (calendarDate = calendarDate.month(e.detail))}
/>
</div>
<NumberInput
value={calendarDate.year()}
min={0}
max={9999}
width={64}
on:change={handleCalendarYearChange}
on:input={cleanYear}
/>
</div>
<button
aria-label="Previous"
title="Previous"
class="spectrum-ActionButton spectrum-ActionButton--quiet spectrum-Calendar-prevMonth"
on:click={() => (calendarDate = calendarDate.subtract(1, "month"))}
>
<svg
class="spectrum-Icon spectrum-UIIcon-ChevronLeft100"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Chevron100" />
</svg>
</button>
<button
aria-label="Next"
title="Next"
class="spectrum-ActionButton spectrum-ActionButton--quiet spectrum-Calendar-nextMonth"
on:click={() => (calendarDate = calendarDate.add(1, "month"))}
>
<svg
class="spectrum-Icon spectrum-UIIcon-ChevronRight100"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Chevron100" />
</svg>
</button>
</div>
<div
class="spectrum-Calendar-body"
aria-readonly="true"
aria-disabled="false"
>
<table role="presentation" class="spectrum-Calendar-table">
<thead role="presentation">
<tr>
{#each DaysOfWeek as day}
<th scope="col" class="spectrum-Calendar-tableCell">
<abbr class="spectrum-Calendar-dayOfWeek" title={day}>
{day[0]}
</abbr>
</th>
{/each}
</tr>
</thead>
<tbody role="presentation">
{#each mondays as monday}
<tr>
{#each [0, 1, 2, 3, 4, 5, 6] as dayOffset}
{@const date = monday.add(dayOffset, "days")}
{@const outsideMonth = date.month() !== calendarDate.month()}
<td
class="spectrum-Calendar-tableCell"
aria-disabled="true"
aria-selected="false"
aria-invalid="false"
title={date.format("dddd, MMMM D, YYYY")}
on:click={() => handleDateChange(date)}
>
<span
role="presentation"
class="spectrum-Calendar-date"
class:is-outsideMonth={outsideMonth}
class:is-today={!outsideMonth && date.isSame(now, "day")}
class:is-selected={date.isSame(value, "day")}
>
{date.date()}
</span>
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
</div>
</div>
<style>
/* Calendar overrides */
.spectrum-Calendar {
width: auto;
}
.spectrum-Calendar-header {
width: auto;
}
.spectrum-Calendar-title {
display: flex;
justify-content: flex-start;
align-items: stretch;
flex: 1 1 auto;
}
.spectrum-Calendar-header button {
border-radius: 4px;
}
.spectrum-Calendar-date.is-outsideMonth {
visibility: visible;
color: var(--spectrum-global-color-gray-400);
}
.spectrum-Calendar-date.is-today,
.spectrum-Calendar-date.is-today::before {
border-color: var(--spectrum-global-color-gray-400);
}
.spectrum-Calendar-date.is-today.is-selected,
.spectrum-Calendar-date.is-today.is-selected::before {
border-color: var(
--primaryColorHover,
var(--spectrum-global-color-blue-700)
);
}
.spectrum-Calendar-date.is-selected:not(.is-range-selection) {
background: var(--primaryColor, var(--spectrum-global-color-blue-400));
}
.spectrum-Calendar tr {
box-sizing: content-box;
height: 40px;
}
.spectrum-Calendar-tableCell {
box-sizing: content-box;
}
.spectrum-Calendar-nextMonth,
.spectrum-Calendar-prevMonth {
order: 1;
padding: 4px;
}
.spectrum-Calendar-date {
color: var(--spectrum-alias-text-color);
}
.spectrum-Calendar-date.is-selected {
color: white;
}
.spectrum-Calendar-dayOfWeek {
color: var(--spectrum-global-color-gray-600);
}
/* Style select */
.month-selector :global(.spectrum-Picker) {
background: none;
border: none;
padding: 4px 6px;
}
.month-selector :global(.spectrum-Picker:hover),
.month-selector :global(.spectrum-Picker.is-open) {
background: var(--spectrum-global-color-gray-200);
}
.month-selector :global(.spectrum-Picker-label) {
font-size: 18px;
font-weight: bold;
}
</style>

View file

@ -0,0 +1,94 @@
<script>
import Icon from "../../../Icon/Icon.svelte"
import { getDateDisplayValue } from "../../../helpers"
export let anchor
export let disabled
export let readonly
export let error
export let focused
export let placeholder
export let id
export let value
export let icon
export let enableTime
export let timeOnly
$: displayValue = getDateDisplayValue(value, { enableTime, timeOnly })
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
bind:this={anchor}
class:is-disabled={disabled || readonly}
class:is-invalid={!!error}
class:is-focused={focused}
class="spectrum-InputGroup spectrum-Datepicker"
aria-readonly={readonly}
aria-required="false"
aria-haspopup="true"
on:click
>
<div
class="spectrum-Textfield spectrum-InputGroup-textfield"
class:is-disabled={disabled}
class:is-invalid={!!error}
>
{#if !!error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<input
{disabled}
{readonly}
data-input
type="text"
class="spectrum-Textfield-input spectrum-InputGroup-input"
class:is-disabled={disabled}
{placeholder}
{id}
value={displayValue}
/>
</div>
{#if !disabled && !readonly}
<button
type="button"
class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button"
tabindex="-1"
class:is-invalid={!!error}
>
<Icon name={icon} />
</button>
{/if}
</div>
<style>
/* Date label overrides */
.spectrum-Textfield-input {
pointer-events: none;
}
.spectrum-Textfield:not(.is-disabled):hover {
cursor: pointer;
}
.spectrum-Datepicker {
width: 100%;
overflow: hidden;
}
.spectrum-Datepicker .spectrum-Textfield {
width: 100%;
}
.is-disabled {
pointer-events: none !important;
}
input:read-only {
border-right-width: 1px;
border-top-right-radius: var(--spectrum-textfield-border-radius);
border-bottom-right-radius: var(--spectrum-textfield-border-radius);
}
</style>

View file

@ -0,0 +1,83 @@
<script>
import "@spectrum-css/calendar/dist/index-vars.css"
import "@spectrum-css/inputgroup/dist/index-vars.css"
import "@spectrum-css/textfield/dist/index-vars.css"
import Popover from "../../../Popover/Popover.svelte"
import { onMount } from "svelte"
import DateInput from "./DateInput.svelte"
import { parseDate } from "../../../helpers"
import DatePickerPopoverContents from "./DatePickerPopoverContents.svelte"
export let id = null
export let disabled = false
export let readonly = false
export let error = null
export let enableTime = true
export let value = null
export let placeholder = null
export let timeOnly = false
export let ignoreTimezones = false
export let useKeyboardShortcuts = true
export let appendTo = null
export let api = null
export let align = "left"
let isOpen = false
let anchor
let popover
$: parsedValue = parseDate(value, { timeOnly, enableTime })
const onOpen = () => {
isOpen = true
}
const onClose = () => {
isOpen = false
}
onMount(() => {
api = {
open: () => popover?.show(),
close: () => popover?.hide(),
}
})
</script>
<DateInput
bind:anchor
{disabled}
{readonly}
{error}
{placeholder}
{id}
{enableTime}
{timeOnly}
focused={isOpen}
value={parsedValue}
on:click={popover?.show}
icon={timeOnly ? "Clock" : "Calendar"}
/>
<Popover
bind:this={popover}
on:open
on:close
on:open={onOpen}
on:close={onClose}
portalTarget={appendTo}
{anchor}
{align}
resizable={false}
>
{#if isOpen}
<DatePickerPopoverContents
{useKeyboardShortcuts}
{ignoreTimezones}
{enableTime}
{timeOnly}
value={parsedValue}
on:change
/>
{/if}
</Popover>

View file

@ -0,0 +1,102 @@
<script>
import dayjs from "dayjs"
import TimePicker from "./TimePicker.svelte"
import Calendar from "./Calendar.svelte"
import ActionButton from "../../../ActionButton/ActionButton.svelte"
import { createEventDispatcher, onMount } from "svelte"
import { stringifyDate } from "../../../helpers"
export let useKeyboardShortcuts = true
export let ignoreTimezones
export let enableTime
export let timeOnly
export let value
const dispatch = createEventDispatcher()
let calendar
$: showCalendar = !timeOnly
$: showTime = enableTime || timeOnly
const setToNow = () => {
const now = dayjs()
calendar?.setDate(now)
handleChange(now)
}
const handleChange = date => {
dispatch(
"change",
stringifyDate(date, { enableTime, timeOnly, ignoreTimezones })
)
}
const clearDateOnBackspace = event => {
// Ignore if we're typing a value
if (document.activeElement?.tagName.toLowerCase() === "input") {
return
}
if (["Backspace", "Clear", "Delete"].includes(event.key)) {
dispatch("change", null)
}
}
onMount(() => {
if (useKeyboardShortcuts) {
document.addEventListener("keyup", clearDateOnBackspace)
}
return () => {
document.removeEventListener("keyup", clearDateOnBackspace)
}
})
</script>
<div class="date-time-popover">
{#if showCalendar}
<Calendar
{value}
on:change={e => handleChange(e.detail)}
bind:this={calendar}
/>
{/if}
<div class="footer" class:spaced={showCalendar}>
{#if showTime}
<TimePicker {value} on:change={e => handleChange(e.detail)} />
{/if}
<div class="actions">
<ActionButton
disabled={!value}
size="S"
on:click={() => dispatch("change", null)}
>
Clear
</ActionButton>
<ActionButton size="S" on:click={setToNow}>
{showTime ? "Now" : "Today"}
</ActionButton>
</div>
</div>
</div>
<style>
.date-time-popover {
padding: 8px;
overflow: hidden;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
gap: 60px;
}
.footer.spaced {
padding-top: 14px;
}
.actions {
padding: 4px 0;
flex: 1 1 auto;
display: flex;
justify-content: flex-end;
gap: 6px;
}
</style>

View file

@ -0,0 +1,54 @@
<script>
export let value
export let min
export let max
export let hideArrows = false
export let width
$: style = width ? `width:${width}px;` : ""
</script>
<input
class:hide-arrows={hideArrows}
type="number"
{style}
{value}
{min}
{max}
onclick="this.select()"
on:change
on:input
/>
<style>
input {
background: none;
border: none;
outline: none;
color: var(--spectrum-alias-text-color);
padding: 4px 6px 5px 6px;
border-radius: 4px;
transition: background 130ms ease-out;
font-size: 18px;
font-weight: bold;
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
box-sizing: content-box !important;
}
input:focus,
input:hover {
--space: 30px;
background: var(--spectrum-global-color-gray-200);
z-index: 1;
}
/* Hide built-in arrows */
input.hide-arrows::-webkit-outer-spin-button,
input.hide-arrows::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input.hide-arrows {
-moz-appearance: textfield;
}
</style>

View file

@ -0,0 +1,59 @@
<script>
import { cleanInput } from "./utils"
import dayjs from "dayjs"
import NumberInput from "./NumberInput.svelte"
import { createEventDispatcher } from "svelte"
export let value
const dispatch = createEventDispatcher()
$: displayValue = value || dayjs()
const handleHourChange = e => {
dispatch("change", displayValue.hour(parseInt(e.target.value)))
}
const handleMinuteChange = e => {
dispatch("change", displayValue.minute(parseInt(e.target.value)))
}
const cleanHour = cleanInput({ max: 23, pad: 2, fallback: "00" })
const cleanMinute = cleanInput({ max: 59, pad: 2, fallback: "00" })
</script>
<div class="time-picker">
<NumberInput
hideArrows
value={displayValue.hour().toString().padStart(2, "0")}
min={0}
max={23}
width={20}
on:input={cleanHour}
on:change={handleHourChange}
/>
<span>:</span>
<NumberInput
hideArrows
value={displayValue.minute().toString().padStart(2, "0")}
min={0}
max={59}
width={20}
on:input={cleanMinute}
on:change={handleMinuteChange}
/>
</div>
<style>
.time-picker {
display: flex;
flex-direction: row;
align-items: center;
}
.time-picker span {
font-weight: bold;
font-size: 18px;
z-index: 0;
margin-bottom: 1px;
}
</style>

View file

@ -0,0 +1,14 @@
export const cleanInput = ({ max, pad, fallback }) => {
return e => {
if (e.target.value) {
const value = parseInt(e.target.value)
if (isNaN(value)) {
e.target.value = fallback
} else {
e.target.value = Math.min(max, value).toString().padStart(pad, "0")
}
} else {
e.target.value = fallback
}
}
}

View file

@ -0,0 +1,69 @@
<script>
import CoreDatePicker from "./DatePicker/DatePicker.svelte"
import Icon from "../../Icon/Icon.svelte"
export let value = null
export let disabled = false
export let readonly = false
export let error = null
export let appendTo = undefined
export let ignoreTimezones = false
let fromDate
let toDate
</script>
<div class="date-range">
<CoreDatePicker
value={fromDate}
on:change={e => (fromDate = e.detail)}
enableTime={false}
/>
<div class="arrow">
<Icon name="ChevronRight" />
</div>
<CoreDatePicker
value={toDate}
on:change={e => (toDate = e.detail)}
enableTime={false}
/>
</div>
<style>
.date-range {
display: flex;
flex-direction: row;
border: 1px solid var(--spectrum-alias-border-color);
border-radius: 4px;
}
.date-range :global(.spectrum-InputGroup),
.date-range :global(.spectrum-Textfield),
.date-range :global(input) {
min-width: 0 !important;
width: 150px !important;
}
.date-range :global(input) {
border: none;
text-align: center;
}
.date-range :global(button) {
display: none;
}
.date-range :global(> :first-child input),
.date-range :global(> :first-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.date-range :global(> :last-child input),
.date-range :global(> :last-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.arrow {
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
z-index: 1;
}
</style>

View file

@ -155,6 +155,7 @@
useAnchorWidth={!autoWidth}
maxWidth={autoWidth ? 400 : null}
customHeight={customPopoverHeight}
maxHeight={240}
>
<div
class="popover-content"

View file

@ -8,7 +8,9 @@ export { default as CoreTextArea } from "./TextArea.svelte"
export { default as CoreCombobox } from "./Combobox.svelte"
export { default as CoreSwitch } from "./Switch.svelte"
export { default as CoreSearch } from "./Search.svelte"
export { default as CoreDatePicker } from "./DatePicker.svelte"
export { default as CoreDatePicker } from "./DatePicker/DatePicker.svelte"
export { default as CoreDatePickerPopoverContents } from "./DatePicker/DatePickerPopoverContents.svelte"
export { default as CoreDateRangePicker } from "./DateRangePicker.svelte"
export { default as CoreDropzone } from "./Dropzone.svelte"
export { default as CoreStepper } from "./Stepper.svelte"
export { default as CoreRichTextField } from "./RichTextField.svelte"

View file

@ -1,6 +1,6 @@
<script>
import Field from "./Field.svelte"
import DatePicker from "./Core/DatePicker.svelte"
import DatePicker from "./Core/DatePicker/DatePicker.svelte"
import { createEventDispatcher } from "svelte"
export let value = null
@ -11,22 +11,15 @@
export let error = null
export let enableTime = true
export let timeOnly = false
export let time24hr = false
export let placeholder = null
export let appendTo = undefined
export let ignoreTimezones = false
export let range = false
export let helpText = null
const dispatch = createEventDispatcher()
const onChange = e => {
if (range) {
// Flatpickr cant take two dates and work out what to display, needs to be provided a string.
// Like - "Date1 to Date2". Hence passing in that specifically from the array
value = e?.detail[1]
} else {
value = e.detail
}
value = e.detail
dispatch("change", e.detail)
}
</script>
@ -40,10 +33,8 @@
{placeholder}
{enableTime}
{timeOnly}
{time24hr}
{appendTo}
{ignoreTimezones}
{range}
on:change={onChange}
/>
</Field>

View file

@ -0,0 +1,34 @@
<script>
import Field from "./Field.svelte"
import DateRangePicker from "./Core/DateRangePicker.svelte"
import { createEventDispatcher } from "svelte"
export let value = null
export let label = null
export let labelPosition = "above"
export let disabled = false
export let readonly = false
export let error = null
export let helpText = null
export let appendTo = undefined
export let ignoreTimezones = false
const dispatch = createEventDispatcher()
const onChange = e => {
value = e.detail
dispatch("change", e.detail)
}
</script>
<Field {helpText} {label} {labelPosition} {error}>
<DateRangePicker
{error}
{disabled}
{readonly}
{value}
{appendTo}
{ignoreTimezones}
on:change={onChange}
/>
</Field>

View file

@ -18,13 +18,15 @@
export let open = false
export let useAnchorWidth = false
export let dismissible = true
export let offset = 5
export let offset = 4
export let customHeight
export let animate = true
export let customZindex
export let handlePostionUpdate
export let showPopover = true
export let clickOutsideOverride = false
export let resizable = true
export let wrap = false
$: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum"
@ -91,6 +93,8 @@
useAnchorWidth,
offset,
customUpdate: handlePostionUpdate,
resizable,
wrap,
}}
use:clickOutside={{
callback: dismissible ? handleOutsideClick : () => {},
@ -116,12 +120,11 @@
min-width: var(--spectrum-global-dimension-size-2000);
border-color: var(--spectrum-global-color-gray-300);
overflow: auto;
transition: opacity 260ms ease-out, transform 260ms ease-out;
transition: opacity 260ms ease-out;
}
.hidden {
opacity: 0;
pointer-events: none;
transform: translateY(-20px);
}
.customZindex {
z-index: var(--customZindex) !important;

View file

@ -1,4 +1,5 @@
import { helpers } from "@budibase/shared-core"
import dayjs from "dayjs"
export const deepGet = helpers.deepGet
@ -115,3 +116,110 @@ export const copyToClipboard = value => {
}
})
}
// Parsed a date value. This is usually an ISO string, but can be a
// bunch of different formats and shapes depending on schema flags.
export const parseDate = (value, { enableTime = true }) => {
// If empty then invalid
if (!value) {
return null
}
// Certain string values need transformed
if (typeof value === "string") {
// Check for time only values
if (!isNaN(new Date(`0-${value}`))) {
value = `0-${value}`
}
// If date only, check for cases where we received a UTC string
else if (!enableTime && value.endsWith("Z")) {
value = value.split("Z")[0]
}
}
// Parse value and check for validity
const parsedDate = dayjs(value)
if (!parsedDate.isValid()) {
return null
}
// By rounding to the nearest second we avoid locking up in an endless
// loop in the builder, caused by potentially enriching {{ now }} to every
// millisecond.
return dayjs(Math.floor(parsedDate.valueOf() / 1000) * 1000)
}
// Stringifies a dayjs object to create an ISO string that respects the various
// schema flags
export const stringifyDate = (
value,
{ enableTime = true, timeOnly = false, ignoreTimezones = false }
) => {
if (!value) {
return null
}
// Time only fields always ignore timezones, otherwise they make no sense.
// For non-timezone-aware fields, create an ISO 8601 timestamp of the exact
// time picked, without timezone
const offsetForTimezone = (enableTime && ignoreTimezones) || timeOnly
if (offsetForTimezone) {
// Ensure we use the correct offset for the date
const referenceDate = timeOnly ? new Date() : value.toDate()
const offset = referenceDate.getTimezoneOffset() * 60000
return new Date(value.valueOf() - offset).toISOString().slice(0, -1)
}
// For date-only fields, construct a manual timestamp string without a time
// or time zone
else if (!enableTime) {
const year = value.year()
const month = `${value.month() + 1}`.padStart(2, "0")
const day = `${value.date()}`.padStart(2, "0")
return `${year}-${month}-${day}T00:00:00.000`
}
// Otherwise use a normal ISO string with time and timezone
else {
return value.toISOString()
}
}
// Determine the dayjs-compatible format of the browser's default locale
const getPatternForPart = part => {
switch (part.type) {
case "day":
return "D".repeat(part.value.length)
case "month":
return "M".repeat(part.value.length)
case "year":
return "Y".repeat(part.value.length)
case "literal":
return part.value
default:
console.log("Unsupported date part", part)
return ""
}
}
const localeDateFormat = new Intl.DateTimeFormat()
.formatToParts(new Date("2021-01-01"))
.map(getPatternForPart)
.join("")
// Formats a dayjs date according to schema flags
export const getDateDisplayValue = (
value,
{ enableTime = true, timeOnly = false }
) => {
if (!value?.isValid()) {
return ""
}
if (timeOnly) {
return value.format("HH:mm")
} else if (!enableTime) {
return value.format(localeDateFormat)
} else {
return value.format(`${localeDateFormat} HH:mm`)
}
}

View file

@ -3,13 +3,34 @@ import "./bbui.css"
// Spectrum icons
import "@spectrum-css/icon/dist/index-vars.css"
// Components
// Form components
export { default as Input } from "./Form/Input.svelte"
export { default as Stepper } from "./Form/Stepper.svelte"
export { default as TextArea } from "./Form/TextArea.svelte"
export { default as Select } from "./Form/Select.svelte"
export { default as Combobox } from "./Form/Combobox.svelte"
export { default as Dropzone } from "./Form/Dropzone.svelte"
export { default as DatePicker } from "./Form/DatePicker.svelte"
export { default as DateRangePicker } from "./Form/DateRangePicker.svelte"
export { default as Toggle } from "./Form/Toggle.svelte"
export { default as RadioGroup } from "./Form/RadioGroup.svelte"
export { default as Checkbox } from "./Form/Checkbox.svelte"
export { default as InputDropdown } from "./Form/InputDropdown.svelte"
export { default as PickerDropdown } from "./Form/PickerDropdown.svelte"
export { default as EnvDropdown } from "./Form/EnvDropdown.svelte"
export { default as Multiselect } from "./Form/Multiselect.svelte"
export { default as Search } from "./Form/Search.svelte"
export { default as RichTextField } from "./Form/RichTextField.svelte"
export { default as Slider } from "./Form/Slider.svelte"
export { default as File } from "./Form/File.svelte"
// Core form components to be used elsewhere (standard components)
export * from "./Form/Core"
// Fancy form components
export * from "./FancyForm"
// Components
export { default as Drawer } from "./Drawer/Drawer.svelte"
export { default as DrawerContent } from "./Drawer/DrawerContent.svelte"
export { default as Avatar } from "./Avatar/Avatar.svelte"
@ -21,12 +42,6 @@ export { default as ButtonGroup } from "./ButtonGroup/ButtonGroup.svelte"
export { default as ClearButton } from "./ClearButton/ClearButton.svelte"
export { default as Icon } from "./Icon/Icon.svelte"
export { default as IconAvatar } from "./Icon/IconAvatar.svelte"
export { default as Toggle } from "./Form/Toggle.svelte"
export { default as RadioGroup } from "./Form/RadioGroup.svelte"
export { default as Checkbox } from "./Form/Checkbox.svelte"
export { default as InputDropdown } from "./Form/InputDropdown.svelte"
export { default as PickerDropdown } from "./Form/PickerDropdown.svelte"
export { default as EnvDropdown } from "./Form/EnvDropdown.svelte"
export { default as DetailSummary } from "./DetailSummary/DetailSummary.svelte"
export { default as Popover } from "./Popover/Popover.svelte"
export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte"
@ -37,11 +52,6 @@ export { default as Page } from "./Layout/Page.svelte"
export { default as Link } from "./Link/Link.svelte"
export { default as Tooltip } from "./Tooltip/Tooltip.svelte"
export { default as TempTooltip } from "./Tooltip/TempTooltip.svelte"
export {
default as AbsTooltip,
TooltipPosition,
TooltipType,
} from "./Tooltip/AbsTooltip.svelte"
export { default as TooltipWrapper } from "./Tooltip/TooltipWrapper.svelte"
export { default as Menu } from "./Menu/Menu.svelte"
export { default as MenuSection } from "./Menu/Section.svelte"
@ -53,8 +63,6 @@ export { default as NotificationDisplay } from "./Notification/NotificationDispl
export { default as Notification } from "./Notification/Notification.svelte"
export { default as SideNavigation } from "./SideNavigation/Navigation.svelte"
export { default as SideNavigationItem } from "./SideNavigation/Item.svelte"
export { default as DatePicker } from "./Form/DatePicker.svelte"
export { default as Multiselect } from "./Form/Multiselect.svelte"
export { default as Context } from "./context"
export { default as Table } from "./Table/Table.svelte"
export { default as Tabs } from "./Tabs/Tabs.svelte"
@ -64,7 +72,6 @@ export { default as Tag } from "./Tags/Tag.svelte"
export { default as TreeView } from "./TreeView/Tree.svelte"
export { default as TreeItem } from "./TreeView/Item.svelte"
export { default as Divider } from "./Divider/Divider.svelte"
export { default as Search } from "./Form/Search.svelte"
export { default as Pagination } from "./Pagination/Pagination.svelte"
export { default as Badge } from "./Badge/Badge.svelte"
export { default as StatusLight } from "./StatusLight/StatusLight.svelte"
@ -76,15 +83,15 @@ export { default as CopyInput } from "./Input/CopyInput.svelte"
export { default as BannerDisplay } from "./Banner/BannerDisplay.svelte"
export { default as MarkdownEditor } from "./Markdown/MarkdownEditor.svelte"
export { default as MarkdownViewer } from "./Markdown/MarkdownViewer.svelte"
export { default as RichTextField } from "./Form/RichTextField.svelte"
export { default as List } from "./List/List.svelte"
export { default as ListItem } from "./List/ListItem.svelte"
export { default as IconSideNav } from "./IconSideNav/IconSideNav.svelte"
export { default as IconSideNavItem } from "./IconSideNav/IconSideNavItem.svelte"
export { default as Slider } from "./Form/Slider.svelte"
export { default as Accordion } from "./Accordion/Accordion.svelte"
export { default as File } from "./Form/File.svelte"
export { default as OptionSelectDnD } from "./OptionSelectDnD/OptionSelectDnD.svelte"
export { default as AbsTooltip } from "./Tooltip/AbsTooltip.svelte"
export { TooltipPosition, TooltipType } from "./Tooltip/AbsTooltip.svelte"
// Renderers
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
export { default as CodeRenderer } from "./Table/CodeRenderer.svelte"
@ -96,9 +103,6 @@ export { default as Heading } from "./Typography/Heading.svelte"
export { default as Detail } from "./Typography/Detail.svelte"
export { default as Code } from "./Typography/Code.svelte"
// Core form components to be used elsewhere (standard components)
export * from "./Form/Core"
// Actions
export { default as autoResizeTextArea } from "./Actions/autoresize_textarea"
export { default as positionDropdown } from "./Actions/position_dropdown"
@ -110,6 +114,3 @@ export { banner, BANNER_TYPES } from "./Stores/banner"
// Helpers
export * as Helpers from "./helpers"
// Fancy form components
export * from "./FancyForm"

View file

@ -106,6 +106,5 @@
display: flex;
flex-direction: column;
background: var(--background);
overflow: hidden;
}
</style>

View file

@ -515,6 +515,7 @@
/>
{/if}
<Select
placeholder={null}
disabled={!typeEnabled}
bind:value={editableColumn.fieldId}
on:change={onHandleTypeChange}

View file

@ -0,0 +1,89 @@
<script>
import { DatePicker } from "@budibase/bbui"
import dayjs from "dayjs"
import { createEventDispatcher } from "svelte"
import { memo } from "@budibase/frontend-core"
export let value
const dispatch = createEventDispatcher()
const valueStore = memo(value)
let date1
let date2
$: valueStore.set(value)
$: parseValue($valueStore)
const parseValue = value => {
if (!Array.isArray(value) || !value[0] || !value[1]) {
date1 = null
date2 = null
} else {
date1 = value[0]
date2 = value[1]
}
}
const onChangeDate1 = e => {
date1 = e.detail ? dayjs(e.detail).startOf("day") : null
if (date1 && (!date2 || date1.isAfter(date2))) {
date2 = date1.endOf("day")
} else if (!date1) {
date2 = null
}
broadcastChange()
}
const onChangeDate2 = e => {
date2 = e.detail ? dayjs(e.detail).endOf("day") : null
if (date2 && (!date1 || date2.isBefore(date1))) {
date1 = date2.startOf("day")
} else if (!date2) {
date1 = null
}
broadcastChange()
}
const broadcastChange = () => {
dispatch("change", [date1, date2])
}
</script>
<div class="date-range-picker">
<DatePicker
value={date1}
label="Date range"
enableTime={false}
on:change={onChangeDate1}
/>
<DatePicker value={date2} enableTime={false} on:change={onChangeDate2} />
</div>
<style>
.date-range-picker {
display: flex;
flex-direction: row;
align-items: flex-end;
}
/* Overlap date pickers to remove double border, but put the focused one on top */
.date-range-picker :global(.spectrum-InputGroup.is-focused) {
z-index: 1;
}
.date-range-picker :global(> :last-child) {
margin-left: -1px;
}
/* Remove border radius at the join */
.date-range-picker :global(> :first-child .spectrum-InputGroup),
.date-range-picker :global(> :first-child .spectrum-Picker) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.date-range-picker :global(> :last-child .spectrum-InputGroup),
.date-range-picker :global(> :last-child .spectrum-Textfield-input) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
</style>

View file

@ -75,14 +75,12 @@
.relationship-container {
display: flex;
align-items: center;
gap: 20px;
gap: var(--spacing-m);
}
.relationship-part {
flex-basis: 70%;
flex: 1 1 auto;
}
.relationship-type {
flex-basis: 30%;
flex: 0 0 128px;
}
</style>

View file

@ -1,7 +1,6 @@
<script>
import {
Button,
DatePicker,
Divider,
Layout,
notifications,
@ -25,13 +24,13 @@
import BackupsDefault from "assets/backups-default.png"
import { BackupTrigger, BackupType } from "constants/backend/backups"
import { onMount } from "svelte"
import DateRangePicker from "components/common/DateRangePicker.svelte"
let loading = true
let backupData = null
let pageInfo = createPaginationStore()
let filterOpt = null
let startDate = null
let endDate = null
let dateRange = []
let filters = [
{
label: "Manual backup",
@ -52,7 +51,7 @@
]
$: page = $pageInfo.page
$: fetchBackups(filterOpt, page, startDate, endDate)
$: fetchBackups(filterOpt, page, dateRange)
let schema = {
type: {
@ -99,13 +98,13 @@
})
}
async function fetchBackups(filters, page, startDate, endDate) {
async function fetchBackups(filters, page, dateRange) {
const response = await backups.searchBackups({
appId: $appStore.appId,
...filters,
page,
startDate,
endDate,
startDate: dateRange[0],
endDate: dateRange[1],
})
pageInfo.fetched(response.hasNextPage, response.nextPage)
@ -165,7 +164,7 @@
}
onMount(async () => {
await fetchBackups(filterOpt, page, startDate, endDate)
await fetchBackups(filterOpt, page, dateRange)
loading = false
})
</script>
@ -207,7 +206,7 @@
View plans
</Button>
</div>
{:else if !backupData?.length && !loading && !filterOpt && !startDate}
{:else if !backupData?.length && !loading && !filterOpt && !dateRange?.length}
<div class="center">
<Layout noPadding gap="S" justifyItems="center">
<img height="130px" src={BackupsDefault} alt="BackupsDefault" />
@ -236,21 +235,15 @@
bind:value={filterOpt}
/>
</div>
<DatePicker
range={true}
label="Date Range"
on:change={e => {
if (e.detail[0].length > 1) {
startDate = e.detail[0][0].toISOString()
endDate = e.detail[0][1].toISOString()
}
}}
<DateRangePicker
value={dateRange}
on:change={e => (dateRange = e.detail)}
/>
</div>
<div>
<Button cta disabled={loading} on:click={createManualBackup}
>Create new backup</Button
>
<Button cta disabled={loading} on:click={createManualBackup}>
Create new backup
</Button>
</div>
</div>
<div class="table">

View file

@ -12,7 +12,6 @@
Icon,
clickOutside,
CoreTextArea,
DatePicker,
Pagination,
Helpers,
Divider,
@ -27,6 +26,8 @@
import TimeRenderer from "./_components/TimeRenderer.svelte"
import AppColumnRenderer from "./_components/AppColumnRenderer.svelte"
import { cloneDeep } from "lodash"
import DateRangePicker from "components/common/DateRangePicker.svelte"
import dayjs from "dayjs"
const schema = {
date: { width: "0.8fr" },
@ -69,16 +70,13 @@
let sidePanelVisible = false
let wideSidePanel = false
let timer
let startDate = new Date()
startDate.setDate(startDate.getDate() - 30)
let endDate = new Date()
let dateRange = [dayjs().subtract(30, "days"), dayjs()]
$: fetchUsers(userPage, userSearchTerm)
$: fetchLogs({
logsPage,
logSearchTerm,
startDate,
endDate,
dateRange,
selectedUsers,
selectedApps,
selectedEvents,
@ -136,8 +134,7 @@
const fetchLogs = async ({
logsPage,
logSearchTerm,
startDate,
endDate,
dateRange,
selectedUsers,
selectedApps,
selectedEvents,
@ -155,8 +152,8 @@
logsPageInfo.loading()
await auditLogs.search({
bookmark: logsPage,
startDate,
endDate,
startDate: dateRange[0],
endDate: dateRange[1],
fullSearch: logSearchTerm,
userIds: selectedUsers,
appIds: selectedApps,
@ -214,8 +211,8 @@
const downloadLogs = async () => {
try {
window.location = auditLogs.getDownloadUrl({
startDate,
endDate,
startDate: dateRange[0],
endDate: dateRange[1],
fullSearch: logSearchTerm,
userIds: selectedUsers,
appIds: selectedApps,
@ -302,22 +299,9 @@
</div>
<div class="date-picker">
<DatePicker
value={[startDate, endDate]}
placeholder="Choose date range"
range={true}
on:change={e => {
if (e.detail[0]?.length === 1) {
startDate = e.detail[0][0].toISOString()
endDate = ""
} else if (e.detail[0]?.length > 1) {
startDate = e.detail[0][0].toISOString()
endDate = e.detail[0][1].toISOString()
} else {
startDate = ""
endDate = ""
}
}}
<DateRangePicker
value={dateRange}
on:change={e => (dateRange = e.detail)}
/>
</div>
<div class="freeSearch">
@ -488,7 +472,7 @@
flex-direction: row;
gap: var(--spacing-l);
flex-wrap: wrap;
align-items: center;
align-items: flex-end;
}
.side-panel-icons {
@ -505,6 +489,13 @@
.date-picker {
flex-basis: calc(70% - 32px);
min-width: 100px;
display: flex;
flex-direction: row;
}
.date-picker :global(.date-range-picker),
.date-picker :global(.spectrum-Form-item) {
flex: 1 1 auto;
width: 0;
}
.freeSearch {

View file

@ -3869,12 +3869,6 @@
"key": "timeOnly",
"defaultValue": false
},
{
"type": "boolean",
"label": "24-hour time",
"key": "time24hr",
"defaultValue": false
},
{
"type": "boolean",
"label": "Ignore time zones",

View file

@ -24,14 +24,7 @@
"@budibase/shared-core": "0.0.0",
"@budibase/string-templates": "0.0.0",
"@budibase/types": "0.0.0",
"@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3",
"@spectrum-css/link": "^3.1.3",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/tag": "^3.1.4",
"@spectrum-css/typography": "^3.0.2",
"@spectrum-css/vars": "^3.0.1",
"@spectrum-css/card": "3.0.3",
"apexcharts": "^3.22.1",
"dayjs": "^1.10.8",
"downloadjs": "1.4.7",

View file

@ -38,10 +38,8 @@
if (!field || !value) {
return null
}
let low = dayjs.utc().subtract(1, "year")
let high = dayjs.utc().add(1, "day")
if (value === "Last 1 day") {
low = dayjs.utc().subtract(1, "day")
} else if (value === "Last 7 days") {
@ -53,7 +51,6 @@
} else if (value === "Last 6 months") {
low = dayjs.utc().subtract(6, "months")
}
return {
range: {
[field]: {

View file

@ -50,6 +50,8 @@
metadata: { dataSource: table },
},
]
$: height = $component.styles?.normal?.height || "408px"
$: styles = getSanitisedStyles($component.styles)
// Provide additional data context for live binding eval
export const getAdditionalDataContext = () => {
@ -106,39 +108,48 @@
},
}))
}
const getSanitisedStyles = styles => {
return {
...styles,
normal: {
...styles?.normal,
height: undefined,
},
}
}
</script>
<div
use:styleable={$component.styles}
class:in-builder={$builderStore.inBuilder}
>
<Provider {actions}>
<Grid
bind:this={grid}
datasource={table}
{API}
{stripeRows}
{quiet}
{initialFilter}
{initialSortColumn}
{initialSortOrder}
{fixedRowHeight}
{columnWhitelist}
{schemaOverrides}
{repeat}
canAddRows={allowAddRows}
canEditRows={allowEditRows}
canDeleteRows={allowDeleteRows}
canEditColumns={false}
canExpandRows={false}
canSaveSchema={false}
showControls={false}
notifySuccess={notificationStore.actions.success}
notifyError={notificationStore.actions.error}
buttons={enrichedButtons}
on:rowclick={e => onRowClick?.({ row: e.detail })}
/>
</Provider>
<div use:styleable={styles} class:in-builder={$builderStore.inBuilder}>
<span style="--height:{height};">
<Provider {actions}>
<Grid
bind:this={grid}
datasource={table}
{API}
{stripeRows}
{quiet}
{initialFilter}
{initialSortColumn}
{initialSortOrder}
{fixedRowHeight}
{columnWhitelist}
{schemaOverrides}
{repeat}
canAddRows={allowAddRows}
canEditRows={allowEditRows}
canDeleteRows={allowDeleteRows}
canEditColumns={false}
canExpandRows={false}
canSaveSchema={false}
showControls={false}
notifySuccess={notificationStore.actions.success}
notifyError={notificationStore.actions.error}
buttons={enrichedButtons}
on:rowclick={e => onRowClick?.({ row: e.detail })}
/>
</Provider>
</span>
</div>
<style>
@ -149,10 +160,14 @@
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: 4px;
overflow: hidden;
min-height: 230px;
height: 410px;
}
div.in-builder :global(*) {
pointer-events: none;
}
span {
display: contents;
}
span :global(.grid) {
height: var(--height);
}
</style>

View file

@ -1,7 +1,4 @@
export const buildBackupsEndpoints = API => ({
/**
* Gets a list of users in the current tenant.
*/
searchBackups: async ({ appId, trigger, type, page, startDate, endDate }) => {
const opts = {}
if (page) {

View file

@ -1,6 +1,7 @@
<script>
import { onMount, getContext } from "svelte"
import { Dropzone } from "@budibase/bbui"
import GridPopover from "../overlays/GridPopover.svelte"
export let value
export let focused = false
@ -8,7 +9,6 @@
export let readonly = false
export let api
export let invertX = false
export let invertY = false
export let schema
export let maximum
@ -16,6 +16,7 @@
const imageExtensions = ["png", "tiff", "gif", "raw", "jpg", "jpeg"]
let isOpen = false
let anchor
$: editable = focused && !readonly
$: {
@ -73,7 +74,12 @@
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="attachment-cell" class:editable on:click={editable ? open : null}>
<div
class="attachment-cell"
class:editable
on:click={editable ? open : null}
bind:this={anchor}
>
{#each value || [] as attachment}
{#if isImage(attachment.extension)}
<img src={attachment.url} alt={attachment.extension} />
@ -86,16 +92,24 @@
</div>
{#if isOpen}
<div class="dropzone" class:invertX class:invertY>
<Dropzone
{value}
compact
on:change={e => onChange(e.detail)}
maximum={maximum || schema.constraints?.length?.maximum}
{processFiles}
{handleFileTooLarge}
/>
</div>
<GridPopover
open={isOpen}
{anchor}
{invertX}
maxHeight={null}
on:close={close}
>
<div class="dropzone">
<Dropzone
{value}
compact
on:change={e => onChange(e.detail)}
maximum={maximum || schema.constraints?.length?.maximum}
{processFiles}
{handleFileTooLarge}
/>
</div>
</GridPopover>
{/if}
<style>
@ -129,23 +143,8 @@
user-select: none;
}
.dropzone {
position: absolute;
top: 100%;
left: 0;
width: 320px;
background: var(--grid-background-alt);
border: var(--cell-border);
width: 320px;
padding: var(--cell-padding);
box-shadow: 0 0 20px -4px rgba(0, 0, 0, 0.15);
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
.dropzone.invertX {
left: auto;
right: 0;
}
.dropzone.invertY {
transform: translateY(-100%);
top: 0;
}
</style>

View file

@ -1,7 +1,8 @@
<script>
import dayjs from "dayjs"
import { CoreDatePicker, Icon } from "@budibase/bbui"
import { CoreDatePickerPopoverContents, Icon, Helpers } from "@budibase/bbui"
import { onMount } from "svelte"
import dayjs from "dayjs"
import GridPopover from "../overlays/GridPopover.svelte"
export let value
export let schema
@ -9,83 +10,117 @@
export let focused = false
export let readonly = false
export let api
export let invertX = false
let flatpickr
let isOpen
let anchor
// Adding the 0- will turn a string like 00:00:00 into a valid ISO
// date, but will make actual ISO dates invalid
$: isTimeValue = !isNaN(new Date(`0-${value}`))
$: timeOnly = isTimeValue || schema?.timeOnly
$: dateOnly = schema?.dateOnly
$: format = timeOnly
? "HH:mm:ss"
: dateOnly
? "MMM D YYYY"
: "MMM D YYYY, HH:mm"
$: timeOnly = schema?.timeOnly
$: enableTime = !schema?.dateOnly
$: ignoreTimezones = schema?.ignoreTimezones
$: editable = focused && !readonly
$: displayValue = getDisplayValue(value, format, timeOnly, isTimeValue)
const getDisplayValue = (value, format, timeOnly, isTimeValue) => {
if (!value) {
return ""
}
// Parse full date strings
if (!timeOnly || !isTimeValue) {
return dayjs(value).format(format)
}
// Otherwise must be a time string
return dayjs(`0-${value}`).format(format)
}
// Ensure we close flatpickr when unselected
$: parsedValue = Helpers.parseDate(value, {
timeOnly,
enableTime,
ignoreTimezones,
})
$: displayValue = getDisplayValue(parsedValue, timeOnly, enableTime)
// Ensure open state matches desired state
$: {
if (!focused) {
flatpickr?.close()
if (!focused && isOpen) {
close()
}
}
const onKeyDown = () => {
return isOpen
const getDisplayValue = (value, timeOnly, enableTime) => {
return Helpers.getDateDisplayValue(value, {
enableTime,
timeOnly,
})
}
const open = () => {
isOpen = true
}
const close = () => {
isOpen = false
// Only save the changed value when closing. If the value is unchanged then
// this is handled upstream and no action is taken.
onChange(value)
}
const onKeyDown = e => {
if (!isOpen) {
return false
}
e.preventDefault()
if (e.key === "ArrowUp") {
changeDate(-1, "week")
} else if (e.key === "ArrowDown") {
changeDate(1, "week")
} else if (e.key === "ArrowLeft") {
changeDate(-1, "day")
} else if (e.key === "ArrowRight") {
changeDate(1, "day")
} else if (e.key === "Enter") {
close()
}
return true
}
const changeDate = (quantity, unit) => {
let newValue
if (!value) {
newValue = dayjs()
} else {
newValue = dayjs(value).add(quantity, unit)
}
value = Helpers.stringifyDate(newValue, {
enableTime,
timeOnly,
ignoreTimezones,
})
}
onMount(() => {
api = {
onKeyDown,
focus: () => flatpickr?.open(),
blur: () => flatpickr?.close(),
focus: open,
blur: close,
isActive: () => isOpen,
}
})
</script>
<div class="container">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="container"
class:editable
on:click={editable ? open : null}
bind:this={anchor}
>
<div class="value">
{#if value}
{displayValue}
{/if}
{displayValue}
</div>
{#if editable}
<Icon name="Calendar" />
{/if}
</div>
{#if editable}
<div class="picker">
<CoreDatePicker
{value}
on:change={e => onChange(e.detail)}
appendTo={document.documentElement}
enableTime={!dateOnly}
{timeOnly}
time24hr
ignoreTimezones={schema.ignoreTimezones}
bind:flatpickr
on:open={() => (isOpen = true)}
on:close={() => (isOpen = false)}
{#if isOpen}
<GridPopover {anchor} {invertX} maxHeight={null} on:close={close}>
<CoreDatePickerPopoverContents
value={parsedValue}
useKeyboardShortcuts={false}
on:change={e => (value = e.detail)}
{enableTime}
{timeOnly}
{ignoreTimezones}
/>
</div>
</GridPopover>
{/if}
<style>
@ -97,6 +132,10 @@
align-items: center;
flex: 1 1 auto;
gap: var(--cell-spacing);
user-select: none;
}
.container.editable:hover {
cursor: pointer;
}
.value {
flex: 1 1 auto;
@ -105,15 +144,6 @@
text-overflow: ellipsis;
white-space: nowrap;
line-height: 20px;
}
.picker {
position: absolute;
opacity: 0;
}
.picker :global(.flatpickr) {
min-width: 0;
}
.picker :global(.spectrum-Textfield-input) {
width: 100%;
height: 20px;
}
</style>

View file

@ -1,20 +1,14 @@
<script>
import { getContext, onMount, tick } from "svelte"
import { canBeDisplayColumn, canBeSortColumn } from "@budibase/shared-core"
import {
Icon,
Popover,
Menu,
MenuItem,
clickOutside,
Modal,
} from "@budibase/bbui"
import { Icon, Menu, MenuItem, Modal } from "@budibase/bbui"
import GridCell from "./GridCell.svelte"
import { getColumnIcon } from "../lib/utils"
import MigrationModal from "../controls/MigrationModal.svelte"
import { debounce } from "../../../utils/utils"
import { FieldType, FormulaType } from "@budibase/types"
import { TableNames } from "../../../constants"
import GridPopover from "../overlays/GridPopover.svelte"
export let column
export let idx
@ -23,7 +17,6 @@
reorder,
isReordering,
isResizing,
rand,
sort,
visibleColumns,
dispatch,
@ -52,7 +45,6 @@
let open = false
let editIsOpen = false
let timeout
let popover
let migrationModal
let searchValue
let input
@ -67,6 +59,11 @@
$: debouncedUpdateFilter(searchValue)
$: orderable = !column.primaryDisplay
const close = () => {
open = false
editIsOpen = false
}
const getSortingLabels = type => {
switch (type) {
case FieldType.NUMBER:
@ -106,12 +103,8 @@
dispatch("edit-column", column.schema)
}
const cancelEdit = () => {
popover.hide()
editIsOpen = false
}
const onMouseDown = e => {
ui.actions.blur()
if ((e.touches?.length || e.button === 0) && orderable) {
timeout = setTimeout(() => {
reorder.actions.startReordering(column.name, e)
@ -237,7 +230,7 @@
}
const debouncedUpdateFilter = debounce(updateFilter, 250)
onMount(() => subscribe("close-edit-column", cancelEdit))
onMount(() => subscribe("close-edit-column", close))
</script>
<Modal bind:this={migrationModal}>
@ -314,89 +307,88 @@
</GridCell>
</div>
<Popover
bind:open
bind:this={popover}
{anchor}
align="right"
offset={0}
popoverTarget={document.getElementById(`grid-${rand}`)}
customZindex={50}
>
{#if editIsOpen}
<div
use:clickOutside={() => {
editIsOpen = false
}}
class="content"
>
<slot />
</div>
{:else}
<Menu>
<MenuItem
icon="Edit"
on:click={editColumn}
disabled={!$config.canEditColumns || column.schema.disabled}
>
Edit column
</MenuItem>
<MenuItem
icon="Duplicate"
on:click={duplicateColumn}
disabled={!$config.canEditColumns}
>
Duplicate column
</MenuItem>
<MenuItem
icon="Label"
on:click={makeDisplayColumn}
disabled={column.primaryDisplay ||
!canBeDisplayColumn(column.schema.type)}
>
Use as display column
</MenuItem>
<MenuItem
icon="SortOrderUp"
on:click={sortAscending}
disabled={!canBeSortColumn(column.schema.type) ||
(column.name === $sort.column && $sort.order === "ascending")}
>
Sort {sortingLabels.ascending}
</MenuItem>
<MenuItem
icon="SortOrderDown"
on:click={sortDescending}
disabled={!canBeSortColumn(column.schema.type) ||
(column.name === $sort.column && $sort.order === "descending")}
>
Sort {sortingLabels.descending}
</MenuItem>
<MenuItem disabled={!canMoveLeft} icon="ChevronLeft" on:click={moveLeft}>
Move left
</MenuItem>
<MenuItem
disabled={!canMoveRight}
icon="ChevronRight"
on:click={moveRight}
>
Move right
</MenuItem>
<MenuItem
disabled={column.primaryDisplay || !$config.showControls}
icon="VisibilityOff"
on:click={hideColumn}
>
Hide column
</MenuItem>
{#if $config.canEditColumns && column.schema.type === "link" && column.schema.tableId === TableNames.USERS}
<MenuItem icon="User" on:click={openMigrationModal}>
Migrate to user column
{#if open}
<GridPopover
{anchor}
align="right"
on:close={close}
maxHeight={null}
resizable
>
{#if editIsOpen}
<div class="content">
<slot />
</div>
{:else}
<Menu>
<MenuItem
icon="Edit"
on:click={editColumn}
disabled={!$config.canEditColumns || column.schema.disabled}
>
Edit column
</MenuItem>
{/if}
</Menu>
{/if}
</Popover>
<MenuItem
icon="Duplicate"
on:click={duplicateColumn}
disabled={!$config.canEditColumns}
>
Duplicate column
</MenuItem>
<MenuItem
icon="Label"
on:click={makeDisplayColumn}
disabled={column.primaryDisplay ||
!canBeDisplayColumn(column.schema.type)}
>
Use as display column
</MenuItem>
<MenuItem
icon="SortOrderUp"
on:click={sortAscending}
disabled={!canBeSortColumn(column.schema.type) ||
(column.name === $sort.column && $sort.order === "ascending")}
>
Sort {sortingLabels.ascending}
</MenuItem>
<MenuItem
icon="SortOrderDown"
on:click={sortDescending}
disabled={!canBeSortColumn(column.schema.type) ||
(column.name === $sort.column && $sort.order === "descending")}
>
Sort {sortingLabels.descending}
</MenuItem>
<MenuItem
disabled={!canMoveLeft}
icon="ChevronLeft"
on:click={moveLeft}
>
Move left
</MenuItem>
<MenuItem
disabled={!canMoveRight}
icon="ChevronRight"
on:click={moveRight}
>
Move right
</MenuItem>
<MenuItem
disabled={column.primaryDisplay || !$config.showControls}
icon="VisibilityOff"
on:click={hideColumn}
>
Hide column
</MenuItem>
{#if $config.canEditColumns && column.schema.type === "link" && column.schema.tableId === TableNames.USERS}
<MenuItem icon="User" on:click={openMigrationModal}>
Migrate to user column
</MenuItem>
{/if}
</Menu>
{/if}
</GridPopover>
{/if}
<style>
.header-cell {
@ -490,7 +482,7 @@
}
.content {
width: 300px;
width: 360px;
padding: 20px;
display: flex;
flex-direction: column;

View file

@ -1,6 +1,7 @@
<script>
import { onMount, tick } from "svelte"
import { clickOutside } from "@budibase/bbui"
import GridPopover from "../overlays/GridPopover.svelte"
export let value
export let focused = false
@ -8,10 +9,10 @@
export let readonly = false
export let api
export let invertX = false
export let invertY = false
let textarea
let isOpen = false
let anchor
$: editable = focused && !readonly
$: {
@ -52,25 +53,30 @@
})
</script>
{#if isOpen}
<textarea
class:invertX
class:invertY
bind:this={textarea}
value={value || ""}
on:change={handleChange}
on:wheel|stopPropagation
spellcheck="false"
use:clickOutside={close}
/>
{:else}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="long-form-cell" on:click={editable ? open : null} class:editable>
<div class="value">
{value || ""}
</div>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="long-form-cell"
on:click={editable ? open : null}
class:editable
bind:this={anchor}
>
<div class="value">
{value || ""}
</div>
</div>
{#if isOpen}
<GridPopover {anchor} {invertX} on:close={close}>
<textarea
bind:this={textarea}
value={value || ""}
on:change={handleChange}
on:wheel|stopPropagation
spellcheck="false"
use:clickOutside={close}
/>
</GridPopover>
{/if}
<style>
@ -93,30 +99,20 @@
line-height: 20px;
}
textarea {
border: none;
width: 320px;
flex: 1 1 auto;
height: var(--max-cell-render-overflow);
padding: var(--cell-padding);
margin: 0;
border: 2px solid var(--cell-color);
background: var(--cell-background);
font-size: var(--cell-font-size);
font-family: var(--font-sans);
color: inherit;
position: absolute;
top: 0;
left: 0;
width: calc(100% + var(--max-cell-render-width-overflow));
height: calc(var(--row-height) + var(--max-cell-render-height));
z-index: 1;
border-radius: 2px;
resize: none;
line-height: 20px;
}
textarea.invertX {
left: auto;
right: 0;
}
textarea.invertY {
transform: translateY(-100%);
top: calc(100% + 1px);
overflow: auto;
}
textarea:focus {
outline: none;

View file

@ -1,7 +1,8 @@
<script>
import { Icon, clickOutside } from "@budibase/bbui"
import { Icon } from "@budibase/bbui"
import { getColor } from "../lib/utils"
import { onMount } from "svelte"
import GridPopover from "../overlays/GridPopover.svelte"
export let value
export let schema
@ -10,12 +11,12 @@
export let multi = false
export let readonly = false
export let api
export let invertX = false
export let invertY = false
export let invertX
export let contentLines = 1
let isOpen = false
let focusedOptionIdx = null
let anchor
$: options = schema?.constraints?.inclusion || []
$: optionColors = schema?.optionColors || {}
@ -23,7 +24,7 @@
$: values = Array.isArray(value) ? value : [value].filter(x => x != null)
$: {
// Close when deselected
if (!focused) {
if (!focused && isOpen) {
close()
}
}
@ -89,6 +90,7 @@
class:editable
class:open
on:click|self={editable ? open : null}
bind:this={anchor}
>
<div
class="values"
@ -115,16 +117,15 @@
<Icon name="ChevronDown" />
</div>
{/if}
{#if isOpen}
<div
class="options"
class:invertX
class:invertY
on:wheel={e => e.stopPropagation()}
use:clickOutside={close}
>
</div>
{#if isOpen}
<GridPopover {anchor} {invertX} on:close={close}>
<div class="options">
{#each options as option, idx}
{@const color = optionColors[option] || getOptionColor(option)}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="option"
on:click={() => toggleOption(option)}
@ -132,7 +133,9 @@
on:mouseenter={() => (focusedOptionIdx = idx)}
>
<div class="badge text" style="--color: {color}">
{option}
<span>
{option}
</span>
</div>
{#if values.includes(option)}
<Icon name="Checkmark" color="var(--accent-color)" />
@ -140,8 +143,8 @@
</div>
{/each}
</div>
{/if}
</div>
</GridPopover>
{/if}
<style>
.container {
@ -211,28 +214,10 @@
);
}
.options {
min-width: calc(100% + 2px);
position: absolute;
top: 100%;
left: -1px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
max-height: var(--max-cell-render-height);
overflow-y: auto;
border: var(--cell-border);
box-shadow: 0 0 20px -4px rgba(0, 0, 0, 0.15);
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
.options.invertX {
left: auto;
right: 0;
}
.options.invertY {
transform: translateY(-100%);
top: 0;
}
.option {
flex: 0 0 var(--default-row-height);
@ -242,10 +227,10 @@
justify-content: space-between;
align-items: center;
gap: var(--cell-spacing);
background-color: var(--grid-background-alt);
}
.option:hover,
.option.focused {
background-color: var(--spectrum-global-color-gray-200);
background-color: var(--grid-background-alt);
cursor: pointer;
}
</style>

View file

@ -1,10 +1,11 @@
<script>
import { getColor } from "../lib/utils"
import { onMount, getContext } from "svelte"
import { Icon, Input, ProgressCircle, clickOutside } from "@budibase/bbui"
import { Icon, Input, ProgressCircle } from "@budibase/bbui"
import { debounce } from "../../../utils/utils"
import GridPopover from "../overlays/GridPopover.svelte"
const { API, dispatch, cache } = getContext("grid")
const { API, cache } = getContext("grid")
export let value
export let api
@ -13,7 +14,6 @@
export let schema
export let onChange
export let invertX = false
export let invertY = false
export let contentLines = 1
export let searchFunction = API.searchTable
export let primaryDisplay
@ -27,15 +27,15 @@
let candidateIndex
let lastSearchId
let searching = false
let valuesHeight = 0
let container
let anchor
$: oneRowOnly = schema?.relationshipType === "one-to-many"
$: editable = focused && !readonly
$: lookupMap = buildLookupMap(value, isOpen)
$: debouncedSearch(searchString)
$: {
if (!focused) {
if (!focused && isOpen) {
close()
}
}
@ -125,7 +125,6 @@
const open = async () => {
isOpen = true
valuesHeight = container.getBoundingClientRect().height
// Find the primary display for the related table
if (!primaryDisplay) {
@ -204,14 +203,6 @@
close()
}
const showRelationship = async id => {
const relatedRow = await API.fetchRow({
tableId: schema.tableId,
rowId: id,
})
dispatch("edit-row", relatedRow)
}
const readable = value => {
if (value == null) {
return ""
@ -238,8 +229,8 @@
class="wrapper"
class:editable
class:focused
class:invertY
style="--color:{color};"
bind:this={anchor}
>
<div class="container" bind:this={container}>
<div
@ -250,11 +241,7 @@
{#each value || [] as relationship}
{#if relationship[primaryDisplay] || relationship.primaryDisplay}
<div class="badge">
<span
on:click={editable
? () => showRelationship(relationship._id)
: null}
>
<span>
{readable(
relationship[primaryDisplay] || relationship.primaryDisplay
)}
@ -282,16 +269,13 @@
</div>
{/if}
</div>
</div>
{#if isOpen}
<div
class="dropdown"
class:invertX
class:invertY
on:wheel|stopPropagation
use:clickOutside={close}
style="--values-height:{valuesHeight}px;"
>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
{#if isOpen}
<GridPopover open={isOpen} {anchor} {invertX} on:close={close}>
<div class="dropdown" on:wheel|stopPropagation>
<div class="search">
<Input
autofocus
@ -327,8 +311,8 @@
</div>
{/if}
</div>
{/if}
</div>
</GridPopover>
{/if}
<style>
.wrapper {
@ -337,7 +321,6 @@
min-height: var(--row-height);
max-height: var(--row-height);
overflow: hidden;
--max-relationship-height: 96px;
}
.wrapper.focused {
position: absolute;
@ -349,10 +332,6 @@
max-height: none;
overflow: visible;
}
.wrapper.invertY {
top: auto;
bottom: 0;
}
.container {
min-height: var(--row-height);
@ -363,7 +342,6 @@
.focused .container {
overflow-y: auto;
border-radius: 2px;
max-height: var(--max-relationship-height);
}
.focused .container:after {
content: " ";
@ -426,10 +404,6 @@
white-space: nowrap;
text-overflow: ellipsis;
}
.editable .values .badge span:hover {
cursor: pointer;
text-decoration: underline;
}
.add {
background: var(--spectrum-global-color-gray-200);
@ -446,30 +420,9 @@
}
.dropdown {
position: absolute;
top: 100%;
left: 0;
width: 100%;
max-height: calc(
var(--max-cell-render-height) + var(--row-height) - var(--values-height)
);
background: var(--grid-background-alt);
border: var(--cell-border);
box-shadow: 0 0 20px -4px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
align-items: stretch;
padding: 0 0 8px 0;
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
.dropdown.invertY {
transform: translateY(-100%);
top: -1px;
}
.dropdown.invertX {
left: auto;
right: 0;
}
.searching {
@ -497,7 +450,8 @@
cursor: pointer;
}
.result .badge {
max-width: calc(100% - 30px);
flex: 1 1 auto;
overflow: hidden;
}
.search {
@ -505,7 +459,6 @@
display: flex;
align-items: center;
margin: 4px var(--cell-padding);
width: calc(100% - 2 * var(--cell-padding));
}
.search :global(.spectrum-Textfield) {
min-width: 0;

View file

@ -10,6 +10,7 @@
import GridBody from "./GridBody.svelte"
import ResizeOverlay from "../overlays/ResizeOverlay.svelte"
import ReorderOverlay from "../overlays/ReorderOverlay.svelte"
import PopoverOverlay from "../overlays/PopoverOverlay.svelte"
import HeaderRow from "./HeaderRow.svelte"
import ScrollOverlay from "../overlays/ScrollOverlay.svelte"
import MenuOverlay from "../overlays/MenuOverlay.svelte"
@ -22,10 +23,12 @@
import NewRow from "./NewRow.svelte"
import { createGridWebsocket } from "../lib/websocket"
import {
MaxCellRenderHeight,
MaxCellRenderWidthOverflow,
MaxCellRenderOverflow,
GutterWidth,
DefaultRowHeight,
Padding,
SmallRowHeight,
ControlsHeight,
} from "../lib/constants"
export let API = null
@ -52,7 +55,7 @@
export let buttons = null
// Unique identifier for DOM nodes inside this instance
const rand = Math.random()
const gridID = `grid-${Math.random().toString().slice(2)}`
// Store props in a store for reference in other stores
const props = writable($$props)
@ -60,7 +63,7 @@
// Build up context
let context = {
API: API || createAPIClient(),
rand,
gridID,
props,
}
context = { ...context, ...createEventManagers() }
@ -104,6 +107,8 @@
notifyError,
buttons,
})
$: minHeight =
Padding + SmallRowHeight + $rowHeight + (showControls ? ControlsHeight : 0)
// Set context for children to consume
setContext("grid", context)
@ -122,14 +127,14 @@
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="grid"
id="grid-{rand}"
id={gridID}
class:is-resizing={$isResizing}
class:is-reordering={$isReordering}
class:stripe={stripeRows}
class:quiet
on:mouseenter={() => gridFocused.set(true)}
on:mouseleave={() => gridFocused.set(false)}
style="--row-height:{$rowHeight}px; --default-row-height:{DefaultRowHeight}px; --gutter-width:{GutterWidth}px; --max-cell-render-height:{MaxCellRenderHeight}px; --max-cell-render-width-overflow:{MaxCellRenderWidthOverflow}px; --content-lines:{$contentLines};"
style="--row-height:{$rowHeight}px; --default-row-height:{DefaultRowHeight}px; --gutter-width:{GutterWidth}px; --max-cell-render-overflow:{MaxCellRenderOverflow}px; --content-lines:{$contentLines}; --min-height:{minHeight}px; --controls-height:{ControlsHeight}px;"
>
{#if showControls}
<div class="controls">
@ -181,6 +186,7 @@
<ReorderOverlay />
<ScrollOverlay />
<MenuOverlay />
<PopoverOverlay />
</div>
</div>
</div>
@ -210,7 +216,6 @@
--cell-spacing: 4px;
--cell-border: 1px solid var(--spectrum-global-color-gray-200);
--cell-font-size: 14px;
--controls-height: 50px;
flex: 1 1 auto;
display: flex;
flex-direction: column;
@ -219,6 +224,7 @@
position: relative;
overflow: hidden;
background: var(--grid-background);
min-height: var(--min-height);
}
.grid,
.grid :global(*) {

View file

@ -12,6 +12,7 @@
bounds,
hoveredRowId,
menu,
focusedCellAPI,
} = getContext("grid")
export let scrollVertically = false
@ -35,6 +36,9 @@
e.preventDefault()
updateScroll(e.deltaX, e.deltaY, e.clientY)
// Close any open popovers when scrolling
$focusedCellAPI?.blur()
// If a context menu was visible, hide it
if ($menu.visible) {
menu.actions.close()

View file

@ -1,11 +1,12 @@
<script>
import { getContext, onMount } from "svelte"
import { Icon, Popover, clickOutside } from "@budibase/bbui"
import { Icon } from "@budibase/bbui"
import GridPopover from "../overlays/GridPopover.svelte"
const { visibleColumns, scroll, width, subscribe } = getContext("grid")
const { visibleColumns, scroll, width, subscribe, ui } = getContext("grid")
let anchor
let open = false
let isOpen = false
$: columnsWidth = $visibleColumns.reduce(
(total, col) => (total += col.width),
@ -14,8 +15,13 @@
$: end = columnsWidth - 1 - $scroll.left
$: left = Math.min($width - 40, end)
const open = () => {
ui.actions.blur()
isOpen = true
}
const close = () => {
open = false
isOpen = false
}
onMount(() => subscribe("close-edit-column", close))
@ -28,27 +34,23 @@
bind:this={anchor}
class="add"
style="left:{left}px"
on:click={() => (open = true)}
on:click={open}
>
<Icon name="Add" />
</div>
<Popover
bind:open
{anchor}
align={$visibleColumns.length ? "right" : "left"}
offset={0}
popoverTarget={document.getElementById(`add-column-button`)}
customZindex={50}
>
<div
use:clickOutside={() => {
open = false
}}
class="content"
{#if isOpen}
<GridPopover
{anchor}
align={$visibleColumns.length ? "right" : "left"}
on:close={close}
maxHeight={null}
resizable
>
<slot />
</div>
</Popover>
<div class="content">
<slot />
</div>
</GridPopover>
{/if}
<style>
.add {

View file

@ -1,5 +1,4 @@
export const Padding = 246
export const MaxCellRenderHeight = 222
export const Padding = 100
export const ScrollBarSize = 8
export const GutterWidth = 72
export const DefaultColumnWidth = 200
@ -11,5 +10,11 @@ export const DefaultRowHeight = SmallRowHeight
export const NewRowID = "new"
export const BlankRowID = "blank"
export const RowPageSize = 100
export const FocusedCellMinOffset = 48
export const MaxCellRenderWidthOverflow = Padding - 3 * ScrollBarSize
export const FocusedCellMinOffset = ScrollBarSize * 3
export const ControlsHeight = 50
// Popovers
export const PopoverMinWidth = 200
export const PopoverMaxWidth = 400
export const PopoverMaxHeight = 236
export const MaxCellRenderOverflow = 222

View file

@ -0,0 +1,71 @@
<script>
import { Popover, clickOutside } from "@budibase/bbui"
import { createEventDispatcher, getContext } from "svelte"
import {
PopoverMinWidth,
PopoverMaxWidth,
PopoverMaxHeight,
} from "../lib/constants"
export let anchor
export let minWidth = PopoverMinWidth
export let maxWidth = PopoverMaxWidth
export let maxHeight = PopoverMaxHeight
export let align = "left"
export let open = true
export let resizable = false
export let wrap = true
const { gridID } = getContext("grid")
const dispatch = createEventDispatcher()
$: style = buildStyles(minWidth, maxWidth, maxHeight)
const buildStyles = (minWidth, maxWidth, maxHeight) => {
let style = ""
if (minWidth != null) {
style += `min-width: ${minWidth}px;`
}
if (maxWidth != null) {
style += `max-width: ${maxWidth}px;`
}
if (maxHeight != null) {
style += `max-height: ${maxHeight}px;`
}
return style
}
</script>
<Popover
{open}
{anchor}
{align}
{resizable}
{wrap}
portalTarget="#{gridID} .grid-popover-container"
offset={0}
>
<div
class="grid-popover-contents"
{style}
use:clickOutside={() => dispatch("close")}
on:wheel={e => e.stopPropagation()}
>
<slot />
</div>
</Popover>
<style>
:global(.grid-popover-container .spectrum-Popover) {
background: var(--grid-background);
min-width: none;
max-width: none;
overflow: hidden;
}
.grid-popover-contents {
overflow-y: auto;
overflow-x: hidden;
display: flex;
flex-direction: column;
}
</style>

View file

@ -20,6 +20,7 @@
const ignoredOriginSelectors = [
".spectrum-Modal",
".date-time-popover",
"#builder-side-panel-container",
"[data-grid-ignore]",
]

View file

@ -1,7 +1,8 @@
<script>
import { clickOutside, Menu, MenuItem, Helpers } from "@budibase/bbui"
import { Menu, MenuItem, Helpers } from "@budibase/bbui"
import { getContext } from "svelte"
import { NewRowID } from "../lib/constants"
import GridPopover from "./GridPopover.svelte"
const {
focusedRow,
@ -20,6 +21,8 @@
isDatasourcePlus,
} = getContext("grid")
let anchor
$: style = makeStyle($menu)
$: isNewRow = $focusedRowId === NewRowID
@ -48,75 +51,74 @@
}
</script>
<div bind:this={anchor} {style} class="menu-anchor" />
{#if $menu.visible}
<div class="menu" {style} use:clickOutside={() => menu.actions.close()}>
<Menu>
<MenuItem
icon="Copy"
on:click={clipboard.actions.copy}
on:click={menu.actions.close}
>
Copy
</MenuItem>
<MenuItem
icon="Paste"
disabled={$copiedCell == null || $focusedCellAPI?.isReadonly()}
on:click={clipboard.actions.paste}
on:click={menu.actions.close}
>
Paste
</MenuItem>
<MenuItem
icon="Maximize"
disabled={isNewRow || !$config.canEditRows || !$config.canExpandRows}
on:click={() => dispatch("edit-row", $focusedRow)}
on:click={menu.actions.close}
>
Edit row in modal
</MenuItem>
<MenuItem
icon="Copy"
disabled={isNewRow || !$focusedRow?._id || !$isDatasourcePlus}
on:click={() => copyToClipboard($focusedRow?._id)}
on:click={menu.actions.close}
>
Copy row _id
</MenuItem>
<MenuItem
icon="Copy"
disabled={isNewRow || !$focusedRow?._rev}
on:click={() => copyToClipboard($focusedRow?._rev)}
on:click={menu.actions.close}
>
Copy row _rev
</MenuItem>
<MenuItem
icon="Duplicate"
disabled={isNewRow || !$config.canAddRows}
on:click={duplicate}
>
Duplicate row
</MenuItem>
<MenuItem
icon="Delete"
disabled={isNewRow || !$config.canDeleteRows}
on:click={deleteRow}
>
Delete row
</MenuItem>
</Menu>
</div>
{#key style}
<GridPopover {anchor} on:close={menu.actions.close} maxHeight={null}>
<Menu>
<MenuItem
icon="Copy"
on:click={clipboard.actions.copy}
on:click={menu.actions.close}
>
Copy
</MenuItem>
<MenuItem
icon="Paste"
disabled={$copiedCell == null || $focusedCellAPI?.isReadonly()}
on:click={clipboard.actions.paste}
on:click={menu.actions.close}
>
Paste
</MenuItem>
<MenuItem
icon="Maximize"
disabled={isNewRow || !$config.canEditRows || !$config.canExpandRows}
on:click={() => dispatch("edit-row", $focusedRow)}
on:click={menu.actions.close}
>
Edit row in modal
</MenuItem>
<MenuItem
icon="Copy"
disabled={isNewRow || !$focusedRow?._id || !$isDatasourcePlus}
on:click={() => copyToClipboard($focusedRow?._id)}
on:click={menu.actions.close}
>
Copy row _id
</MenuItem>
<MenuItem
icon="Copy"
disabled={isNewRow || !$focusedRow?._rev}
on:click={() => copyToClipboard($focusedRow?._rev)}
on:click={menu.actions.close}
>
Copy row _rev
</MenuItem>
<MenuItem
icon="Duplicate"
disabled={isNewRow || !$config.canAddRows}
on:click={duplicate}
>
Duplicate row
</MenuItem>
<MenuItem
icon="Delete"
disabled={isNewRow || !$config.canDeleteRows}
on:click={deleteRow}
>
Delete row
</MenuItem>
</Menu>
</GridPopover>
{/key}
{/if}
<style>
.menu {
.menu-anchor {
opacity: 0;
pointer-events: none;
position: absolute;
background: var(--cell-background);
border: 1px solid var(--spectrum-global-color-gray-300);
width: 180px;
border-radius: 4px;
display: flex;
flex-direction: column;
box-shadow: 0 0 20px -4px rgba(0, 0, 0, 0.15);
}
</style>

View file

@ -0,0 +1,9 @@
<div class="grid-popover-container" />
<style>
.grid-popover-container {
position: fixed;
top: 0;
left: 0;
}
</style>

View file

@ -18,6 +18,7 @@
height,
isDragging,
menu,
focusedCellAPI,
} = getContext("grid")
// State for dragging bars
@ -48,10 +49,11 @@
$: barLeft = ScrollBarSize + availWidth * ($scrollLeft / $maxScrollLeft)
// Helper to close the context menu if it's open
const closeMenu = () => {
const closePopovers = () => {
if ($menu.visible) {
menu.actions.close()
}
$focusedCellAPI?.blur()
}
// V scrollbar drag handlers
@ -64,7 +66,7 @@
document.addEventListener("mouseup", stopVDragging)
document.addEventListener("touchend", stopVDragging)
isDraggingV = true
closeMenu()
closePopovers()
}
const moveVDragging = domDebounce(e => {
const delta = parseEventLocation(e).y - initialMouse
@ -93,7 +95,7 @@
document.addEventListener("mouseup", stopHDragging)
document.addEventListener("touchend", stopHDragging)
isDraggingH = true
closeMenu()
closePopovers()
}
const moveHDragging = domDebounce(e => {
const delta = parseEventLocation(e).x - initialMouse

View file

@ -13,13 +13,13 @@ export const createStores = () => {
}
export const createActions = context => {
const { menu, focusedCellId, rand } = context
const { menu, focusedCellId, gridID } = context
const open = (cellId, e) => {
e.preventDefault()
// Get DOM node for grid data wrapper to compute relative position to
const gridNode = document.getElementById(`grid-${rand}`)
const gridNode = document.getElementById(gridID)
const dataNode = gridNode?.getElementsByClassName("grid-data-outer")?.[0]
if (!dataNode) {
return

View file

@ -32,7 +32,6 @@ export const createActions = context => {
scroll,
bounds,
stickyColumn,
ui,
maxScrollLeft,
width,
} = context
@ -45,7 +44,6 @@ export const createActions = context => {
const $visibleColumns = get(visibleColumns)
const $bounds = get(bounds)
const $stickyColumn = get(stickyColumn)
ui.actions.blur()
// Generate new breakpoints for the current columns
let breakpoints = $visibleColumns.map(col => ({
@ -97,7 +95,7 @@ export const createActions = context => {
// Check if we need to start auto-scrolling
const $reorder = get(reorder)
const proximityCutoff = Math.min(140, get(width) / 6)
const speedFactor = 8
const speedFactor = 16
const rightProximity = Math.max(0, $reorder.gridLeft + $reorder.width - x)
const leftProximity = Math.max(0, x - $reorder.gridLeft)
if (rightProximity < proximityCutoff) {

View file

@ -196,6 +196,20 @@ export const createActions = context => {
// Handles validation errors from the rows API and updates local validation
// state, storing error messages against relevant cells
const handleValidationError = (rowId, error) => {
// If the server doesn't reply with a valid error, assume that the source
// of the error is the focused cell's column
if (!error?.json?.validationErrors && error?.message) {
const focusedColumn = get(focusedCellId)?.split("-")[1]
if (focusedColumn) {
error = {
json: {
validationErrors: {
[focusedColumn]: error.message,
},
},
}
}
}
if (error?.json?.validationErrors) {
// Normal validation errors
const keys = Object.keys(error.json.validationErrors)
@ -214,11 +228,19 @@ export const createActions = context => {
// Process errors for columns that we have
for (let column of erroredColumns) {
// Ensure we have a valid error to display
let err = error.json.validationErrors[column]
if (Array.isArray(err)) {
err = err[0]
}
if (typeof err !== "string" || !err.length) {
error = "Something went wrong"
}
// Set error against the cell
validation.actions.setError(
`${rowId}-${column}`,
`${column} ${error.json.validationErrors[column]}`
Helpers.capitalise(err)
)
// Ensure the column is visible
const index = $columns.findIndex(x => x.name === column)
if (index !== -1 && !$columns[index].visible) {
@ -523,6 +545,7 @@ export const initialise = context => {
previousFocusedCellId,
rows,
validation,
focusedCellId,
} = context
// Wipe the row change cache when changing row
@ -537,12 +560,22 @@ export const initialise = context => {
// Ensure any unsaved changes are saved when changing cell
previousFocusedCellId.subscribe(async id => {
const rowId = id?.split("-")[0]
const hasErrors = validation.actions.rowHasErrors(rowId)
const hasChanges = Object.keys(get(rowChangeCache)[rowId] || {}).length > 0
const isSavingChanges = get(inProgressChanges)[rowId]
if (rowId && !hasErrors && hasChanges && !isSavingChanges) {
await rows.actions.applyRowChanges(rowId)
if (!id) {
return
}
// Stop if we changed row
const oldRowId = id.split("-")[0]
const oldColumn = id.split("-")[1]
const newRowId = get(focusedCellId)?.split("-")[0]
if (oldRowId !== newRowId) {
return
}
// Otherwise we just changed cell in the same row
const hasChanges = oldColumn in (get(rowChangeCache)[oldRowId] || {})
const hasErrors = validation.actions.rowHasErrors(oldRowId)
const isSavingChanges = get(inProgressChanges)[oldRowId]
if (oldRowId && !hasErrors && hasChanges && !isSavingChanges) {
await rows.actions.applyRowChanges(oldRowId)
}
})
}

View file

@ -1,7 +1,6 @@
import { derived } from "svelte/store"
import {
MaxCellRenderHeight,
MaxCellRenderWidthOverflow,
MaxCellRenderOverflow,
MinColumnWidth,
ScrollBarSize,
} from "../lib/constants"
@ -95,11 +94,11 @@ export const deriveStores = context => {
// Compute the last row index with space to render popovers below it
const minBottom =
$height - ScrollBarSize * 3 - MaxCellRenderHeight + offset
$height - ScrollBarSize * 3 - MaxCellRenderOverflow + offset
const lastIdx = Math.floor(minBottom / $rowHeight)
// Compute the first row index with space to render popovers above it
const minTop = MaxCellRenderHeight + offset
const minTop = MaxCellRenderOverflow + offset
const firstIdx = Math.ceil(minTop / $rowHeight)
// Use the greater of the two indices so that we prefer content below,
@ -117,7 +116,7 @@ export const deriveStores = context => {
let inversionIdx = $visibleColumns.length
for (let i = $visibleColumns.length - 1; i >= 0; i--, inversionIdx--) {
const rightEdge = $visibleColumns[i].left + $visibleColumns[i].width
if (rightEdge + MaxCellRenderWidthOverflow <= cutoff) {
if (rightEdge + MaxCellRenderOverflow <= cutoff) {
break
}
}

View file

@ -65,9 +65,7 @@ export async function rawQuery(ds: Datasource, sql: string): Promise<any> {
}
export async function startContainer(container: GenericContainer) {
if (process.env.REUSE_CONTAINERS) {
container = container.withReuse()
}
container = container.withReuse().withLabels({ "com.budibase": "true" })
const startedContainer = await container.start()

View file

@ -38,7 +38,7 @@
"doctrine": "^3.0.0",
"jest": "29.7.0",
"marked": "^4.0.10",
"rollup": "^4.9.6",
"rollup": "^2.36.2",
"rollup-plugin-inject-process-env": "^1.3.1",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-globals": "^1.4.0",

19
scripts/killTestcontainers.sh Executable file
View file

@ -0,0 +1,19 @@
#!/bin/bash
# Find all Docker containers with the label "org.testcontainers=true"
containers=$(docker ps -q -f "label=org.testcontainers=true")
# Check if there are any containers to stop
if [ -z "$containers" ]; then
echo "No containers with label 'org.testcontainers=true' found."
else
# Stop the containers
echo "Stopping containers..."
docker stop $containers
# Remove the containers
echo "Removing containers..."
docker rm $containers
echo "Containers have been stopped and removed."
fi

224
yarn.lock
View file

@ -1988,7 +1988,7 @@
resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
"@babel/runtime@^7.10.5", "@babel/runtime@^7.13.10":
"@babel/runtime@^7.10.5":
version "7.23.9"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7"
integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==
@ -2002,6 +2002,13 @@
dependencies:
regenerator-runtime "^0.14.0"
"@babel/runtime@^7.13.10":
version "7.24.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd"
integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/template@^7.22.15", "@babel/template@^7.22.5", "@babel/template@^7.3.3":
version "7.22.15"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
@ -2751,7 +2758,7 @@
"@grpc/proto-loader" "^0.7.0"
"@types/node" ">=12.12.47"
"@grpc/proto-loader@0.7.10", "@grpc/proto-loader@^0.7.0", "@grpc/proto-loader@^0.7.8":
"@grpc/proto-loader@0.7.10", "@grpc/proto-loader@^0.7.0":
version "0.7.10"
resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.10.tgz#6bf26742b1b54d0a473067743da5d3189d06d720"
integrity sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==
@ -2761,6 +2768,16 @@
protobufjs "^7.2.4"
yargs "^17.7.2"
"@grpc/proto-loader@^0.7.8":
version "0.7.12"
resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.12.tgz#787b58e3e3771df30b1567c057b6ab89e3a42911"
integrity sha512-DCVwMxqYzpUCiDMl7hQ384FqP4T3DbNpXU8pt681l3UWCip1WUiD5JrkImUwCB9a7f2cq4CUTmi5r/xIMRPY1Q==
dependencies:
lodash.camelcase "^4.3.0"
long "^5.0.0"
protobufjs "^7.2.4"
yargs "^17.7.2"
"@hapi/hoek@^9.0.0":
version "9.3.0"
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb"
@ -4776,17 +4793,17 @@
resolved "https://registry.yarnpkg.com/@spectrum-css/button/-/button-3.0.1.tgz#6db8c3e851baecd0f1c2d88fef37d49d01c6e643"
integrity sha512-YXrBtjIYisk4Vaxnp0RiE4gdElQX04P2mc4Pi2GlQ27dJKlHmufYcF+kAqGdtiyK5yjdN/vKRcC8y13aA4rusA==
"@spectrum-css/button@^3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@spectrum-css/button/-/button-3.0.3.tgz#2df1efaab6c7e0b3b06cb4b59e1eae59c7f1fc84"
integrity sha512-6CnLPqqtaU/PcSSIGeGRi0iFIIxIUByYLKFO6zn5NEUc12KQ28dJ4PLwB6WBa0L8vRoAGlnWWH2ZZweTijbXgg==
"@spectrum-css/buttongroup@3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@spectrum-css/buttongroup/-/buttongroup-3.0.2.tgz#fd3387973ca3131609e32112de42a1c0400a48d8"
integrity sha512-Wu7B4GJ/SAeVHz9SUGAkeIH8pLaZh4t+w2ykSKOPQIRuK2jCBoudkEClVxviNVwqekccf5XLFXg9GpYF1a3Uaw==
"@spectrum-css/card@^3.0.3":
"@spectrum-css/calendar@3.2.7":
version "3.2.7"
resolved "https://registry.yarnpkg.com/@spectrum-css/calendar/-/calendar-3.2.7.tgz#10fd44176b6afbdf5baf29ce16728baa98b0d844"
integrity sha512-e2BGyuXzP+VOv0q855EIgrR+ne7e/EP8AMMuSAWazgq2fPZ4CoJIeLYP3tnniKnj2dlb3Gr1LH+6MPlUXS74RA==
"@spectrum-css/card@3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@spectrum-css/card/-/card-3.0.3.tgz#56b2e2da6b80c1583228baa279de7407383bfb6b"
integrity sha512-+oKLUI2a0QmQP9EzySeq/G4FpUkkdaDNbuEbqCj2IkPMc/2v/nwzsPhh1fj2UIghGAiiUwXfPpzax1e8fyhQUg==
@ -4808,13 +4825,6 @@
dependencies:
"@spectrum-css/vars" "^3.0.2"
"@spectrum-css/divider@^1.0.3":
version "1.0.27"
resolved "https://registry.yarnpkg.com/@spectrum-css/divider/-/divider-1.0.27.tgz#435bf738a65b4eb15c899edf5c536bea22f2d679"
integrity sha512-hWKPHOEo9lkOGN5zecpVVwVxE3x0SJHQJKDNx1g0xs/P/AthAboK+L1c9Rq29czNfcQ2kUjumi4igzQzcqABMQ==
dependencies:
"@spectrum-css/vars" "^8.0.0"
"@spectrum-css/dropzone@3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@spectrum-css/dropzone/-/dropzone-3.0.2.tgz#34f137851054442b219fed7f32006b93fc5e0bcf"
@ -4860,11 +4870,6 @@
resolved "https://registry.yarnpkg.com/@spectrum-css/link/-/link-3.1.1.tgz#cb526a2e10b50ef5a7ae29cca7272e2610d597eb"
integrity sha512-Bi88lRhTY7g6nM/ryW1yY4Cji211ZYNtRxkxbV7n2lPvwMAAQtyx0qVD3ru4kTGj/FFVvmPR3XiOE10K13HSNA==
"@spectrum-css/link@^3.1.3":
version "3.1.23"
resolved "https://registry.yarnpkg.com/@spectrum-css/link/-/link-3.1.23.tgz#9d9ff64c41366edbfdb19d04a5deec88bf2ea8fd"
integrity sha512-CAJQGnGTrTtR4tF1L94ou9Y+c4vnx9d5rWhb3AMzKb2Focqz02xSkTyaCCH7OM/3CwD8TCLOMANon8LcRpGAjA==
"@spectrum-css/menu@3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@spectrum-css/menu/-/menu-3.0.1.tgz#2a376f991acc24e12ec892bb6b9db2650fc41fbe"
@ -4952,11 +4957,6 @@
resolved "https://registry.yarnpkg.com/@spectrum-css/tabs/-/tabs-3.2.12.tgz#9b08f23d5aa881b3441af7757800c7173e5685ff"
integrity sha512-rPFUW9SSW4+3/UJ3UrtY2/l3sQvlqB1fqxHLPDjgykvbfrnMejcCTNV4ZrFNHXpE/6+kGnk+yVViSPtWGwJzkA==
"@spectrum-css/tag@^3.1.4":
version "3.3.15"
resolved "https://registry.yarnpkg.com/@spectrum-css/tag/-/tag-3.3.15.tgz#971184fd8cb977b85a529f808313851863123278"
integrity sha512-pF6Wh61Z7hmAy20twIlpjdDuivYj6UPtWIzK7giyJKr/qcn20BjVN2ChIeFB1N+vBamJdLsuQOewv4AJ3+LZ2Q==
"@spectrum-css/tags@3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@spectrum-css/tags/-/tags-3.0.2.tgz#5bf35fb79c97cd9344de485bd4626ad5b9f07757"
@ -4987,11 +4987,6 @@
resolved "https://registry.yarnpkg.com/@spectrum-css/typography/-/typography-3.0.1.tgz#957dafd9b18c314fa37a88b549042ba2175f5b3f"
integrity sha512-XyR68K2rIZX3u4j7HhMLOqLVHDJZcapp3XUqgYMzMWccBFleA0qPxKpfRWqVIA5DzTMSIw0wEcZPYKWFZ2e6dA==
"@spectrum-css/typography@^3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@spectrum-css/typography/-/typography-3.0.2.tgz#ea3ca0a60e18064527819d48c8c4364cab4fcd38"
integrity sha512-5ZOLmQe0edzsDMyhghUd4hBb5uxGsFrxzf+WasfcUw9klSfTsRZ09n1BsaaWbgrLjlMQ+EEHS46v5VNo0Ms2CA==
"@spectrum-css/underlay@2.0.9":
version "2.0.9"
resolved "https://registry.yarnpkg.com/@spectrum-css/underlay/-/underlay-2.0.9.tgz#fc10f971d1325cc844b727e6260f7217844060e8"
@ -5012,11 +5007,6 @@
resolved "https://registry.yarnpkg.com/@spectrum-css/vars/-/vars-4.3.1.tgz#d333fa41909f691c8750b5c15ad9ba029df2248e"
integrity sha512-rX6Iasu9BsFMVgEN0vGRPm9dmSxva+IK/uqQAa9HM0lliwqUiFrJxrFXHHpiAgNuux/U4srEJwbSpGzfF+CegQ==
"@spectrum-css/vars@^8.0.0":
version "8.0.4"
resolved "https://registry.yarnpkg.com/@spectrum-css/vars/-/vars-8.0.4.tgz#dcf115551f240b25ba629a3b6c4d3eb1429bee15"
integrity sha512-3jYj5HYxbVfkR4jLV9l+L3g6jS4R09m0lV+gupqnXWpwcThlP0EOjkCkevu195imoS4pZ/i2iLpd98l4qcTc2Q==
"@sveltejs/vite-plugin-svelte@1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.4.0.tgz#412a735de489ca731d0c780c2b410f45dd95b392"
@ -5756,9 +5746,9 @@
integrity sha512-7GgtHCs/QZrBrDzgIJnQtuSvhFSwhyYSI2uafSwZoNt1iOGhEN5fwNrQMjtONyHm9+/LoA4453jH0CMYcr06Pg==
"@types/node@>=8.1.0":
version "20.11.10"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.10.tgz#6c3de8974d65c362f82ee29db6b5adf4205462f9"
integrity sha512-rZEfe/hJSGYmdfX9tvcPMYeYPW2sNl50nsw4jZmRcaG0HIAb0WYEpsB05GOb53vjqpyE9GUhlDQ4jLSoB5q9kg==
version "20.12.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.4.tgz#af5921bd75ccdf3a3d8b3fa75bf3d3359268cd11"
integrity sha512-E+Fa9z3wSQpzgYQdYmme5X3OTuejnnTx88A6p6vkkJosR3KBz+HpE3kqNm98VE6cfLFcISx7zW7MsJkH6KwbTw==
dependencies:
undici-types "~5.26.4"
@ -8202,6 +8192,17 @@ call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.4, call-bind@^1.0.5:
get-intrinsic "^1.2.1"
set-function-length "^1.1.1"
call-bind@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==
dependencies:
es-define-property "^1.0.0"
es-errors "^1.3.0"
function-bind "^1.1.2"
get-intrinsic "^1.2.4"
set-function-length "^1.2.1"
call-me-maybe@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
@ -9660,6 +9661,15 @@ define-data-property@^1.0.1, define-data-property@^1.1.0, define-data-property@^
gopd "^1.0.1"
has-property-descriptors "^1.0.0"
define-data-property@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e"
integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==
dependencies:
es-define-property "^1.0.0"
es-errors "^1.3.0"
gopd "^1.0.1"
define-lazy-prop@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f"
@ -10138,9 +10148,9 @@ dotenv@8.6.0, dotenv@^8.2.0:
integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==
dotenv@^16.3.1:
version "16.4.1"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.1.tgz#1d9931f1d3e5d2959350d1250efab299561f7f11"
integrity sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==
version "16.4.5"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
dotenv@~10.0.0:
version "10.0.0"
@ -10509,6 +10519,18 @@ es-aggregate-error@^1.0.9:
has-property-descriptors "^1.0.0"
set-function-name "^2.0.1"
es-define-property@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845"
integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==
dependencies:
get-intrinsic "^1.2.4"
es-errors@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
es-get-iterator@^1.1.2:
version "1.1.3"
resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6"
@ -11189,9 +11211,9 @@ fast-xml-parser@4.2.5:
strnum "^1.0.5"
fast-xml-parser@^4.1.3:
version "4.3.3"
resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.3.3.tgz#aeaf5778392329f17168c40c51bcbfec8ff965be"
integrity sha512-coV/D1MhrShMvU6D0I+VAK3umz6hUaxxhL0yp/9RjfiYUfAv14rDhGQL+PLForhMdr0wq3PiV07WtkkNjJjNHg==
version "4.3.6"
resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.3.6.tgz#190f9d99097f0c8f2d3a0e681a10404afca052ff"
integrity sha512-M2SovcRxD4+vC493Uc2GZVcZaj66CCJhWurC4viynVSTvrpErCShNcDz1lAho6n9REQKvL/ll4A4/fw6Y9z8nw==
dependencies:
strnum "^1.0.5"
@ -11521,9 +11543,9 @@ formidable@^2.1.2:
qs "^6.11.0"
fp-ts@^2.5.1:
version "2.16.2"
resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-2.16.2.tgz#7faa90f6fc2e8cf84c711d2c4e606afe2be9e342"
integrity sha512-CkqAjnIKFqvo3sCyoBTqgJvF+bHrSik584S9nhTjtBESLx26cbtVMR/T9a6ApChOcSDAaM3JydDmWDUn4EEXng==
version "2.16.5"
resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-2.16.5.tgz#d79b97168aeafcf9612f18bbc017f513ecb20ac9"
integrity sha512-N8T8PwMSeTKKtkm9lkj/zSTAnPC/aJIIrQhnHxxkL0KLsRCNUPANksJOlMXxcKKCo7H1ORP3No9EMD+fP0tsdA==
fresh@^0.5.2, fresh@~0.5.2:
version "0.5.2"
@ -11590,7 +11612,7 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
fsevents@^2.3.2, fsevents@~2.3.2:
fsevents@^2.3.2, fsevents@~2.3.1, fsevents@~2.3.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
@ -11753,6 +11775,17 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@
has-symbols "^1.0.3"
hasown "^2.0.0"
get-intrinsic@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd"
integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==
dependencies:
es-errors "^1.3.0"
function-bind "^1.1.2"
has-proto "^1.0.1"
has-symbols "^1.0.3"
hasown "^2.0.0"
get-object@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/get-object/-/get-object-0.2.0.tgz#d92ff7d5190c64530cda0543dac63a3d47fe8c0c"
@ -12373,6 +12406,13 @@ has-property-descriptors@^1.0.0:
dependencies:
get-intrinsic "^1.1.1"
has-property-descriptors@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854"
integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==
dependencies:
es-define-property "^1.0.0"
has-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0"
@ -18520,9 +18560,9 @@ pprof-format@^2.0.7:
integrity sha512-1qWaGAzwMpaXJP9opRa23nPnt2Egi7RMNoNBptEE/XwHbcn4fC2b/4U4bKc5arkGkIh2ZabpF2bEb+c5GNHEKA==
preact@^10.19.3:
version "10.19.3"
resolved "https://registry.yarnpkg.com/preact/-/preact-10.19.3.tgz#7a7107ed2598a60676c943709ea3efb8aaafa899"
integrity sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==
version "10.20.1"
resolved "https://registry.yarnpkg.com/preact/-/preact-10.20.1.tgz#1bc598ab630d8612978f7533da45809a8298542b"
integrity sha512-JIFjgFg9B2qnOoGiYMVBtrcFxHqn+dNXbq76bVmcaHYJFYR4lW67AOcXgAYQQTDYXDOg/kTZrKPNCdRgJ2UJmw==
prebuild-install@^7.1.1:
version "7.1.1"
@ -18918,7 +18958,14 @@ q@^1.1.2:
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==
qs@^6.10.3, qs@^6.11.0, qs@^6.4.0:
qs@^6.10.3:
version "6.12.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.0.tgz#edd40c3b823995946a8a0b1f208669c7a200db77"
integrity sha512-trVZiI6RMOkO476zLGaBIzszOdFPnCCXHPG9kn0yuS1uz6xdVxPfZdB3vUig9pxPFDM9BRAgz/YUIVQ1/vuiUg==
dependencies:
side-channel "^1.0.6"
qs@^6.11.0, qs@^6.4.0:
version "6.11.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9"
integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==
@ -19714,6 +19761,20 @@ rollup-pluginutils@^2.3.1, rollup-pluginutils@^2.5.0, rollup-pluginutils@^2.8.1,
dependencies:
estree-walker "^0.6.1"
rollup@2.45.2:
version "2.45.2"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.45.2.tgz#8fb85917c9f35605720e92328f3ccbfba6f78b48"
integrity sha512-kRRU7wXzFHUzBIv0GfoFFIN3m9oteY4uAsKllIpQDId5cfnkWF2J130l+27dzDju0E6MScKiV0ZM5Bw8m4blYQ==
optionalDependencies:
fsevents "~2.3.1"
rollup@^2.36.2, rollup@^2.45.2:
version "2.79.1"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7"
integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==
optionalDependencies:
fsevents "~2.3.2"
rollup@^3.27.1:
version "3.29.4"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981"
@ -19986,6 +20047,18 @@ set-function-length@^1.1.1:
gopd "^1.0.1"
has-property-descriptors "^1.0.0"
set-function-length@^1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==
dependencies:
define-data-property "^1.1.4"
es-errors "^1.3.0"
function-bind "^1.1.2"
get-intrinsic "^1.2.4"
gopd "^1.0.1"
has-property-descriptors "^1.0.2"
set-function-name@^2.0.0, set-function-name@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.1.tgz#12ce38b7954310b9f61faa12701620a0c882793a"
@ -20077,6 +20150,16 @@ side-channel@^1.0.4:
get-intrinsic "^1.0.2"
object-inspect "^1.9.0"
side-channel@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2"
integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==
dependencies:
call-bind "^1.0.7"
es-errors "^1.3.0"
get-intrinsic "^1.2.4"
object-inspect "^1.13.1"
siginfo@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30"
@ -20664,7 +20747,16 @@ string-similarity@^4.0.4:
resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b"
integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -20754,7 +20846,7 @@ stringify-object@^3.2.1:
is-obj "^1.0.1"
is-regexp "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -20768,6 +20860,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
dependencies:
ansi-regex "^4.1.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2"
@ -22704,7 +22803,7 @@ worker-farm@1.7.0:
dependencies:
errno "~0.1.7"
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -22722,6 +22821,15 @@ wrap-ansi@^5.1.0:
string-width "^3.0.0"
strip-ansi "^5.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
@ -23103,9 +23211,9 @@ z-schema@^5.0.1:
commander "^9.4.1"
zeebe-node@^8.2.5:
version "8.3.1"
resolved "https://registry.yarnpkg.com/zeebe-node/-/zeebe-node-8.3.1.tgz#e100bf3708464e305305b4efa1ffde53f9786c45"
integrity sha512-68ascWO3g7g+9WwDzvfa3I9TkLKHku5auEgSINP+k5ktNfsfGW68ELDmEJA+XHZgzvGsdGILZqGRzVd5SC8aaQ==
version "8.3.2"
resolved "https://registry.yarnpkg.com/zeebe-node/-/zeebe-node-8.3.2.tgz#64d156b715f03f8637054aeedb3d3024b4a09db4"
integrity sha512-3/xbiTvhaa668JHtMEwELv5dN6HR7Qw8gzmCdjp3Brj6ekdhROVx8x/0JWKSV3Mx64ac3+eEc+9nB5+ZXcO/bg==
dependencies:
"@grpc/grpc-js" "1.9.7"
"@grpc/proto-loader" "0.7.10"