diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index fd4d8cf7c8..7d09451614 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -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 diff --git a/globalSetup.ts b/globalSetup.ts index 115796c395..dd1a7dbaa0 100644 --- a/globalSetup.ts +++ b/globalSetup.ts @@ -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) } } diff --git a/lerna.json b/lerna.json index 94631c6820..f683123ca1 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.23.12", + "version": "2.24.0", "npmClient": "yarn", "packages": [ "packages/*", diff --git a/package.json b/package.json index fcd6989b6c..98524e0ee4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/backend-core/tests/core/utilities/testContainerUtils.ts b/packages/backend-core/tests/core/utilities/testContainerUtils.ts index d0dd2c9b4d..32841e4c3a 100644 --- a/packages/backend-core/tests/core/utilities/testContainerUtils.ts +++ b/packages/backend-core/tests/core/utilities/testContainerUtils.ts @@ -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) { diff --git a/packages/bbui/package.json b/packages/bbui/package.json index a1baa2a38b..1b3510cf3b 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -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", @@ -82,7 +83,6 @@ "dayjs": "^1.10.8", "easymde": "^2.16.1", "svelte-dnd-action": "^0.9.8", - "svelte-flatpickr": "3.2.3", "svelte-portal": "^1.0.0" }, "resolutions": { diff --git a/packages/bbui/src/ActionMenu/ActionMenu.svelte b/packages/bbui/src/ActionMenu/ActionMenu.svelte index c55d1cb43d..75ddd679da 100644 --- a/packages/bbui/src/ActionMenu/ActionMenu.svelte +++ b/packages/bbui/src/ActionMenu/ActionMenu.svelte @@ -38,7 +38,15 @@
- + diff --git a/packages/bbui/src/Actions/click_outside.js b/packages/bbui/src/Actions/click_outside.js index eafca657f3..535473f395 100644 --- a/packages/bbui/src/Actions/click_outside.js +++ b/packages/bbui/src/Actions/click_outside.js @@ -1,7 +1,12 @@ const ignoredClasses = [ - ".flatpickr-calendar", - ".spectrum-Popover", ".download-js-link", + ".spectrum-Menu", + ".date-time-popover", +] +const conditionallyIgnoredClasses = [ + ".spectrum-Underlay", + ".drawer-wrapper", + ".spectrum-Popover", ] let clickHandlers = [] @@ -9,6 +14,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 +29,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 +53,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 diff --git a/packages/bbui/src/Actions/position_dropdown.js b/packages/bbui/src/Actions/position_dropdown.js index 770d1bd507..6c4fcab757 100644 --- a/packages/bbui/src/Actions/position_dropdown.js +++ b/packages/bbui/src/Actions/position_dropdown.js @@ -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) + } + } } } diff --git a/packages/bbui/src/Form/Core/DatePicker.svelte b/packages/bbui/src/Form/Core/DatePicker.svelte deleted file mode 100644 index 348488ff52..0000000000 --- a/packages/bbui/src/Form/Core/DatePicker.svelte +++ /dev/null @@ -1,268 +0,0 @@ - - -{#key redrawOptions} - -
- - -
- -
- -
-
-{/key} -{#if open} - -
-{/if} - - diff --git a/packages/bbui/src/Form/Core/DatePicker/Calendar.svelte b/packages/bbui/src/Form/Core/DatePicker/Calendar.svelte new file mode 100644 index 0000000000..bc06530044 --- /dev/null +++ b/packages/bbui/src/Form/Core/DatePicker/Calendar.svelte @@ -0,0 +1,249 @@ + + +
+
+
+
+ +
+ {#if !disabled && !readonly} + + {/if} +
+ + diff --git a/packages/bbui/src/Form/Core/DatePicker/DatePicker.svelte b/packages/bbui/src/Form/Core/DatePicker/DatePicker.svelte new file mode 100644 index 0000000000..f5189c9edd --- /dev/null +++ b/packages/bbui/src/Form/Core/DatePicker/DatePicker.svelte @@ -0,0 +1,83 @@ + + + + + + {#if isOpen} + + {/if} + diff --git a/packages/bbui/src/Form/Core/DatePicker/DatePickerPopoverContents.svelte b/packages/bbui/src/Form/Core/DatePicker/DatePickerPopoverContents.svelte new file mode 100644 index 0000000000..f1bb809b29 --- /dev/null +++ b/packages/bbui/src/Form/Core/DatePicker/DatePickerPopoverContents.svelte @@ -0,0 +1,102 @@ + + +
+ {#if showCalendar} + handleChange(e.detail)} + bind:this={calendar} + /> + {/if} + +
+ + diff --git a/packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte b/packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte new file mode 100644 index 0000000000..dc4886d28d --- /dev/null +++ b/packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte @@ -0,0 +1,54 @@ + + + + + diff --git a/packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte b/packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte new file mode 100644 index 0000000000..047e5a4f08 --- /dev/null +++ b/packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte @@ -0,0 +1,59 @@ + + +
+ + : + +
+ + diff --git a/packages/bbui/src/Form/Core/DatePicker/utils.js b/packages/bbui/src/Form/Core/DatePicker/utils.js new file mode 100644 index 0000000000..953c3eb6c0 --- /dev/null +++ b/packages/bbui/src/Form/Core/DatePicker/utils.js @@ -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 + } + } +} diff --git a/packages/bbui/src/Form/Core/DateRangePicker.svelte b/packages/bbui/src/Form/Core/DateRangePicker.svelte new file mode 100644 index 0000000000..9084942ba7 --- /dev/null +++ b/packages/bbui/src/Form/Core/DateRangePicker.svelte @@ -0,0 +1,69 @@ + + +
+ (fromDate = e.detail)} + enableTime={false} + /> +
+ +
+ (toDate = e.detail)} + enableTime={false} + /> +
+ + diff --git a/packages/bbui/src/Form/Core/Picker.svelte b/packages/bbui/src/Form/Core/Picker.svelte index f6bffbbf10..eb7a5db655 100644 --- a/packages/bbui/src/Form/Core/Picker.svelte +++ b/packages/bbui/src/Form/Core/Picker.svelte @@ -155,6 +155,7 @@ useAnchorWidth={!autoWidth} maxWidth={autoWidth ? 400 : null} customHeight={customPopoverHeight} + maxHeight={240} >
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) } @@ -40,10 +33,8 @@ {placeholder} {enableTime} {timeOnly} - {time24hr} {appendTo} {ignoreTimezones} - {range} on:change={onChange} /> diff --git a/packages/bbui/src/Form/DateRangePicker.svelte b/packages/bbui/src/Form/DateRangePicker.svelte new file mode 100644 index 0000000000..39c2acb96a --- /dev/null +++ b/packages/bbui/src/Form/DateRangePicker.svelte @@ -0,0 +1,34 @@ + + + + + diff --git a/packages/bbui/src/Layout/Page.svelte b/packages/bbui/src/Layout/Page.svelte index 2169a12459..62dd9cc909 100644 --- a/packages/bbui/src/Layout/Page.svelte +++ b/packages/bbui/src/Layout/Page.svelte @@ -7,11 +7,11 @@ export let narrower = false export let noPadding = false - let sidePanelVisble = false + let sidePanelVisible = false setContext("side-panel", { - open: () => (sidePanelVisble = true), - close: () => (sidePanelVisble = false), + open: () => (sidePanelVisible = true), + close: () => (sidePanelVisible = false), }) @@ -24,9 +24,9 @@
{ - sidePanelVisble = false + sidePanelVisible = false }} > diff --git a/packages/bbui/src/Popover/Popover.svelte b/packages/bbui/src/Popover/Popover.svelte index c51af48300..aa811afe1e 100644 --- a/packages/bbui/src/Popover/Popover.svelte +++ b/packages/bbui/src/Popover/Popover.svelte @@ -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; diff --git a/packages/bbui/src/helpers.js b/packages/bbui/src/helpers.js index abb7c9a8aa..90b447f3c1 100644 --- a/packages/bbui/src/helpers.js +++ b/packages/bbui/src/helpers.js @@ -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`) + } +} diff --git a/packages/bbui/src/index.js b/packages/bbui/src/index.js index d18c4d6910..f28e185305 100644 --- a/packages/bbui/src/index.js +++ b/packages/bbui/src/index.js @@ -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" diff --git a/packages/builder/src/components/backend/DataTable/TableDataTable.svelte b/packages/builder/src/components/backend/DataTable/TableDataTable.svelte index 6a5cd2f282..77229f3a17 100644 --- a/packages/builder/src/components/backend/DataTable/TableDataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/TableDataTable.svelte @@ -106,6 +106,5 @@ display: flex; flex-direction: column; background: var(--background); - overflow: hidden; } diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index 8a27e81cd9..643d7710f5 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -498,6 +498,7 @@ /> {/if}