Merge branch 'master' of github.com:Budibase/budibase into feature/sqs-table-cleanup
This commit is contained in:
commit
7f1500ac86
96 changed files with 2517 additions and 1222 deletions
4
.github/workflows/budibase_ci.yml
vendored
4
.github/workflows/budibase_ci.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,9 +19,6 @@ RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
|
|||
RUN ./scripts/removeWorkspaceDependencies.sh packages/server/package.json
|
||||
RUN ./scripts/removeWorkspaceDependencies.sh packages/worker/package.json
|
||||
|
||||
|
||||
# We will never want to sync pro, but the script is still required
|
||||
RUN echo '' > scripts/syncProPackage.js
|
||||
RUN jq 'del(.scripts.postinstall)' package.json > temp.json && mv temp.json package.json
|
||||
RUN ./scripts/removeWorkspaceDependencies.sh package.json
|
||||
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production --frozen-lockfile
|
||||
|
|
|
@ -32,7 +32,6 @@
|
|||
"yargs": "^17.7.2"
|
||||
},
|
||||
"scripts": {
|
||||
"preinstall": "node scripts/syncProPackage.js",
|
||||
"get-past-client-version": "node scripts/getPastClientVersion.js",
|
||||
"setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev",
|
||||
"build": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream",
|
||||
|
@ -60,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",
|
||||
|
@ -107,6 +107,7 @@
|
|||
"@budibase/shared-core": "0.0.0",
|
||||
"@budibase/string-templates": "0.0.0",
|
||||
"@budibase/types": "0.0.0",
|
||||
"@budibase/pro": "npm:@budibase/pro@latest",
|
||||
"tough-cookie": "4.1.3",
|
||||
"node-fetch": "2.6.7",
|
||||
"semver": "7.5.3",
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
249
packages/bbui/src/Form/Core/DatePicker/Calendar.svelte
Normal file
249
packages/bbui/src/Form/Core/DatePicker/Calendar.svelte
Normal 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>
|
94
packages/bbui/src/Form/Core/DatePicker/DateInput.svelte
Normal file
94
packages/bbui/src/Form/Core/DatePicker/DateInput.svelte
Normal 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>
|
83
packages/bbui/src/Form/Core/DatePicker/DatePicker.svelte
Normal file
83
packages/bbui/src/Form/Core/DatePicker/DatePicker.svelte
Normal 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>
|
|
@ -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>
|
54
packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte
Normal file
54
packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte
Normal 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>
|
59
packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte
Normal file
59
packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte
Normal 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>
|
14
packages/bbui/src/Form/Core/DatePicker/utils.js
Normal file
14
packages/bbui/src/Form/Core/DatePicker/utils.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
69
packages/bbui/src/Form/Core/DateRangePicker.svelte
Normal file
69
packages/bbui/src/Form/Core/DateRangePicker.svelte
Normal 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>
|
|
@ -155,6 +155,7 @@
|
|||
useAnchorWidth={!autoWidth}
|
||||
maxWidth={autoWidth ? 400 : null}
|
||||
customHeight={customPopoverHeight}
|
||||
maxHeight={240}
|
||||
>
|
||||
<div
|
||||
class="popover-content"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
34
packages/bbui/src/Form/DateRangePicker.svelte
Normal file
34
packages/bbui/src/Form/DateRangePicker.svelte
Normal 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>
|
|
@ -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;
|
||||
|
|
|
@ -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`)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -106,6 +106,5 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--background);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -29,7 +29,11 @@
|
|||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
||||
import { getBindings } from "components/backend/DataTable/formula"
|
||||
import JSONSchemaModal from "./JSONSchemaModal.svelte"
|
||||
import { FieldType, FieldSubtype, SourceName } from "@budibase/types"
|
||||
import {
|
||||
FieldType,
|
||||
BBReferenceFieldSubType,
|
||||
SourceName,
|
||||
} from "@budibase/types"
|
||||
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
|
||||
import { RowUtils } from "@budibase/frontend-core"
|
||||
import ServerBindingPanel from "components/common/bindings/ServerBindingPanel.svelte"
|
||||
|
@ -41,8 +45,6 @@
|
|||
const NUMBER_TYPE = FieldType.NUMBER
|
||||
const JSON_TYPE = FieldType.JSON
|
||||
const DATE_TYPE = FieldType.DATETIME
|
||||
const USER_TYPE = FieldSubtype.USER
|
||||
const USERS_TYPE = FieldSubtype.USERS
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
|
||||
|
@ -263,9 +265,9 @@
|
|||
delete saveColumn.fieldName
|
||||
}
|
||||
if (isUsersColumn(saveColumn)) {
|
||||
if (saveColumn.subtype === USER_TYPE) {
|
||||
if (saveColumn.subtype === BBReferenceFieldSubType.USER) {
|
||||
saveColumn.relationshipType = RelationshipType.ONE_TO_MANY
|
||||
} else if (saveColumn.subtype === USERS_TYPE) {
|
||||
} else if (saveColumn.subtype === BBReferenceFieldSubType.USERS) {
|
||||
saveColumn.relationshipType = RelationshipType.MANY_TO_MANY
|
||||
}
|
||||
}
|
||||
|
@ -363,19 +365,17 @@
|
|||
|
||||
function getAllowedTypes() {
|
||||
if (originalName) {
|
||||
const possibleTypes = (
|
||||
SWITCHABLE_TYPES[field.type] || [editableColumn.type]
|
||||
).map(t => t.toLowerCase())
|
||||
const possibleTypes = SWITCHABLE_TYPES[field.type] || [
|
||||
editableColumn.type,
|
||||
]
|
||||
return Object.entries(FIELDS)
|
||||
.filter(([fieldType]) =>
|
||||
possibleTypes.includes(fieldType.toLowerCase())
|
||||
)
|
||||
.filter(([_, field]) => possibleTypes.includes(field.type))
|
||||
.map(([_, fieldDefinition]) => fieldDefinition)
|
||||
}
|
||||
|
||||
const isUsers =
|
||||
editableColumn.type === FieldType.BB_REFERENCE &&
|
||||
editableColumn.subtype === FieldSubtype.USERS
|
||||
editableColumn.subtype === BBReferenceFieldSubType.USERS
|
||||
|
||||
if (!externalTable) {
|
||||
return [
|
||||
|
@ -485,7 +485,9 @@
|
|||
function isUsersColumn(column) {
|
||||
return (
|
||||
column.type === FieldType.BB_REFERENCE &&
|
||||
[FieldSubtype.USER, FieldSubtype.USERS].includes(column.subtype)
|
||||
[BBReferenceFieldSubType.USER, BBReferenceFieldSubType.USERS].includes(
|
||||
column.subtype
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -513,6 +515,7 @@
|
|||
/>
|
||||
{/if}
|
||||
<Select
|
||||
placeholder={null}
|
||||
disabled={!typeEnabled}
|
||||
bind:value={editableColumn.fieldId}
|
||||
on:change={onHandleTypeChange}
|
||||
|
@ -688,12 +691,14 @@
|
|||
>
|
||||
{:else if isUsersColumn(editableColumn) && datasource?.source !== SourceName.GOOGLE_SHEETS}
|
||||
<Toggle
|
||||
value={editableColumn.subtype === FieldSubtype.USERS}
|
||||
value={editableColumn.subtype === BBReferenceFieldSubType.USERS}
|
||||
on:change={e =>
|
||||
handleTypeChange(
|
||||
makeFieldId(
|
||||
FieldType.BB_REFERENCE,
|
||||
e.detail ? FieldSubtype.USERS : FieldSubtype.USER
|
||||
e.detail
|
||||
? BBReferenceFieldSubType.USERS
|
||||
: BBReferenceFieldSubType.USER
|
||||
)
|
||||
)}
|
||||
disabled={!isCreating}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { FieldType, FieldSubtype } from "@budibase/types"
|
||||
import { FieldType, BBReferenceFieldSubType } from "@budibase/types"
|
||||
import { Select, Toggle, Multiselect } from "@budibase/bbui"
|
||||
import { DB_TYPE_INTERNAL } from "constants/backend"
|
||||
import { API } from "api"
|
||||
|
@ -60,11 +60,11 @@
|
|||
},
|
||||
{
|
||||
label: "User",
|
||||
value: `${FieldType.BB_REFERENCE}${FieldSubtype.USER}`,
|
||||
value: `${FieldType.BB_REFERENCE}${BBReferenceFieldSubType.USER}`,
|
||||
},
|
||||
{
|
||||
label: "Users",
|
||||
value: `${FieldType.BB_REFERENCE}${FieldSubtype.USERS}`,
|
||||
value: `${FieldType.BB_REFERENCE}${BBReferenceFieldSubType.USERS}`,
|
||||
},
|
||||
]
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {
|
||||
FieldType,
|
||||
FieldSubtype,
|
||||
BBReferenceFieldSubType,
|
||||
INTERNAL_TABLE_SOURCE_ID,
|
||||
AutoFieldSubType,
|
||||
Hosting,
|
||||
|
@ -160,13 +160,13 @@ export const FIELDS = {
|
|||
USER: {
|
||||
name: "User",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: FieldSubtype.USER,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
icon: TypeIconMap[FieldType.USER],
|
||||
},
|
||||
USERS: {
|
||||
name: "Users",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: FieldSubtype.USERS,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
icon: TypeIconMap[FieldType.USERS],
|
||||
constraints: {
|
||||
type: "array",
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -3869,12 +3869,6 @@
|
|||
"key": "timeOnly",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "24-hour time",
|
||||
"key": "time24hr",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Ignore time zones",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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]: {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import RelationshipCell from "./RelationshipCell.svelte"
|
||||
import { FieldSubtype, RelationshipType } from "@budibase/types"
|
||||
import { BBReferenceFieldSubType, RelationshipType } from "@budibase/types"
|
||||
|
||||
export let api
|
||||
|
||||
|
@ -13,13 +13,16 @@
|
|||
// This is not really used, just adding some content to be able to render the relationship cell
|
||||
tableId: "external",
|
||||
relationshipType:
|
||||
subtype === FieldSubtype.USER
|
||||
subtype === BBReferenceFieldSubType.USER
|
||||
? RelationshipType.ONE_TO_MANY
|
||||
: RelationshipType.MANY_TO_MANY,
|
||||
}
|
||||
|
||||
async function searchFunction(searchParams) {
|
||||
if (subtype !== FieldSubtype.USER && subtype !== FieldSubtype.USERS) {
|
||||
if (
|
||||
subtype !== BBReferenceFieldSubType.USER &&
|
||||
subtype !== BBReferenceFieldSubType.USERS
|
||||
) {
|
||||
throw `Search for '${subtype}' not implemented`
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -43,6 +43,9 @@
|
|||
on:mouseup
|
||||
on:click
|
||||
on:contextmenu
|
||||
on:touchstart
|
||||
on:touchend
|
||||
on:touchcancel
|
||||
{style}
|
||||
>
|
||||
{#if error}
|
||||
|
|
|
@ -1,30 +1,22 @@
|
|||
<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
|
||||
export let orderable = true
|
||||
|
||||
const {
|
||||
reorder,
|
||||
isReordering,
|
||||
isResizing,
|
||||
rand,
|
||||
sort,
|
||||
visibleColumns,
|
||||
dispatch,
|
||||
|
@ -53,7 +45,6 @@
|
|||
let open = false
|
||||
let editIsOpen = false
|
||||
let timeout
|
||||
let popover
|
||||
let migrationModal
|
||||
let searchValue
|
||||
let input
|
||||
|
@ -66,6 +57,12 @@
|
|||
$: resetSearchValue(column.name)
|
||||
$: searching = searchValue != null
|
||||
$: debouncedUpdateFilter(searchValue)
|
||||
$: orderable = !column.primaryDisplay
|
||||
|
||||
const close = () => {
|
||||
open = false
|
||||
editIsOpen = false
|
||||
}
|
||||
|
||||
const getSortingLabels = type => {
|
||||
switch (type) {
|
||||
|
@ -106,22 +103,19 @@
|
|||
dispatch("edit-column", column.schema)
|
||||
}
|
||||
|
||||
const cancelEdit = () => {
|
||||
popover.hide()
|
||||
editIsOpen = false
|
||||
}
|
||||
|
||||
const onMouseDown = e => {
|
||||
if (e.button === 0 && orderable) {
|
||||
ui.actions.blur()
|
||||
if ((e.touches?.length || e.button === 0) && orderable) {
|
||||
timeout = setTimeout(() => {
|
||||
reorder.actions.startReordering(column.name, e)
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
|
||||
const onMouseUp = e => {
|
||||
if (e.button === 0 && orderable) {
|
||||
const onMouseUp = () => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
timeout = null
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -236,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}>
|
||||
|
@ -258,6 +252,9 @@
|
|||
<GridCell
|
||||
on:mousedown={onMouseDown}
|
||||
on:mouseup={onMouseUp}
|
||||
on:touchstart={onMouseDown}
|
||||
on:touchend={onMouseUp}
|
||||
on:touchcancel={onMouseUp}
|
||||
on:contextmenu={onContextMenu}
|
||||
width={column.width}
|
||||
left={column.left}
|
||||
|
@ -310,88 +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={idx === "sticky" || !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={idx === "sticky" || !$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 {
|
||||
|
@ -485,7 +482,7 @@
|
|||
}
|
||||
|
||||
.content {
|
||||
width: 300px;
|
||||
width: 360px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -81,6 +81,7 @@
|
|||
size="S"
|
||||
value={column.visible}
|
||||
on:change={e => toggleVisibility(column, e.detail)}
|
||||
disabled={column.primaryDisplay}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,11 @@
|
|||
} from "@budibase/bbui"
|
||||
import { getContext } from "svelte"
|
||||
import { ValidColumnNameRegex } from "@budibase/shared-core"
|
||||
import { FieldSubtype, FieldType, RelationshipType } from "@budibase/types"
|
||||
import {
|
||||
BBReferenceFieldSubType,
|
||||
FieldType,
|
||||
RelationshipType,
|
||||
} from "@budibase/types"
|
||||
|
||||
const { API, definition, rows } = getContext("grid")
|
||||
|
||||
|
@ -29,9 +33,9 @@
|
|||
}
|
||||
|
||||
const migrateUserColumn = async () => {
|
||||
let subtype = FieldSubtype.USERS
|
||||
let subtype = BBReferenceFieldSubType.USERS
|
||||
if (column.schema.relationshipType === RelationshipType.ONE_TO_MANY) {
|
||||
subtype = FieldSubtype.USER
|
||||
subtype = BBReferenceFieldSubType.USER
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
@ -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(*) {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
refreshing,
|
||||
config,
|
||||
filter,
|
||||
inlineFilters,
|
||||
columnRenderMap,
|
||||
} = getContext("grid")
|
||||
|
||||
|
@ -157,7 +158,11 @@
|
|||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<TempTooltip
|
||||
text="Click here to create your first row"
|
||||
condition={hasNoRows && $loaded && !$filter?.length && !$refreshing}
|
||||
condition={hasNoRows &&
|
||||
$loaded &&
|
||||
!$filter?.length &&
|
||||
!$inlineFilters?.length &&
|
||||
!$refreshing}
|
||||
type={TooltipType.Info}
|
||||
>
|
||||
{#if !visible && !selectedRowCount && $config.canAddRows}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -20,3 +20,10 @@ export const getColumnIcon = column => {
|
|||
|
||||
return result || "Text"
|
||||
}
|
||||
|
||||
export const parseEventLocation = e => {
|
||||
return {
|
||||
x: e.clientX ?? e.touches?.[0]?.clientX,
|
||||
y: e.clientY ?? e.touches?.[0]?.clientY,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
const ignoredOriginSelectors = [
|
||||
".spectrum-Modal",
|
||||
".date-time-popover",
|
||||
"#builder-side-panel-container",
|
||||
"[data-grid-ignore]",
|
||||
]
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<div class="grid-popover-container" />
|
||||
|
||||
<style>
|
||||
.grid-popover-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
|
@ -21,6 +21,7 @@
|
|||
class="resize-slider"
|
||||
class:visible={activeColumn === $stickyColumn.name}
|
||||
on:mousedown={e => resize.actions.startResizing($stickyColumn, e)}
|
||||
on:touchstart={e => resize.actions.startResizing($stickyColumn, e)}
|
||||
on:dblclick={() => resize.actions.resetSize($stickyColumn)}
|
||||
style="left:{GutterWidth + $stickyColumn.width}px;"
|
||||
>
|
||||
|
@ -32,6 +33,7 @@
|
|||
class="resize-slider"
|
||||
class:visible={activeColumn === column.name}
|
||||
on:mousedown={e => resize.actions.startResizing(column, e)}
|
||||
on:touchstart={e => resize.actions.startResizing(column, e)}
|
||||
on:dblclick={() => resize.actions.resetSize(column)}
|
||||
style={getStyle(column, offset, $scrollLeft)}
|
||||
>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { getContext } from "svelte"
|
||||
import { domDebounce } from "../../../utils/utils"
|
||||
import { DefaultRowHeight, ScrollBarSize } from "../lib/constants"
|
||||
import { parseEventLocation } from "../lib/utils"
|
||||
|
||||
const {
|
||||
scroll,
|
||||
|
@ -17,6 +18,7 @@
|
|||
height,
|
||||
isDragging,
|
||||
menu,
|
||||
focusedCellAPI,
|
||||
} = getContext("grid")
|
||||
|
||||
// State for dragging bars
|
||||
|
@ -47,33 +49,27 @@
|
|||
$: 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()
|
||||
}
|
||||
}
|
||||
|
||||
const getLocation = e => {
|
||||
return {
|
||||
y: e.touches?.[0]?.clientY ?? e.clientY,
|
||||
x: e.touches?.[0]?.clientX ?? e.clientX,
|
||||
}
|
||||
$focusedCellAPI?.blur()
|
||||
}
|
||||
|
||||
// V scrollbar drag handlers
|
||||
const startVDragging = e => {
|
||||
e.preventDefault()
|
||||
initialMouse = getLocation(e).y
|
||||
initialMouse = parseEventLocation(e).y
|
||||
initialScroll = $scrollTop
|
||||
document.addEventListener("mousemove", moveVDragging)
|
||||
document.addEventListener("touchmove", moveVDragging)
|
||||
document.addEventListener("mouseup", stopVDragging)
|
||||
document.addEventListener("touchend", stopVDragging)
|
||||
isDraggingV = true
|
||||
closeMenu()
|
||||
closePopovers()
|
||||
}
|
||||
const moveVDragging = domDebounce(e => {
|
||||
const delta = getLocation(e).y - initialMouse
|
||||
const delta = parseEventLocation(e).y - initialMouse
|
||||
const weight = delta / availHeight
|
||||
const newScrollTop = initialScroll + weight * $maxScrollTop
|
||||
scroll.update(state => ({
|
||||
|
@ -92,17 +88,17 @@
|
|||
// H scrollbar drag handlers
|
||||
const startHDragging = e => {
|
||||
e.preventDefault()
|
||||
initialMouse = getLocation(e).x
|
||||
initialMouse = parseEventLocation(e).x
|
||||
initialScroll = $scrollLeft
|
||||
document.addEventListener("mousemove", moveHDragging)
|
||||
document.addEventListener("touchmove", moveHDragging)
|
||||
document.addEventListener("mouseup", stopHDragging)
|
||||
document.addEventListener("touchend", stopHDragging)
|
||||
isDraggingH = true
|
||||
closeMenu()
|
||||
closePopovers()
|
||||
}
|
||||
const moveHDragging = domDebounce(e => {
|
||||
const delta = getLocation(e).x - initialMouse
|
||||
const delta = parseEventLocation(e).x - initialMouse
|
||||
const weight = delta / availWidth
|
||||
const newScrollLeft = initialScroll + weight * $maxScrollLeft
|
||||
scroll.update(state => ({
|
||||
|
|
|
@ -48,22 +48,28 @@ export const createStores = () => {
|
|||
export const deriveStores = context => {
|
||||
const { columns, stickyColumn } = context
|
||||
|
||||
// Derive if we have any normal columns
|
||||
const hasNonAutoColumn = derived(
|
||||
// Quick access to all columns
|
||||
const allColumns = derived(
|
||||
[columns, stickyColumn],
|
||||
([$columns, $stickyColumn]) => {
|
||||
let allCols = $columns || []
|
||||
if ($stickyColumn) {
|
||||
allCols = [...allCols, $stickyColumn]
|
||||
}
|
||||
const normalCols = allCols.filter(column => {
|
||||
return !column.schema?.autocolumn
|
||||
})
|
||||
return normalCols.length > 0
|
||||
return allCols
|
||||
}
|
||||
)
|
||||
|
||||
// Derive if we have any normal columns
|
||||
const hasNonAutoColumn = derived(allColumns, $allColumns => {
|
||||
const normalCols = $allColumns.filter(column => {
|
||||
return !column.schema?.autocolumn
|
||||
})
|
||||
return normalCols.length > 0
|
||||
})
|
||||
|
||||
return {
|
||||
allColumns,
|
||||
hasNonAutoColumn,
|
||||
}
|
||||
}
|
||||
|
@ -142,24 +148,26 @@ export const createActions = context => {
|
|||
}
|
||||
|
||||
export const initialise = context => {
|
||||
const { definition, columns, stickyColumn, enrichedSchema } = context
|
||||
const {
|
||||
definition,
|
||||
columns,
|
||||
stickyColumn,
|
||||
allColumns,
|
||||
enrichedSchema,
|
||||
compact,
|
||||
} = context
|
||||
|
||||
// Merge new schema fields with existing schema in order to preserve widths
|
||||
enrichedSchema.subscribe($enrichedSchema => {
|
||||
const processColumns = $enrichedSchema => {
|
||||
if (!$enrichedSchema) {
|
||||
columns.set([])
|
||||
stickyColumn.set(null)
|
||||
return
|
||||
}
|
||||
const $definition = get(definition)
|
||||
const $columns = get(columns)
|
||||
const $allColumns = get(allColumns)
|
||||
const $stickyColumn = get(stickyColumn)
|
||||
|
||||
// Generate array of all columns to easily find pre-existing columns
|
||||
let allColumns = $columns || []
|
||||
if ($stickyColumn) {
|
||||
allColumns.push($stickyColumn)
|
||||
}
|
||||
const $compact = get(compact)
|
||||
|
||||
// Find primary display
|
||||
let primaryDisplay
|
||||
|
@ -171,7 +179,7 @@ export const initialise = context => {
|
|||
// Get field list
|
||||
let fields = []
|
||||
Object.keys($enrichedSchema).forEach(field => {
|
||||
if (field !== primaryDisplay) {
|
||||
if ($compact || field !== primaryDisplay) {
|
||||
fields.push(field)
|
||||
}
|
||||
})
|
||||
|
@ -181,7 +189,7 @@ export const initialise = context => {
|
|||
fields
|
||||
.map(field => {
|
||||
const fieldSchema = $enrichedSchema[field]
|
||||
const oldColumn = allColumns?.find(x => x.name === field)
|
||||
const oldColumn = $allColumns?.find(x => x.name === field)
|
||||
return {
|
||||
name: field,
|
||||
label: fieldSchema.displayName || field,
|
||||
|
@ -189,9 +197,18 @@ export const initialise = context => {
|
|||
width: fieldSchema.width || oldColumn?.width || DefaultColumnWidth,
|
||||
visible: fieldSchema.visible ?? true,
|
||||
order: fieldSchema.order ?? oldColumn?.order,
|
||||
primaryDisplay: field === primaryDisplay,
|
||||
}
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// If we don't have a pinned column then primary display will be in
|
||||
// the normal columns list, and should be first
|
||||
if (a.name === primaryDisplay) {
|
||||
return -1
|
||||
} else if (b.name === primaryDisplay) {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Sort by order first
|
||||
const orderA = a.order
|
||||
const orderB = b.order
|
||||
|
@ -214,12 +231,12 @@ export const initialise = context => {
|
|||
)
|
||||
|
||||
// Update sticky column
|
||||
if (!primaryDisplay) {
|
||||
if ($compact || !primaryDisplay) {
|
||||
stickyColumn.set(null)
|
||||
return
|
||||
}
|
||||
const stickySchema = $enrichedSchema[primaryDisplay]
|
||||
const oldStickyColumn = allColumns?.find(x => x.name === primaryDisplay)
|
||||
const oldStickyColumn = $allColumns?.find(x => x.name === primaryDisplay)
|
||||
stickyColumn.set({
|
||||
name: primaryDisplay,
|
||||
label: stickySchema.displayName || primaryDisplay,
|
||||
|
@ -228,6 +245,13 @@ export const initialise = context => {
|
|||
visible: true,
|
||||
order: 0,
|
||||
left: GutterWidth,
|
||||
primaryDisplay: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Process columns when schema changes
|
||||
enrichedSchema.subscribe(processColumns)
|
||||
|
||||
// Process columns when compact flag changes
|
||||
compact.subscribe(() => processColumns(get(enrichedSchema)))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { get, writable, derived } from "svelte/store"
|
||||
import { parseEventLocation } from "../lib/utils"
|
||||
|
||||
const reorderInitialState = {
|
||||
sourceColumn: null,
|
||||
|
@ -31,8 +32,8 @@ export const createActions = context => {
|
|||
scroll,
|
||||
bounds,
|
||||
stickyColumn,
|
||||
ui,
|
||||
maxScrollLeft,
|
||||
width,
|
||||
} = context
|
||||
|
||||
let autoScrollInterval
|
||||
|
@ -43,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 => ({
|
||||
|
@ -55,6 +55,11 @@ export const createActions = context => {
|
|||
x: 0,
|
||||
column: $stickyColumn.name,
|
||||
})
|
||||
} else if (!$visibleColumns[0].primaryDisplay) {
|
||||
breakpoints.unshift({
|
||||
x: 0,
|
||||
column: null,
|
||||
})
|
||||
}
|
||||
|
||||
// Update state
|
||||
|
@ -69,6 +74,9 @@ export const createActions = context => {
|
|||
// Add listeners to handle mouse movement
|
||||
document.addEventListener("mousemove", onReorderMouseMove)
|
||||
document.addEventListener("mouseup", stopReordering)
|
||||
document.addEventListener("touchmove", onReorderMouseMove)
|
||||
document.addEventListener("touchend", stopReordering)
|
||||
document.addEventListener("touchcancel", stopReordering)
|
||||
|
||||
// Trigger a move event immediately so ensure a candidate column is chosen
|
||||
onReorderMouseMove(e)
|
||||
|
@ -77,7 +85,7 @@ export const createActions = context => {
|
|||
// Callback when moving the mouse when reordering columns
|
||||
const onReorderMouseMove = e => {
|
||||
// Immediately handle the current position
|
||||
const x = e.clientX
|
||||
const { x } = parseEventLocation(e)
|
||||
reorder.update(state => ({
|
||||
...state,
|
||||
latestX: x,
|
||||
|
@ -86,8 +94,8 @@ export const createActions = context => {
|
|||
|
||||
// Check if we need to start auto-scrolling
|
||||
const $reorder = get(reorder)
|
||||
const proximityCutoff = 140
|
||||
const speedFactor = 8
|
||||
const proximityCutoff = Math.min(140, get(width) / 6)
|
||||
const speedFactor = 16
|
||||
const rightProximity = Math.max(0, $reorder.gridLeft + $reorder.width - x)
|
||||
const leftProximity = Math.max(0, x - $reorder.gridLeft)
|
||||
if (rightProximity < proximityCutoff) {
|
||||
|
@ -158,19 +166,22 @@ export const createActions = context => {
|
|||
// Ensure auto-scrolling is stopped
|
||||
stopAutoScroll()
|
||||
|
||||
// Swap position of columns
|
||||
let { sourceColumn, targetColumn } = get(reorder)
|
||||
moveColumn(sourceColumn, targetColumn)
|
||||
|
||||
// Reset state
|
||||
reorder.set(reorderInitialState)
|
||||
|
||||
// Remove event handlers
|
||||
document.removeEventListener("mousemove", onReorderMouseMove)
|
||||
document.removeEventListener("mouseup", stopReordering)
|
||||
document.removeEventListener("touchmove", onReorderMouseMove)
|
||||
document.removeEventListener("touchend", stopReordering)
|
||||
document.removeEventListener("touchcancel", stopReordering)
|
||||
|
||||
// Save column changes
|
||||
await columns.actions.saveChanges()
|
||||
// Ensure there's actually a change
|
||||
let { sourceColumn, targetColumn } = get(reorder)
|
||||
if (sourceColumn !== targetColumn) {
|
||||
moveColumn(sourceColumn, targetColumn)
|
||||
await columns.actions.saveChanges()
|
||||
}
|
||||
|
||||
// Reset state
|
||||
reorder.set(reorderInitialState)
|
||||
}
|
||||
|
||||
// Moves a column after another columns.
|
||||
|
@ -185,8 +196,7 @@ export const createActions = context => {
|
|||
if (--targetIdx < sourceIdx) {
|
||||
targetIdx++
|
||||
}
|
||||
state.splice(targetIdx, 0, removed[0])
|
||||
return state.slice()
|
||||
return state.toSpliced(targetIdx, 0, removed[0])
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { writable, get, derived } from "svelte/store"
|
||||
import { MinColumnWidth, DefaultColumnWidth } from "../lib/constants"
|
||||
import { parseEventLocation } from "../lib/utils"
|
||||
|
||||
const initialState = {
|
||||
initialMouseX: null,
|
||||
|
@ -24,8 +25,11 @@ export const createActions = context => {
|
|||
|
||||
// Starts resizing a certain column
|
||||
const startResizing = (column, e) => {
|
||||
const { x } = parseEventLocation(e)
|
||||
|
||||
// Prevent propagation to stop reordering triggering
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
ui.actions.blur()
|
||||
|
||||
// Find and cache index
|
||||
|
@ -39,7 +43,7 @@ export const createActions = context => {
|
|||
width: column.width,
|
||||
left: column.left,
|
||||
initialWidth: column.width,
|
||||
initialMouseX: e.clientX,
|
||||
initialMouseX: x,
|
||||
column: column.name,
|
||||
columnIdx,
|
||||
})
|
||||
|
@ -47,12 +51,16 @@ export const createActions = context => {
|
|||
// Add mouse event listeners to handle resizing
|
||||
document.addEventListener("mousemove", onResizeMouseMove)
|
||||
document.addEventListener("mouseup", stopResizing)
|
||||
document.addEventListener("touchmove", onResizeMouseMove)
|
||||
document.addEventListener("touchend", stopResizing)
|
||||
document.addEventListener("touchcancel", stopResizing)
|
||||
}
|
||||
|
||||
// Handler for moving the mouse to resize columns
|
||||
const onResizeMouseMove = e => {
|
||||
const { initialMouseX, initialWidth, width, columnIdx } = get(resize)
|
||||
const dx = e.clientX - initialMouseX
|
||||
const { x } = parseEventLocation(e)
|
||||
const dx = x - initialMouseX
|
||||
const newWidth = Math.round(Math.max(MinColumnWidth, initialWidth + dx))
|
||||
|
||||
// Ignore small changes
|
||||
|
@ -87,6 +95,9 @@ export const createActions = context => {
|
|||
resize.set(initialState)
|
||||
document.removeEventListener("mousemove", onResizeMouseMove)
|
||||
document.removeEventListener("mouseup", stopResizing)
|
||||
document.removeEventListener("touchmove", onResizeMouseMove)
|
||||
document.removeEventListener("touchend", stopResizing)
|
||||
document.removeEventListener("touchcancel", stopResizing)
|
||||
|
||||
// Persist width if it changed
|
||||
if ($resize.width !== $resize.initialWidth) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -98,7 +98,7 @@ export const deriveStores = context => {
|
|||
|
||||
// Derive whether we should use the compact UI, depending on width
|
||||
const compact = derived([stickyColumn, width], ([$stickyColumn, $width]) => {
|
||||
return ($stickyColumn?.width || 0) + $width + GutterWidth < 1100
|
||||
return ($stickyColumn?.width || 0) + $width + GutterWidth < 800
|
||||
})
|
||||
|
||||
return {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
export { OperatorOptions, SqlNumberTypeRangeMap } from "@budibase/shared-core"
|
||||
export { Feature as Features } from "@budibase/types"
|
||||
import { BpmCorrelationKey } from "@budibase/shared-core"
|
||||
import { FieldType, FieldTypeSubtypes } from "@budibase/types"
|
||||
import { FieldType, BBReferenceFieldSubType } from "@budibase/types"
|
||||
|
||||
// Cookie names
|
||||
export const Cookies = {
|
||||
|
@ -134,7 +134,7 @@ export const TypeIconMap = {
|
|||
[FieldType.USER]: "User",
|
||||
[FieldType.USERS]: "UserGroup",
|
||||
[FieldType.BB_REFERENCE]: {
|
||||
[FieldTypeSubtypes.BB_REFERENCE.USER]: "User",
|
||||
[FieldTypeSubtypes.BB_REFERENCE.USERS]: "UserGroup",
|
||||
[BBReferenceFieldSubType.USER]: "User",
|
||||
[BBReferenceFieldSubType.USERS]: "UserGroup",
|
||||
},
|
||||
}
|
||||
|
|
|
@ -8,6 +8,10 @@ const isBetterSample = (newValue, oldValue) => {
|
|||
return true
|
||||
}
|
||||
|
||||
if (oldValue != null && newValue == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Don't change type
|
||||
const oldType = typeof oldValue
|
||||
const newType = typeof newValue
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit dff7b5a9dd1fd770f8a48fb8e6df1740be605f18
|
||||
Subproject commit 479879246aac5dd3073cc695945c62c41fae5b0e
|
|
@ -98,7 +98,7 @@
|
|||
"memorystream": "0.3.1",
|
||||
"mongodb": "^6.3.0",
|
||||
"mssql": "10.0.1",
|
||||
"mysql2": "3.5.2",
|
||||
"mysql2": "3.9.7",
|
||||
"node-fetch": "2.6.7",
|
||||
"object-sizeof": "2.6.1",
|
||||
"open": "8.4.0",
|
||||
|
|
|
@ -9,7 +9,7 @@ import { mocks } from "@budibase/backend-core/tests"
|
|||
import {
|
||||
Datasource,
|
||||
FieldSchema,
|
||||
FieldSubtype,
|
||||
BBReferenceFieldSubType,
|
||||
FieldType,
|
||||
QueryPreview,
|
||||
RelationshipType,
|
||||
|
@ -337,7 +337,7 @@ describe("/datasources", () => {
|
|||
[FieldType.BB_REFERENCE]: {
|
||||
name: "bb_reference",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: FieldSubtype.USERS,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
DeleteRow,
|
||||
FieldSchema,
|
||||
FieldType,
|
||||
FieldTypeSubtypes,
|
||||
BBReferenceFieldSubType,
|
||||
FormulaType,
|
||||
INTERNAL_TABLE_SOURCE_ID,
|
||||
NumberFieldMetadata,
|
||||
|
@ -1015,12 +1015,12 @@ describe.each([
|
|||
user: {
|
||||
name: "user",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: FieldTypeSubtypes.BB_REFERENCE.USER,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
},
|
||||
users: {
|
||||
name: "users",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: FieldTypeSubtypes.BB_REFERENCE.USERS,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
},
|
||||
}),
|
||||
() => config.createUser(),
|
||||
|
|
|
@ -3,6 +3,7 @@ import { DatabaseName, getDatasource } from "../../../integrations/tests/utils"
|
|||
|
||||
import * as setup from "./utilities"
|
||||
import {
|
||||
AutoFieldSubType,
|
||||
Datasource,
|
||||
EmptyFilterOption,
|
||||
FieldType,
|
||||
|
@ -18,15 +19,16 @@ import _ from "lodash"
|
|||
jest.unmock("mssql")
|
||||
|
||||
describe.each([
|
||||
["internal", undefined],
|
||||
["internal-sqs", undefined],
|
||||
["lucene", undefined],
|
||||
["sqs", undefined],
|
||||
[DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)],
|
||||
[DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)],
|
||||
[DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)],
|
||||
[DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)],
|
||||
])("/api/:sourceId/search (%s)", (name, dsProvider) => {
|
||||
const isSqs = name === "internal-sqs"
|
||||
const isInternal = name === "internal"
|
||||
const isSqs = name === "sqs"
|
||||
const isLucene = name === "lucene"
|
||||
const isInternal = isSqs || isLucene
|
||||
const config = setup.getConfig()
|
||||
|
||||
let envCleanup: (() => void) | undefined
|
||||
|
@ -59,7 +61,9 @@ describe.each([
|
|||
}
|
||||
|
||||
async function createRows(rows: Record<string, any>[]) {
|
||||
await Promise.all(rows.map(r => config.api.row.save(table._id!, r)))
|
||||
for (const row of rows) {
|
||||
await config.api.row.save(table._id!, row)
|
||||
}
|
||||
}
|
||||
|
||||
class SearchAssertion {
|
||||
|
@ -339,14 +343,14 @@ describe.each([
|
|||
}).toFindNothing())
|
||||
|
||||
// We never implemented half-open ranges in Lucene.
|
||||
!isInternal &&
|
||||
!isLucene &&
|
||||
it("can search using just a low value", () =>
|
||||
expectQuery({
|
||||
range: { age: { low: 5 } },
|
||||
}).toContainExactly([{ age: 10 }]))
|
||||
|
||||
// We never implemented half-open ranges in Lucene.
|
||||
!isInternal &&
|
||||
!isLucene &&
|
||||
it("can search using just a high value", () =>
|
||||
expectQuery({
|
||||
range: { age: { high: 5 } },
|
||||
|
@ -457,14 +461,14 @@ describe.each([
|
|||
}).toFindNothing())
|
||||
|
||||
// We never implemented half-open ranges in Lucene.
|
||||
!isInternal &&
|
||||
!isLucene &&
|
||||
it("can search using just a low value", () =>
|
||||
expectQuery({
|
||||
range: { dob: { low: JAN_5TH } },
|
||||
}).toContainExactly([{ dob: JAN_10TH }]))
|
||||
|
||||
// We never implemented half-open ranges in Lucene.
|
||||
!isInternal &&
|
||||
!isLucene &&
|
||||
it("can search using just a high value", () =>
|
||||
expectQuery({
|
||||
range: { dob: { high: JAN_5TH } },
|
||||
|
@ -642,7 +646,7 @@ describe.each([
|
|||
// Range searches against bigints don't seem to work at all in Lucene, and I
|
||||
// couldn't figure out why. Given that we're replacing Lucene with SQS,
|
||||
// we've decided not to spend time on it.
|
||||
!isInternal &&
|
||||
!isLucene &&
|
||||
describe("range", () => {
|
||||
it("successfully finds a row", () =>
|
||||
expectQuery({
|
||||
|
@ -675,4 +679,137 @@ describe.each([
|
|||
}).toContainExactly([{ num: SMALL }, { num: MEDIUM }]))
|
||||
})
|
||||
})
|
||||
|
||||
isInternal &&
|
||||
describe("auto", () => {
|
||||
beforeAll(async () => {
|
||||
await createTable({
|
||||
auto: {
|
||||
name: "auto",
|
||||
type: FieldType.AUTO,
|
||||
autocolumn: true,
|
||||
subtype: AutoFieldSubType.AUTO_ID,
|
||||
},
|
||||
})
|
||||
await createRows(new Array(10).fill({}))
|
||||
})
|
||||
|
||||
describe("equal", () => {
|
||||
it("successfully finds a row", () =>
|
||||
expectQuery({ equal: { auto: 1 } }).toContainExactly([{ auto: 1 }]))
|
||||
|
||||
it("fails to find nonexistent row", () =>
|
||||
expectQuery({ equal: { auto: 0 } }).toFindNothing())
|
||||
})
|
||||
|
||||
describe("not equal", () => {
|
||||
it("successfully finds a row", () =>
|
||||
expectQuery({ notEqual: { auto: 1 } }).toContainExactly([
|
||||
{ auto: 2 },
|
||||
{ auto: 3 },
|
||||
{ auto: 4 },
|
||||
{ auto: 5 },
|
||||
{ auto: 6 },
|
||||
{ auto: 7 },
|
||||
{ auto: 8 },
|
||||
{ auto: 9 },
|
||||
{ auto: 10 },
|
||||
]))
|
||||
|
||||
it("fails to find nonexistent row", () =>
|
||||
expectQuery({ notEqual: { auto: 0 } }).toContainExactly([
|
||||
{ auto: 1 },
|
||||
{ auto: 2 },
|
||||
{ auto: 3 },
|
||||
{ auto: 4 },
|
||||
{ auto: 5 },
|
||||
{ auto: 6 },
|
||||
{ auto: 7 },
|
||||
{ auto: 8 },
|
||||
{ auto: 9 },
|
||||
{ auto: 10 },
|
||||
]))
|
||||
})
|
||||
|
||||
describe("oneOf", () => {
|
||||
it("successfully finds a row", () =>
|
||||
expectQuery({ oneOf: { auto: [1] } }).toContainExactly([{ auto: 1 }]))
|
||||
|
||||
it("fails to find nonexistent row", () =>
|
||||
expectQuery({ oneOf: { auto: [0] } }).toFindNothing())
|
||||
})
|
||||
|
||||
describe("range", () => {
|
||||
it("successfully finds a row", () =>
|
||||
expectQuery({
|
||||
range: { auto: { low: 1, high: 1 } },
|
||||
}).toContainExactly([{ auto: 1 }]))
|
||||
|
||||
it("successfully finds multiple rows", () =>
|
||||
expectQuery({
|
||||
range: { auto: { low: 1, high: 2 } },
|
||||
}).toContainExactly([{ auto: 1 }, { auto: 2 }]))
|
||||
|
||||
it("successfully finds a row with a high bound", () =>
|
||||
expectQuery({
|
||||
range: { auto: { low: 2, high: 2 } },
|
||||
}).toContainExactly([{ auto: 2 }]))
|
||||
|
||||
it("successfully finds no rows", () =>
|
||||
expectQuery({
|
||||
range: { auto: { low: 0, high: 0 } },
|
||||
}).toFindNothing())
|
||||
|
||||
isSqs &&
|
||||
it("can search using just a low value", () =>
|
||||
expectQuery({
|
||||
range: { auto: { low: 9 } },
|
||||
}).toContainExactly([{ auto: 9 }, { auto: 10 }]))
|
||||
|
||||
isSqs &&
|
||||
it("can search using just a high value", () =>
|
||||
expectQuery({
|
||||
range: { auto: { high: 2 } },
|
||||
}).toContainExactly([{ auto: 1 }, { auto: 2 }]))
|
||||
})
|
||||
|
||||
isSqs &&
|
||||
describe("sort", () => {
|
||||
it("sorts ascending", () =>
|
||||
expectSearch({
|
||||
query: {},
|
||||
sort: "auto",
|
||||
sortOrder: SortOrder.ASCENDING,
|
||||
}).toMatchExactly([
|
||||
{ auto: 1 },
|
||||
{ auto: 2 },
|
||||
{ auto: 3 },
|
||||
{ auto: 4 },
|
||||
{ auto: 5 },
|
||||
{ auto: 6 },
|
||||
{ auto: 7 },
|
||||
{ auto: 8 },
|
||||
{ auto: 9 },
|
||||
{ auto: 10 },
|
||||
]))
|
||||
|
||||
it("sorts descending", () =>
|
||||
expectSearch({
|
||||
query: {},
|
||||
sort: "auto",
|
||||
sortOrder: SortOrder.DESCENDING,
|
||||
}).toMatchExactly([
|
||||
{ auto: 10 },
|
||||
{ auto: 9 },
|
||||
{ auto: 8 },
|
||||
{ auto: 7 },
|
||||
{ auto: 6 },
|
||||
{ auto: 5 },
|
||||
{ auto: 4 },
|
||||
{ auto: 3 },
|
||||
{ auto: 2 },
|
||||
{ auto: 1 },
|
||||
]))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -2,7 +2,7 @@ import { context, events } from "@budibase/backend-core"
|
|||
import {
|
||||
AutoFieldSubType,
|
||||
Datasource,
|
||||
FieldSubtype,
|
||||
BBReferenceFieldSubType,
|
||||
FieldType,
|
||||
INTERNAL_TABLE_SOURCE_ID,
|
||||
InternalTable,
|
||||
|
@ -497,7 +497,7 @@ describe.each([
|
|||
newColumn: {
|
||||
name: "user column",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: FieldSubtype.USER,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -562,7 +562,7 @@ describe.each([
|
|||
newColumn: {
|
||||
name: "user column",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: FieldSubtype.USERS,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -614,7 +614,7 @@ describe.each([
|
|||
newColumn: {
|
||||
name: "user column",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: FieldSubtype.USERS,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -669,7 +669,7 @@ describe.each([
|
|||
newColumn: {
|
||||
name: "user column",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: FieldSubtype.USERS,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -728,7 +728,7 @@ describe.each([
|
|||
newColumn: {
|
||||
name: "",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: FieldSubtype.USERS,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
|
@ -743,7 +743,7 @@ describe.each([
|
|||
newColumn: {
|
||||
name: "_id",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: FieldSubtype.USERS,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
|
@ -758,7 +758,7 @@ describe.each([
|
|||
newColumn: {
|
||||
name: "num",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: FieldSubtype.USERS,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
|
@ -772,12 +772,12 @@ describe.each([
|
|||
oldColumn: {
|
||||
name: "not a column",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: FieldSubtype.USERS,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
},
|
||||
newColumn: {
|
||||
name: "new column",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: FieldSubtype.USERS,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
|
|
|
@ -12,7 +12,7 @@ import SqlTableQueryBuilder from "./sqlTable"
|
|||
import {
|
||||
BBReferenceFieldMetadata,
|
||||
FieldSchema,
|
||||
FieldSubtype,
|
||||
BBReferenceFieldSubType,
|
||||
FieldType,
|
||||
JsonFieldMetadata,
|
||||
Operation,
|
||||
|
@ -767,7 +767,7 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
|
|||
return (
|
||||
field.type === FieldType.JSON ||
|
||||
(field.type === FieldType.BB_REFERENCE &&
|
||||
field.subtype === FieldSubtype.USERS)
|
||||
field.subtype === BBReferenceFieldSubType.USERS)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Knex, knex } from "knex"
|
||||
import {
|
||||
FieldSubtype,
|
||||
BBReferenceFieldSubType,
|
||||
FieldType,
|
||||
NumberFieldMetadata,
|
||||
Operation,
|
||||
|
@ -64,10 +64,10 @@ function generateSchema(
|
|||
case FieldType.BB_REFERENCE: {
|
||||
const subtype = column.subtype
|
||||
switch (subtype) {
|
||||
case FieldSubtype.USER:
|
||||
case BBReferenceFieldSubType.USER:
|
||||
schema.text(key)
|
||||
break
|
||||
case FieldSubtype.USERS:
|
||||
case BBReferenceFieldSubType.USERS:
|
||||
schema.json(key)
|
||||
break
|
||||
default:
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { searchInputMapping } from "../utils"
|
|||
import { db as dbCore } from "@budibase/backend-core"
|
||||
import {
|
||||
FieldType,
|
||||
FieldTypeSubtypes,
|
||||
BBReferenceFieldSubType,
|
||||
INTERNAL_TABLE_SOURCE_ID,
|
||||
RowSearchParams,
|
||||
Table,
|
||||
|
@ -20,7 +20,7 @@ const tableWithUserCol: Table = {
|
|||
user: {
|
||||
name: "user",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: FieldTypeSubtypes.BB_REFERENCE.USER,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ const tableWithUsersCol: Table = {
|
|||
user: {
|
||||
name: "user",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: FieldTypeSubtypes.BB_REFERENCE.USERS,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import {
|
|||
Table,
|
||||
DocumentType,
|
||||
SEPARATOR,
|
||||
FieldSubtype,
|
||||
BBReferenceFieldSubType,
|
||||
SearchFilters,
|
||||
SearchIndex,
|
||||
SearchResponse,
|
||||
|
@ -89,8 +89,8 @@ export function searchInputMapping(table: Table, options: RowSearchParams) {
|
|||
case FieldType.BB_REFERENCE: {
|
||||
const subtype = column.subtype
|
||||
switch (subtype) {
|
||||
case FieldSubtype.USER:
|
||||
case FieldSubtype.USERS:
|
||||
case BBReferenceFieldSubType.USER:
|
||||
case BBReferenceFieldSubType.USERS:
|
||||
userColumnMapping(key, options)
|
||||
break
|
||||
default:
|
||||
|
|
|
@ -33,7 +33,7 @@ const FieldTypeMap: Record<FieldType, SQLiteType> = {
|
|||
[FieldType.LONGFORM]: SQLiteType.TEXT,
|
||||
[FieldType.NUMBER]: SQLiteType.REAL,
|
||||
[FieldType.STRING]: SQLiteType.TEXT,
|
||||
[FieldType.AUTO]: SQLiteType.TEXT,
|
||||
[FieldType.AUTO]: SQLiteType.REAL,
|
||||
[FieldType.OPTIONS]: SQLiteType.TEXT,
|
||||
[FieldType.JSON]: SQLiteType.BLOB,
|
||||
[FieldType.INTERNAL]: SQLiteType.BLOB,
|
||||
|
|
|
@ -2,7 +2,7 @@ import { BadRequestError, context, db as dbCore } from "@budibase/backend-core"
|
|||
import {
|
||||
BBReferenceFieldMetadata,
|
||||
FieldSchema,
|
||||
FieldSubtype,
|
||||
BBReferenceFieldSubType,
|
||||
InternalTable,
|
||||
isBBReferenceField,
|
||||
isRelationshipField,
|
||||
|
@ -96,7 +96,7 @@ function getColumnMigrator(
|
|||
}
|
||||
|
||||
if (oldColumn.relationshipType === RelationshipType.ONE_TO_MANY) {
|
||||
if (newColumn.subtype !== FieldSubtype.USER) {
|
||||
if (newColumn.subtype !== BBReferenceFieldSubType.USER) {
|
||||
throw new BadRequestError(
|
||||
`Column "${oldColumn.name}" is a one-to-many column but "${newColumn.name}" is not a single user column`
|
||||
)
|
||||
|
@ -107,7 +107,7 @@ function getColumnMigrator(
|
|||
oldColumn.relationshipType === RelationshipType.MANY_TO_MANY ||
|
||||
oldColumn.relationshipType === RelationshipType.MANY_TO_ONE
|
||||
) {
|
||||
if (newColumn.subtype !== FieldSubtype.USERS) {
|
||||
if (newColumn.subtype !== BBReferenceFieldSubType.USERS) {
|
||||
throw new BadRequestError(
|
||||
`Column "${oldColumn.name}" is a ${oldColumn.relationshipType} column but "${newColumn.name}" is not a multi user column`
|
||||
)
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
import { cache, db as dbCore } from "@budibase/backend-core"
|
||||
import { utils } from "@budibase/shared-core"
|
||||
import { FieldSubtype, DocumentType, SEPARATOR } from "@budibase/types"
|
||||
import {
|
||||
BBReferenceFieldSubType,
|
||||
DocumentType,
|
||||
SEPARATOR,
|
||||
} from "@budibase/types"
|
||||
import { InvalidBBRefError } from "./errors"
|
||||
|
||||
const ROW_PREFIX = DocumentType.ROW + SEPARATOR
|
||||
|
||||
export async function processInputBBReferences(
|
||||
value: string | string[] | { _id: string } | { _id: string }[],
|
||||
subtype: FieldSubtype.USER | FieldSubtype.USERS
|
||||
subtype: BBReferenceFieldSubType.USER | BBReferenceFieldSubType.USERS
|
||||
): Promise<string | string[] | null> {
|
||||
let referenceIds: string[] = []
|
||||
|
||||
|
@ -40,15 +44,18 @@ export async function processInputBBReferences(
|
|||
})
|
||||
|
||||
switch (subtype) {
|
||||
case FieldSubtype.USER:
|
||||
case FieldSubtype.USERS: {
|
||||
case BBReferenceFieldSubType.USER:
|
||||
case BBReferenceFieldSubType.USERS: {
|
||||
const { notFoundIds } = await cache.user.getUsers(referenceIds)
|
||||
|
||||
if (notFoundIds?.length) {
|
||||
throw new InvalidBBRefError(notFoundIds[0], FieldSubtype.USER)
|
||||
throw new InvalidBBRefError(
|
||||
notFoundIds[0],
|
||||
BBReferenceFieldSubType.USER
|
||||
)
|
||||
}
|
||||
|
||||
if (subtype === FieldSubtype.USERS) {
|
||||
if (subtype === BBReferenceFieldSubType.USERS) {
|
||||
return referenceIds
|
||||
}
|
||||
|
||||
|
@ -61,7 +68,7 @@ export async function processInputBBReferences(
|
|||
|
||||
export async function processOutputBBReferences(
|
||||
value: string | string[],
|
||||
subtype: FieldSubtype.USER | FieldSubtype.USERS
|
||||
subtype: BBReferenceFieldSubType.USER | BBReferenceFieldSubType.USERS
|
||||
) {
|
||||
if (value === null || value === undefined) {
|
||||
// Already processed or nothing to process
|
||||
|
@ -72,8 +79,8 @@ export async function processOutputBBReferences(
|
|||
typeof value === "string" ? value.split(",").filter(id => !!id) : value
|
||||
|
||||
switch (subtype) {
|
||||
case FieldSubtype.USER:
|
||||
case FieldSubtype.USERS: {
|
||||
case BBReferenceFieldSubType.USER:
|
||||
case BBReferenceFieldSubType.USERS: {
|
||||
const { users } = await cache.user.getUsers(ids)
|
||||
if (!users.length) {
|
||||
return undefined
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { FieldSubtype } from "@budibase/types"
|
||||
import { BBReferenceFieldSubType } from "@budibase/types"
|
||||
|
||||
export class InvalidBBRefError extends Error {
|
||||
constructor(id: string, subtype: FieldSubtype) {
|
||||
constructor(id: string, subtype: BBReferenceFieldSubType) {
|
||||
super(`Id "${id}" is not valid for the subtype "${subtype}"`)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import _ from "lodash"
|
||||
import * as backendCore from "@budibase/backend-core"
|
||||
import { FieldSubtype, User } from "@budibase/types"
|
||||
import { BBReferenceFieldSubType, User } from "@budibase/types"
|
||||
import {
|
||||
processInputBBReferences,
|
||||
processOutputBBReferences,
|
||||
|
@ -63,7 +63,7 @@ describe("bbReferenceProcessor", () => {
|
|||
const userId = user!._id!
|
||||
|
||||
const result = await config.doInTenant(() =>
|
||||
processInputBBReferences(userId, FieldSubtype.USER)
|
||||
processInputBBReferences(userId, BBReferenceFieldSubType.USER)
|
||||
)
|
||||
|
||||
expect(result).toEqual(userId)
|
||||
|
@ -76,9 +76,11 @@ describe("bbReferenceProcessor", () => {
|
|||
|
||||
await expect(
|
||||
config.doInTenant(() =>
|
||||
processInputBBReferences(userId, FieldSubtype.USER)
|
||||
processInputBBReferences(userId, BBReferenceFieldSubType.USER)
|
||||
)
|
||||
).rejects.toThrow(new InvalidBBRefError(userId, FieldSubtype.USER))
|
||||
).rejects.toThrow(
|
||||
new InvalidBBRefError(userId, BBReferenceFieldSubType.USER)
|
||||
)
|
||||
expect(cacheGetUsersSpy).toHaveBeenCalledTimes(1)
|
||||
expect(cacheGetUsersSpy).toHaveBeenCalledWith([userId])
|
||||
})
|
||||
|
@ -88,7 +90,7 @@ describe("bbReferenceProcessor", () => {
|
|||
|
||||
const userIdCsv = userIds.join(" , ")
|
||||
const result = await config.doInTenant(() =>
|
||||
processInputBBReferences(userIdCsv, FieldSubtype.USER)
|
||||
processInputBBReferences(userIdCsv, BBReferenceFieldSubType.USER)
|
||||
)
|
||||
|
||||
expect(result).toEqual(userIds.join(","))
|
||||
|
@ -108,16 +110,21 @@ describe("bbReferenceProcessor", () => {
|
|||
|
||||
await expect(
|
||||
config.doInTenant(() =>
|
||||
processInputBBReferences(userIdCsv, FieldSubtype.USER)
|
||||
processInputBBReferences(userIdCsv, BBReferenceFieldSubType.USER)
|
||||
)
|
||||
).rejects.toThrow(new InvalidBBRefError(wrongId, FieldSubtype.USER))
|
||||
).rejects.toThrow(
|
||||
new InvalidBBRefError(wrongId, BBReferenceFieldSubType.USER)
|
||||
)
|
||||
})
|
||||
|
||||
it("validate valid user object", async () => {
|
||||
const userId = _.sample(users)!._id!
|
||||
|
||||
const result = await config.doInTenant(() =>
|
||||
processInputBBReferences({ _id: userId }, FieldSubtype.USER)
|
||||
processInputBBReferences(
|
||||
{ _id: userId },
|
||||
BBReferenceFieldSubType.USER
|
||||
)
|
||||
)
|
||||
|
||||
expect(result).toEqual(userId)
|
||||
|
@ -129,7 +136,7 @@ describe("bbReferenceProcessor", () => {
|
|||
const userIds = _.sampleSize(users, 3).map(x => x._id!)
|
||||
|
||||
const result = await config.doInTenant(() =>
|
||||
processInputBBReferences(userIds, FieldSubtype.USER)
|
||||
processInputBBReferences(userIds, BBReferenceFieldSubType.USER)
|
||||
)
|
||||
|
||||
expect(result).toEqual(userIds.join(","))
|
||||
|
@ -139,7 +146,7 @@ describe("bbReferenceProcessor", () => {
|
|||
|
||||
it("empty strings will return null", async () => {
|
||||
const result = await config.doInTenant(() =>
|
||||
processInputBBReferences("", FieldSubtype.USER)
|
||||
processInputBBReferences("", BBReferenceFieldSubType.USER)
|
||||
)
|
||||
|
||||
expect(result).toEqual(null)
|
||||
|
@ -147,7 +154,7 @@ describe("bbReferenceProcessor", () => {
|
|||
|
||||
it("empty arrays will return null", async () => {
|
||||
const result = await config.doInTenant(() =>
|
||||
processInputBBReferences([], FieldSubtype.USER)
|
||||
processInputBBReferences([], BBReferenceFieldSubType.USER)
|
||||
)
|
||||
|
||||
expect(result).toEqual(null)
|
||||
|
@ -157,7 +164,7 @@ describe("bbReferenceProcessor", () => {
|
|||
const userId = _.sample(users)!._id!
|
||||
const userMetadataId = backendCore.db.generateUserMetadataID(userId)
|
||||
const result = await config.doInTenant(() =>
|
||||
processInputBBReferences(userMetadataId, FieldSubtype.USER)
|
||||
processInputBBReferences(userMetadataId, BBReferenceFieldSubType.USER)
|
||||
)
|
||||
expect(result).toBe(userId)
|
||||
})
|
||||
|
@ -171,7 +178,7 @@ describe("bbReferenceProcessor", () => {
|
|||
const userId = user._id!
|
||||
|
||||
const result = await config.doInTenant(() =>
|
||||
processOutputBBReferences(userId, FieldSubtype.USER)
|
||||
processOutputBBReferences(userId, BBReferenceFieldSubType.USER)
|
||||
)
|
||||
|
||||
expect(result).toEqual([
|
||||
|
@ -195,7 +202,7 @@ describe("bbReferenceProcessor", () => {
|
|||
const result = await config.doInTenant(() =>
|
||||
processOutputBBReferences(
|
||||
[userId1, userId2].join(","),
|
||||
FieldSubtype.USER
|
||||
BBReferenceFieldSubType.USER
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { inputProcessing } from ".."
|
|||
import { generator, structures } from "@budibase/backend-core/tests"
|
||||
import {
|
||||
FieldType,
|
||||
FieldTypeSubtypes,
|
||||
BBReferenceFieldSubType,
|
||||
INTERNAL_TABLE_SOURCE_ID,
|
||||
Table,
|
||||
TableSourceType,
|
||||
|
@ -39,7 +39,7 @@ describe("rowProcessor - inputProcessing", () => {
|
|||
},
|
||||
user: {
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: FieldTypeSubtypes.BB_REFERENCE.USER,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
name: "user",
|
||||
constraints: {
|
||||
presence: true,
|
||||
|
@ -93,7 +93,7 @@ describe("rowProcessor - inputProcessing", () => {
|
|||
},
|
||||
user: {
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: FieldTypeSubtypes.BB_REFERENCE.USER,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
name: "user",
|
||||
constraints: {
|
||||
presence: false,
|
||||
|
@ -135,7 +135,7 @@ describe("rowProcessor - inputProcessing", () => {
|
|||
},
|
||||
user: {
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: FieldTypeSubtypes.BB_REFERENCE.USER,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
name: "user",
|
||||
constraints: {
|
||||
presence: false,
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import {
|
||||
FieldSubtype,
|
||||
FieldType,
|
||||
FieldTypeSubtypes,
|
||||
BBReferenceFieldSubType,
|
||||
INTERNAL_TABLE_SOURCE_ID,
|
||||
RowAttachment,
|
||||
Table,
|
||||
|
@ -42,7 +41,7 @@ describe("rowProcessor - outputProcessing", () => {
|
|||
},
|
||||
user: {
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: FieldTypeSubtypes.BB_REFERENCE.USER,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
name: "user",
|
||||
constraints: {
|
||||
presence: false,
|
||||
|
@ -69,7 +68,7 @@ describe("rowProcessor - outputProcessing", () => {
|
|||
).toHaveBeenCalledTimes(1)
|
||||
expect(bbReferenceProcessor.processOutputBBReferences).toHaveBeenCalledWith(
|
||||
"123",
|
||||
FieldSubtype.USER
|
||||
BBReferenceFieldSubType.USER
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -175,7 +174,7 @@ describe("rowProcessor - outputProcessing", () => {
|
|||
},
|
||||
user: {
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: FieldTypeSubtypes.BB_REFERENCE.USER,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
name: "user",
|
||||
constraints: {
|
||||
presence: false,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {
|
||||
FieldType,
|
||||
FieldSubtype,
|
||||
BBReferenceFieldSubType,
|
||||
TableSchema,
|
||||
FieldSchema,
|
||||
Row,
|
||||
|
@ -137,10 +137,10 @@ export function parse(rows: Rows, schema: TableSchema): Rows {
|
|||
parsedRow[columnName] = undefined
|
||||
} else {
|
||||
switch (columnSubtype) {
|
||||
case FieldSubtype.USER:
|
||||
case BBReferenceFieldSubType.USER:
|
||||
parsedRow[columnName] = parsedValues[0]?._id
|
||||
break
|
||||
case FieldSubtype.USERS:
|
||||
case BBReferenceFieldSubType.USERS:
|
||||
parsedRow[columnName] = parsedValues.map(u => u._id)
|
||||
break
|
||||
default:
|
||||
|
@ -164,11 +164,11 @@ export function parse(rows: Rows, schema: TableSchema): Rows {
|
|||
|
||||
function isValidBBReference(
|
||||
columnData: any,
|
||||
columnSubtype: FieldSubtype.USER | FieldSubtype.USERS
|
||||
columnSubtype: BBReferenceFieldSubType.USER | BBReferenceFieldSubType.USERS
|
||||
): boolean {
|
||||
switch (columnSubtype) {
|
||||
case FieldSubtype.USER:
|
||||
case FieldSubtype.USERS: {
|
||||
case BBReferenceFieldSubType.USER:
|
||||
case BBReferenceFieldSubType.USERS: {
|
||||
if (typeof columnData !== "string") {
|
||||
return false
|
||||
}
|
||||
|
@ -177,7 +177,10 @@ function isValidBBReference(
|
|||
return false
|
||||
}
|
||||
|
||||
if (columnSubtype === FieldSubtype.USER && userArray.length > 1) {
|
||||
if (
|
||||
columnSubtype === BBReferenceFieldSubType.USER &&
|
||||
userArray.length > 1
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {
|
||||
Datasource,
|
||||
FieldSubtype,
|
||||
BBReferenceFieldSubType,
|
||||
FieldType,
|
||||
FormulaType,
|
||||
SearchFilter,
|
||||
|
@ -22,7 +22,7 @@ const HBS_REGEX = /{{([^{].*?)}}/g
|
|||
export const getValidOperatorsForType = (
|
||||
fieldType: {
|
||||
type: FieldType
|
||||
subtype?: FieldSubtype
|
||||
subtype?: BBReferenceFieldSubType
|
||||
formulaType?: FormulaType
|
||||
},
|
||||
field: string,
|
||||
|
@ -68,9 +68,15 @@ export const getValidOperatorsForType = (
|
|||
ops = numOps
|
||||
} else if (type === FieldType.FORMULA && formulaType === FormulaType.STATIC) {
|
||||
ops = stringOps.concat([Op.MoreThan, Op.LessThan])
|
||||
} else if (type === FieldType.BB_REFERENCE && subtype == FieldSubtype.USER) {
|
||||
} else if (
|
||||
type === FieldType.BB_REFERENCE &&
|
||||
subtype == BBReferenceFieldSubType.USER
|
||||
) {
|
||||
ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty, Op.In]
|
||||
} else if (type === FieldType.BB_REFERENCE && subtype == FieldSubtype.USERS) {
|
||||
} else if (
|
||||
type === FieldType.BB_REFERENCE &&
|
||||
subtype == BBReferenceFieldSubType.USERS
|
||||
) {
|
||||
ops = [Op.Contains, Op.NotContains, Op.ContainsAny, Op.Empty, Op.NotEmpty]
|
||||
}
|
||||
|
||||
|
|
|
@ -124,16 +124,3 @@ export interface Row extends Document {
|
|||
_viewId?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export enum FieldSubtype {
|
||||
USER = "user",
|
||||
USERS = "users",
|
||||
}
|
||||
|
||||
// The 'as' are required for typescript not to type the outputs as generic FieldSubtype
|
||||
export const FieldTypeSubtypes = {
|
||||
BB_REFERENCE: {
|
||||
USER: FieldSubtype.USER as FieldSubtype.USER,
|
||||
USERS: FieldSubtype.USERS as FieldSubtype.USERS,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -24,3 +24,8 @@ export enum FormulaType {
|
|||
STATIC = "static",
|
||||
DYNAMIC = "dynamic",
|
||||
}
|
||||
|
||||
export enum BBReferenceFieldSubType {
|
||||
USER = "user",
|
||||
USERS = "users",
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
// all added by grid/table when defining the
|
||||
// column size, position and whether it can be viewed
|
||||
import { FieldSubtype, FieldType } from "../row"
|
||||
import { FieldType } from "../row"
|
||||
import {
|
||||
AutoFieldSubType,
|
||||
AutoReason,
|
||||
BBReferenceFieldSubType,
|
||||
FormulaType,
|
||||
JsonFieldSubType,
|
||||
RelationshipType,
|
||||
|
@ -109,7 +110,7 @@ export interface FormulaFieldMetadata extends BaseFieldSchema {
|
|||
export interface BBReferenceFieldMetadata
|
||||
extends Omit<BaseFieldSchema, "subtype"> {
|
||||
type: FieldType.BB_REFERENCE
|
||||
subtype: FieldSubtype.USER | FieldSubtype.USERS
|
||||
subtype: BBReferenceFieldSubType.USER | BBReferenceFieldSubType.USERS
|
||||
relationshipType?: RelationshipType
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ export enum PlanType {
|
|||
/** @deprecated */
|
||||
BUSINESS = "business",
|
||||
ENTERPRISE_BASIC = "enterprise_basic",
|
||||
ENTERPRISE_BASIC_TRIAL = "enterprise_basic_trial",
|
||||
ENTERPRISE = "enterprise",
|
||||
}
|
||||
|
||||
|
|
19
scripts/killTestcontainers.sh
Executable file
19
scripts/killTestcontainers.sh
Executable 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
|
|
@ -1,41 +0,0 @@
|
|||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
const { execSync } = require("child_process")
|
||||
|
||||
let version = "0.0.0"
|
||||
const localPro = fs.existsSync("packages/pro/src")
|
||||
if (!localPro) {
|
||||
version = "latest"
|
||||
}
|
||||
|
||||
// Get the list of workspaces with mismatched dependencies
|
||||
const output = execSync("yarn --silent workspaces info --json", {
|
||||
encoding: "utf-8",
|
||||
})
|
||||
const data = JSON.parse(output)
|
||||
|
||||
// Loop through each workspace and update the dependencies
|
||||
Object.keys(data).forEach(workspace => {
|
||||
// Loop through each dependency and update its version in package.json
|
||||
const packageJsonPath = path.join(data[workspace].location, "package.json")
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"))
|
||||
if (packageJson.version !== "0.0.0") {
|
||||
// Don't change if we are not using local versions
|
||||
return
|
||||
}
|
||||
|
||||
let hasChanges = false
|
||||
|
||||
if (packageJson.dependencies && packageJson.dependencies["@budibase/pro"]) {
|
||||
packageJson.dependencies["@budibase/pro"] = version
|
||||
hasChanges = true
|
||||
}
|
||||
|
||||
// Write changes to package.json if there are any
|
||||
if (hasChanges) {
|
||||
fs.writeFileSync(
|
||||
packageJsonPath,
|
||||
JSON.stringify(packageJson, null, 2) + "\n"
|
||||
)
|
||||
}
|
||||
})
|
292
yarn.lock
292
yarn.lock
|
@ -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"
|
||||
|
@ -2046,6 +2053,44 @@
|
|||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||
|
||||
"@budibase/backend-core@2.23.12":
|
||||
version "0.0.0"
|
||||
dependencies:
|
||||
"@budibase/nano" "10.1.5"
|
||||
"@budibase/pouchdb-replication-stream" "1.2.10"
|
||||
"@budibase/shared-core" "0.0.0"
|
||||
"@budibase/types" "0.0.0"
|
||||
"@govtechsg/passport-openidconnect" "^1.0.2"
|
||||
aws-cloudfront-sign "3.0.2"
|
||||
aws-sdk "2.1030.0"
|
||||
bcrypt "5.1.0"
|
||||
bcryptjs "2.4.3"
|
||||
bull "4.10.1"
|
||||
correlation-id "4.0.0"
|
||||
dd-trace "5.2.0"
|
||||
dotenv "16.0.1"
|
||||
ioredis "5.3.2"
|
||||
joi "17.6.0"
|
||||
jsonwebtoken "9.0.2"
|
||||
koa-passport "^6.0.0"
|
||||
koa-pino-logger "4.0.0"
|
||||
lodash "4.17.21"
|
||||
node-fetch "2.6.7"
|
||||
passport-google-oauth "2.0.0"
|
||||
passport-local "1.0.0"
|
||||
passport-oauth2-refresh "^2.1.0"
|
||||
pino "8.11.0"
|
||||
pino-http "8.3.3"
|
||||
posthog-node "1.3.0"
|
||||
pouchdb "7.3.0"
|
||||
pouchdb-find "7.2.2"
|
||||
redlock "4.2.0"
|
||||
rotating-file-stream "3.1.0"
|
||||
sanitize-s3-objectkey "0.0.1"
|
||||
semver "^7.5.4"
|
||||
tar-fs "2.1.1"
|
||||
uuid "^8.3.2"
|
||||
|
||||
"@budibase/handlebars-helpers@^0.13.1":
|
||||
version "0.13.1"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/handlebars-helpers/-/handlebars-helpers-0.13.1.tgz#d02e73c0df8305cd675e70dc37f8427eb0842080"
|
||||
|
@ -2090,6 +2135,44 @@
|
|||
pouchdb-promise "^6.0.4"
|
||||
through2 "^2.0.0"
|
||||
|
||||
"@budibase/pro@npm:@budibase/pro@latest":
|
||||
version "2.23.12"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.23.12.tgz#b2e813c547a5ed22b5bd86b1158159fe4b918260"
|
||||
integrity sha512-DMtfkrJDSIF9V7AL6brpuWWw7Ot5XxO4YQ32ggmr0264uU9KYsTFvlFXFP3MSF2H+247ZYUouSJU76+XeC13qQ==
|
||||
dependencies:
|
||||
"@budibase/backend-core" "2.23.12"
|
||||
"@budibase/shared-core" "2.23.12"
|
||||
"@budibase/string-templates" "2.23.12"
|
||||
"@budibase/types" "2.23.12"
|
||||
"@koa/router" "8.0.8"
|
||||
bull "4.10.1"
|
||||
joi "17.6.0"
|
||||
jsonwebtoken "9.0.2"
|
||||
lru-cache "^7.14.1"
|
||||
memorystream "^0.3.1"
|
||||
node-fetch "2.6.7"
|
||||
scim-patch "^0.8.1"
|
||||
scim2-parse-filter "^0.2.8"
|
||||
|
||||
"@budibase/shared-core@2.23.12":
|
||||
version "0.0.0"
|
||||
dependencies:
|
||||
"@budibase/types" "0.0.0"
|
||||
cron-validate "1.4.5"
|
||||
|
||||
"@budibase/string-templates@2.23.12":
|
||||
version "0.0.0"
|
||||
dependencies:
|
||||
"@budibase/handlebars-helpers" "^0.13.1"
|
||||
dayjs "^1.10.8"
|
||||
handlebars "^4.7.8"
|
||||
lodash.clonedeep "^4.5.0"
|
||||
|
||||
"@budibase/types@2.23.12":
|
||||
version "0.0.0"
|
||||
dependencies:
|
||||
scim-patch "^0.8.1"
|
||||
|
||||
"@bull-board/api@5.10.2":
|
||||
version "5.10.2"
|
||||
resolved "https://registry.yarnpkg.com/@bull-board/api/-/api-5.10.2.tgz#ae8ff6918b23897bf879a6ead3683f964374c4b3"
|
||||
|
@ -2675,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==
|
||||
|
@ -2685,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"
|
||||
|
@ -4700,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==
|
||||
|
@ -4732,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"
|
||||
|
@ -4784,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"
|
||||
|
@ -4876,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"
|
||||
|
@ -4911,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"
|
||||
|
@ -4936,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"
|
||||
|
@ -5680,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"
|
||||
|
||||
|
@ -8126,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"
|
||||
|
@ -9584,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"
|
||||
|
@ -10062,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"
|
||||
|
@ -10433,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"
|
||||
|
@ -11113,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"
|
||||
|
||||
|
@ -11445,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"
|
||||
|
@ -11677,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"
|
||||
|
@ -12297,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"
|
||||
|
@ -16141,10 +16257,10 @@ mute-stream@~1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-1.0.0.tgz#e31bd9fe62f0aed23520aa4324ea6671531e013e"
|
||||
integrity sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==
|
||||
|
||||
mysql2@3.5.2:
|
||||
version "3.5.2"
|
||||
resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.5.2.tgz#a06050e1514e9ac15711a8b883ffd51cb44b2dc8"
|
||||
integrity sha512-cptobmhYkYeTBIFp2c0piw2+gElpioga1rUw5UidHvo8yaHijMZoo8A3zyBVoo/K71f7ZFvrShA9iMIy9dCzCA==
|
||||
mysql2@3.9.7:
|
||||
version "3.9.7"
|
||||
resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.9.7.tgz#843755daf65b5ef08afe545fe14b8fb62824741a"
|
||||
integrity sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==
|
||||
dependencies:
|
||||
denque "^2.1.0"
|
||||
generate-function "^2.3.1"
|
||||
|
@ -18444,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"
|
||||
|
@ -18842,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==
|
||||
|
@ -19924,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"
|
||||
|
@ -20015,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"
|
||||
|
@ -20602,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==
|
||||
|
@ -20692,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==
|
||||
|
@ -20706,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"
|
||||
|
@ -22642,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==
|
||||
|
@ -22660,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"
|
||||
|
@ -23041,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"
|
||||
|
|
Loading…
Reference in a new issue