Merge remote-tracking branch 'origin/master' into feature/signature-field-and-component
This commit is contained in:
commit
a29301a2fc
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
|
"root": true,
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"es6": true,
|
"es6": true,
|
||||||
|
@ -42,6 +43,8 @@
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
|
"local-rules/no-budibase-imports": "error",
|
||||||
|
"local-rules/no-console-error": "error",
|
||||||
"@typescript-eslint/no-unused-vars": [
|
"@typescript-eslint/no-unused-vars": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
|
|
|
@ -92,8 +92,6 @@ jobs:
|
||||||
|
|
||||||
test-libraries:
|
test-libraries:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
|
||||||
REUSE_CONTAINERS: true
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
@ -150,8 +148,6 @@ jobs:
|
||||||
|
|
||||||
test-server:
|
test-server:
|
||||||
runs-on: budi-tubby-tornado-quad-core-150gb
|
runs-on: budi-tubby-tornado-quad-core-150gb
|
||||||
env:
|
|
||||||
REUSE_CONTAINERS: true
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
|
@ -12,4 +12,5 @@ packages/pro/coverage
|
||||||
packages/account-portal/packages/ui/build
|
packages/account-portal/packages/ui/build
|
||||||
packages/account-portal/packages/ui/.routify
|
packages/account-portal/packages/ui/.routify
|
||||||
packages/account-portal/packages/server/build
|
packages/account-portal/packages/server/build
|
||||||
|
packages/account-portal/packages/server/coverage
|
||||||
**/*.ivm.bundle.js
|
**/*.ivm.bundle.js
|
|
@ -106,6 +106,8 @@ spec:
|
||||||
value: {{ .Values.services.objectStore.globalBucketName | quote }}
|
value: {{ .Values.services.objectStore.globalBucketName | quote }}
|
||||||
- name: BACKUPS_BUCKET_NAME
|
- name: BACKUPS_BUCKET_NAME
|
||||||
value: {{ .Values.services.objectStore.backupsBucketName | quote }}
|
value: {{ .Values.services.objectStore.backupsBucketName | quote }}
|
||||||
|
- name: TEMP_BUCKET_NAME
|
||||||
|
value: {{ .Values.globals.tempBucketName | quote }}
|
||||||
- name: PORT
|
- name: PORT
|
||||||
value: {{ .Values.services.apps.port | quote }}
|
value: {{ .Values.services.apps.port | quote }}
|
||||||
{{ if .Values.services.worker.publicApiRateLimitPerSecond }}
|
{{ if .Values.services.worker.publicApiRateLimitPerSecond }}
|
||||||
|
|
|
@ -107,6 +107,8 @@ spec:
|
||||||
value: {{ .Values.services.objectStore.globalBucketName | quote }}
|
value: {{ .Values.services.objectStore.globalBucketName | quote }}
|
||||||
- name: BACKUPS_BUCKET_NAME
|
- name: BACKUPS_BUCKET_NAME
|
||||||
value: {{ .Values.services.objectStore.backupsBucketName | quote }}
|
value: {{ .Values.services.objectStore.backupsBucketName | quote }}
|
||||||
|
- name: TEMP_BUCKET_NAME
|
||||||
|
value: {{ .Values.globals.tempBucketName | quote }}
|
||||||
- name: PORT
|
- name: PORT
|
||||||
value: {{ .Values.services.automationWorkers.port | quote }}
|
value: {{ .Values.services.automationWorkers.port | quote }}
|
||||||
{{ if .Values.services.worker.publicApiRateLimitPerSecond }}
|
{{ if .Values.services.worker.publicApiRateLimitPerSecond }}
|
||||||
|
|
|
@ -106,6 +106,8 @@ spec:
|
||||||
value: {{ .Values.services.objectStore.globalBucketName | quote }}
|
value: {{ .Values.services.objectStore.globalBucketName | quote }}
|
||||||
- name: BACKUPS_BUCKET_NAME
|
- name: BACKUPS_BUCKET_NAME
|
||||||
value: {{ .Values.services.objectStore.backupsBucketName | quote }}
|
value: {{ .Values.services.objectStore.backupsBucketName | quote }}
|
||||||
|
- name: TEMP_BUCKET_NAME
|
||||||
|
value: {{ .Values.globals.tempBucketName | quote }}
|
||||||
- name: PORT
|
- name: PORT
|
||||||
value: {{ .Values.services.worker.port | quote }}
|
value: {{ .Values.services.worker.port | quote }}
|
||||||
- name: MULTI_TENANCY
|
- name: MULTI_TENANCY
|
||||||
|
|
|
@ -121,6 +121,9 @@ globals:
|
||||||
# to the old value for the duration of the rotation.
|
# to the old value for the duration of the rotation.
|
||||||
jwtSecretFallback: ""
|
jwtSecretFallback: ""
|
||||||
|
|
||||||
|
## -- If using S3 the bucket name to be used for storing temporary files
|
||||||
|
tempBucketName: ""
|
||||||
|
|
||||||
smtp:
|
smtp:
|
||||||
# -- Whether to enable SMTP or not.
|
# -- Whether to enable SMTP or not.
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
|
@ -1,4 +1,25 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
"no-console-error": {
|
||||||
|
create: function(context) {
|
||||||
|
return {
|
||||||
|
CallExpression(node) {
|
||||||
|
if (
|
||||||
|
node.callee.type === "MemberExpression" &&
|
||||||
|
node.callee.object.name === "console" &&
|
||||||
|
node.callee.property.name === "error" &&
|
||||||
|
node.arguments.length === 1 &&
|
||||||
|
node.arguments[0].name &&
|
||||||
|
node.arguments[0].name.startsWith("err")
|
||||||
|
) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
message: 'Using console.error(err) on its own is not allowed. Either provide context to the error (console.error(msg, err)) or throw it.',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
"no-budibase-imports": {
|
"no-budibase-imports": {
|
||||||
create: function (context) {
|
create: function (context) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,16 +1,49 @@
|
||||||
import { GenericContainer, Wait } from "testcontainers"
|
import {
|
||||||
|
GenericContainer,
|
||||||
|
Wait,
|
||||||
|
getContainerRuntimeClient,
|
||||||
|
} from "testcontainers"
|
||||||
|
import { ContainerInfo } from "dockerode"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import lockfile from "proper-lockfile"
|
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() {
|
export default async function setup() {
|
||||||
const lockPath = path.resolve(__dirname, "globalSetup.ts")
|
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
|
||||||
// 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
|
||||||
// shared container to get started multiple times despite having an
|
// identical reuse hash. To avoid that, we do a filesystem-based lock so
|
||||||
// identical reuse hash. To avoid that, we do a filesystem-based lock so
|
// that only one globalSetup.ts is running at a time.
|
||||||
// that only one globalSetup.ts is running at a time.
|
lockfile.lockSync(lockPath)
|
||||||
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 {
|
try {
|
||||||
let couchdb = new GenericContainer("budibase/couchdb:v3.2.1-sqs")
|
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",
|
target: "/opt/couchdb/etc/local.d/test-couchdb.ini",
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
.withLabels({ "com.budibase": "true" })
|
||||||
|
.withReuse()
|
||||||
.withWaitStrategy(
|
.withWaitStrategy(
|
||||||
Wait.forSuccessfulCommand(
|
Wait.forSuccessfulCommand(
|
||||||
"curl http://budibase:budibase@localhost:5984/_up"
|
"curl http://budibase:budibase@localhost:5984/_up"
|
||||||
).withStartupTimeout(20000)
|
).withStartupTimeout(20000)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (process.env.REUSE_CONTAINERS) {
|
|
||||||
couchdb = couchdb.withReuse()
|
|
||||||
}
|
|
||||||
|
|
||||||
await couchdb.start()
|
await couchdb.start()
|
||||||
} finally {
|
} finally {
|
||||||
if (process.env.REUSE_CONTAINERS) {
|
lockfile.unlockSync(lockPath)
|
||||||
lockfile.unlockSync(lockPath)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,10 +70,10 @@ sed -i "s#COUCHDB_ERLANG_COOKIE#${COUCHDB_ERLANG_COOKIE}#g" /opt/clouseau/clouse
|
||||||
/opt/clouseau/bin/clouseau > /dev/stdout 2>&1 &
|
/opt/clouseau/bin/clouseau > /dev/stdout 2>&1 &
|
||||||
|
|
||||||
# Start CouchDB.
|
# Start CouchDB.
|
||||||
/docker-entrypoint.sh /opt/couchdb/bin/couchdb &
|
/docker-entrypoint.sh /opt/couchdb/bin/couchdb > /dev/stdout 2>&1 &
|
||||||
|
|
||||||
# Start SQS.
|
# Start SQS. Use 127.0.0.1 instead of localhost to avoid IPv6 issues.
|
||||||
/opt/sqs/sqs --server "http://localhost:5984" --data-dir ${DATA_DIR}/sqs --bind-address=0.0.0.0 &
|
/opt/sqs/sqs --server "http://127.0.0.1:5984" --data-dir ${DATA_DIR}/sqs --bind-address=0.0.0.0 > /dev/stdout 2>&1 &
|
||||||
|
|
||||||
# Wait for CouchDB to start up.
|
# Wait for CouchDB to start up.
|
||||||
while [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/_up -o /dev/null) -ne 200 ]]; do
|
while [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/_up -o /dev/null) -ne 200 ]]; do
|
||||||
|
|
|
@ -19,9 +19,6 @@ RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
|
||||||
RUN ./scripts/removeWorkspaceDependencies.sh packages/server/package.json
|
RUN ./scripts/removeWorkspaceDependencies.sh packages/server/package.json
|
||||||
RUN ./scripts/removeWorkspaceDependencies.sh packages/worker/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 jq 'del(.scripts.postinstall)' package.json > temp.json && mv temp.json package.json
|
||||||
RUN ./scripts/removeWorkspaceDependencies.sh 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
|
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production --frozen-lockfile
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.23.8",
|
"version": "2.24.1",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
|
@ -32,7 +32,6 @@
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "node scripts/syncProPackage.js",
|
|
||||||
"get-past-client-version": "node scripts/getPastClientVersion.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",
|
"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",
|
"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: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:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built",
|
||||||
"dev:docker": "./scripts/devDocker.sh",
|
"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:eslint": "eslint packages --max-warnings=0",
|
||||||
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"",
|
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"",
|
||||||
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
||||||
|
@ -107,6 +107,7 @@
|
||||||
"@budibase/shared-core": "0.0.0",
|
"@budibase/shared-core": "0.0.0",
|
||||||
"@budibase/string-templates": "0.0.0",
|
"@budibase/string-templates": "0.0.0",
|
||||||
"@budibase/types": "0.0.0",
|
"@budibase/types": "0.0.0",
|
||||||
|
"@budibase/pro": "npm:@budibase/pro@latest",
|
||||||
"tough-cookie": "4.1.3",
|
"tough-cookie": "4.1.3",
|
||||||
"node-fetch": "2.6.7",
|
"node-fetch": "2.6.7",
|
||||||
"semver": "7.5.3",
|
"semver": "7.5.3",
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit eb7d5da233885c5cffd9c255d3e954d0cd39185e
|
Subproject commit c167c331ff9b8161fc18e2ecbaaf1ea5815ba964
|
|
@ -64,7 +64,6 @@ async function refreshOIDCAccessToken(
|
||||||
}
|
}
|
||||||
strategy = await oidc.strategyFactory(enrichedConfig, ssoSaveUserNoOp)
|
strategy = await oidc.strategyFactory(enrichedConfig, ssoSaveUserNoOp)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
|
||||||
throw new Error("Could not refresh OAuth Token")
|
throw new Error("Could not refresh OAuth Token")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +98,6 @@ async function refreshGoogleAccessToken(
|
||||||
ssoSaveUserNoOp
|
ssoSaveUserNoOp
|
||||||
)
|
)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err)
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Error constructing OIDC refresh strategy: message=${err.message}`
|
`Error constructing OIDC refresh strategy: message=${err.message}`
|
||||||
)
|
)
|
||||||
|
|
|
@ -29,6 +29,7 @@ const DefaultBucketName = {
|
||||||
TEMPLATES: "templates",
|
TEMPLATES: "templates",
|
||||||
GLOBAL: "global",
|
GLOBAL: "global",
|
||||||
PLUGINS: "plugins",
|
PLUGINS: "plugins",
|
||||||
|
TEMP: "tmp-file-attachments",
|
||||||
}
|
}
|
||||||
|
|
||||||
const selfHosted = !!parseInt(process.env.SELF_HOSTED || "")
|
const selfHosted = !!parseInt(process.env.SELF_HOSTED || "")
|
||||||
|
@ -146,6 +147,7 @@ const environment = {
|
||||||
process.env.GLOBAL_BUCKET_NAME || DefaultBucketName.GLOBAL,
|
process.env.GLOBAL_BUCKET_NAME || DefaultBucketName.GLOBAL,
|
||||||
PLUGIN_BUCKET_NAME:
|
PLUGIN_BUCKET_NAME:
|
||||||
process.env.PLUGIN_BUCKET_NAME || DefaultBucketName.PLUGINS,
|
process.env.PLUGIN_BUCKET_NAME || DefaultBucketName.PLUGINS,
|
||||||
|
TEMP_BUCKET_NAME: process.env.TEMP_BUCKET_NAME || DefaultBucketName.TEMP,
|
||||||
USE_COUCH: process.env.USE_COUCH || true,
|
USE_COUCH: process.env.USE_COUCH || true,
|
||||||
MOCK_REDIS: process.env.MOCK_REDIS,
|
MOCK_REDIS: process.env.MOCK_REDIS,
|
||||||
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
|
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
|
||||||
|
|
|
@ -138,7 +138,6 @@ export default function (
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
authenticated = false
|
authenticated = false
|
||||||
console.error(`Auth Error: ${err.message}`)
|
console.error(`Auth Error: ${err.message}`)
|
||||||
console.error(err)
|
|
||||||
// remove the cookie as the user does not exist anymore
|
// remove the cookie as the user does not exist anymore
|
||||||
clearCookie(ctx, Cookie.Auth)
|
clearCookie(ctx, Cookie.Auth)
|
||||||
}
|
}
|
||||||
|
@ -187,7 +186,6 @@ export default function (
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(`Auth Error: ${err.message}`)
|
console.error(`Auth Error: ${err.message}`)
|
||||||
console.error(err)
|
|
||||||
// invalid token, clear the cookie
|
// invalid token, clear the cookie
|
||||||
if (err?.name === "JsonWebTokenError") {
|
if (err?.name === "JsonWebTokenError") {
|
||||||
clearCookie(ctx, Cookie.Auth)
|
clearCookie(ctx, Cookie.Auth)
|
||||||
|
|
|
@ -12,7 +12,7 @@ export async function errorHandling(ctx: any, next: any) {
|
||||||
if (status >= 400 && status < 500) {
|
if (status >= 400 && status < 500) {
|
||||||
console.warn(err)
|
console.warn(err)
|
||||||
} else {
|
} else {
|
||||||
console.error(err)
|
console.error("Got 400 response code", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
let error: APIError = {
|
let error: APIError = {
|
||||||
|
|
|
@ -68,7 +68,6 @@ export async function strategyFactory(
|
||||||
verify
|
verify
|
||||||
)
|
)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err)
|
|
||||||
throw new Error(`Error constructing google authentication strategy: ${err}`)
|
throw new Error(`Error constructing google authentication strategy: ${err}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,7 +103,6 @@ export async function strategyFactory(
|
||||||
strategy.name = "oidc"
|
strategy.name = "oidc"
|
||||||
return strategy
|
return strategy
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err)
|
|
||||||
throw new Error(`Error constructing OIDC authentication strategy - ${err}`)
|
throw new Error(`Error constructing OIDC authentication strategy - ${err}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -142,7 +141,6 @@ export async function fetchStrategyConfig(
|
||||||
callbackURL: callbackUrl,
|
callbackURL: callbackUrl,
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Error constructing OIDC authentication configuration - ${err}`
|
`Error constructing OIDC authentication configuration - ${err}`
|
||||||
)
|
)
|
||||||
|
|
|
@ -26,7 +26,6 @@ export const getMigrationsDoc = async (db: any) => {
|
||||||
if (err.status && err.status === 404) {
|
if (err.status && err.status === 404) {
|
||||||
return { _id: DocumentType.MIGRATIONS }
|
return { _id: DocumentType.MIGRATIONS }
|
||||||
} else {
|
} else {
|
||||||
console.error(err)
|
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,31 +7,41 @@ import tar from "tar-fs"
|
||||||
import zlib from "zlib"
|
import zlib from "zlib"
|
||||||
import { promisify } from "util"
|
import { promisify } from "util"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import fs, { ReadStream } from "fs"
|
import fs, { PathLike, ReadStream } from "fs"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import { budibaseTempDir } from "./utils"
|
import { bucketTTLConfig, budibaseTempDir } from "./utils"
|
||||||
import { v4 } from "uuid"
|
import { v4 } from "uuid"
|
||||||
import { APP_PREFIX, APP_DEV_PREFIX } from "../db"
|
import { APP_PREFIX, APP_DEV_PREFIX } from "../db"
|
||||||
|
import fsp from "fs/promises"
|
||||||
|
|
||||||
const streamPipeline = promisify(stream.pipeline)
|
const streamPipeline = promisify(stream.pipeline)
|
||||||
// use this as a temporary store of buckets that are being created
|
// use this as a temporary store of buckets that are being created
|
||||||
const STATE = {
|
const STATE = {
|
||||||
bucketCreationPromises: {},
|
bucketCreationPromises: {},
|
||||||
}
|
}
|
||||||
|
const signedFilePrefix = "/files/signed"
|
||||||
|
|
||||||
type ListParams = {
|
type ListParams = {
|
||||||
ContinuationToken?: string
|
ContinuationToken?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type UploadParams = {
|
type BaseUploadParams = {
|
||||||
bucket: string
|
bucket: string
|
||||||
filename: string
|
filename: string
|
||||||
path: string
|
|
||||||
type?: string | null
|
type?: string | null
|
||||||
// can be undefined, we will remove it
|
metadata?: { [key: string]: string | undefined }
|
||||||
metadata?: {
|
body?: ReadableStream | Buffer
|
||||||
[key: string]: string | undefined
|
ttl?: number
|
||||||
}
|
addTTL?: boolean
|
||||||
|
extra?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
type UploadParams = BaseUploadParams & {
|
||||||
|
path?: string | PathLike
|
||||||
|
}
|
||||||
|
|
||||||
|
type StreamUploadParams = BaseUploadParams & {
|
||||||
|
stream: ReadStream
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONTENT_TYPE_MAP: any = {
|
const CONTENT_TYPE_MAP: any = {
|
||||||
|
@ -41,6 +51,8 @@ const CONTENT_TYPE_MAP: any = {
|
||||||
js: "application/javascript",
|
js: "application/javascript",
|
||||||
json: "application/json",
|
json: "application/json",
|
||||||
gz: "application/gzip",
|
gz: "application/gzip",
|
||||||
|
svg: "image/svg+xml",
|
||||||
|
form: "multipart/form-data",
|
||||||
}
|
}
|
||||||
|
|
||||||
const STRING_CONTENT_TYPES = [
|
const STRING_CONTENT_TYPES = [
|
||||||
|
@ -105,7 +117,10 @@ export function ObjectStore(
|
||||||
* Given an object store and a bucket name this will make sure the bucket exists,
|
* Given an object store and a bucket name this will make sure the bucket exists,
|
||||||
* if it does not exist then it will create it.
|
* if it does not exist then it will create it.
|
||||||
*/
|
*/
|
||||||
export async function makeSureBucketExists(client: any, bucketName: string) {
|
export async function createBucketIfNotExists(
|
||||||
|
client: any,
|
||||||
|
bucketName: string
|
||||||
|
): Promise<{ created: boolean; exists: boolean }> {
|
||||||
bucketName = sanitizeBucket(bucketName)
|
bucketName = sanitizeBucket(bucketName)
|
||||||
try {
|
try {
|
||||||
await client
|
await client
|
||||||
|
@ -113,15 +128,16 @@ export async function makeSureBucketExists(client: any, bucketName: string) {
|
||||||
Bucket: bucketName,
|
Bucket: bucketName,
|
||||||
})
|
})
|
||||||
.promise()
|
.promise()
|
||||||
|
return { created: false, exists: true }
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const promises: any = STATE.bucketCreationPromises
|
const promises: any = STATE.bucketCreationPromises
|
||||||
const doesntExist = err.statusCode === 404,
|
const doesntExist = err.statusCode === 404,
|
||||||
noAccess = err.statusCode === 403
|
noAccess = err.statusCode === 403
|
||||||
if (promises[bucketName]) {
|
if (promises[bucketName]) {
|
||||||
await promises[bucketName]
|
await promises[bucketName]
|
||||||
|
return { created: false, exists: true }
|
||||||
} else if (doesntExist || noAccess) {
|
} else if (doesntExist || noAccess) {
|
||||||
if (doesntExist) {
|
if (doesntExist) {
|
||||||
// bucket doesn't exist create it
|
|
||||||
promises[bucketName] = client
|
promises[bucketName] = client
|
||||||
.createBucket({
|
.createBucket({
|
||||||
Bucket: bucketName,
|
Bucket: bucketName,
|
||||||
|
@ -129,13 +145,15 @@ export async function makeSureBucketExists(client: any, bucketName: string) {
|
||||||
.promise()
|
.promise()
|
||||||
await promises[bucketName]
|
await promises[bucketName]
|
||||||
delete promises[bucketName]
|
delete promises[bucketName]
|
||||||
|
return { created: true, exists: false }
|
||||||
|
} else {
|
||||||
|
throw new Error("Access denied to object store bucket." + err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Unable to write to object store bucket.")
|
throw new Error("Unable to write to object store bucket.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uploads the contents of a file given the required parameters, useful when
|
* Uploads the contents of a file given the required parameters, useful when
|
||||||
* temp files in use (for example file uploaded as an attachment).
|
* temp files in use (for example file uploaded as an attachment).
|
||||||
|
@ -146,12 +164,22 @@ export async function upload({
|
||||||
path,
|
path,
|
||||||
type,
|
type,
|
||||||
metadata,
|
metadata,
|
||||||
|
body,
|
||||||
|
ttl,
|
||||||
}: UploadParams) {
|
}: UploadParams) {
|
||||||
const extension = filename.split(".").pop()
|
const extension = filename.split(".").pop()
|
||||||
const fileBytes = fs.readFileSync(path)
|
|
||||||
|
const fileBytes = path ? (await fsp.open(path)).createReadStream() : body
|
||||||
|
|
||||||
const objectStore = ObjectStore(bucketName)
|
const objectStore = ObjectStore(bucketName)
|
||||||
await makeSureBucketExists(objectStore, bucketName)
|
const bucketCreated = await createBucketIfNotExists(objectStore, bucketName)
|
||||||
|
|
||||||
|
if (ttl && (bucketCreated.created || bucketCreated.exists)) {
|
||||||
|
let ttlConfig = bucketTTLConfig(bucketName, ttl)
|
||||||
|
if (objectStore.putBucketLifecycleConfiguration) {
|
||||||
|
await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let contentType = type
|
let contentType = type
|
||||||
if (!contentType) {
|
if (!contentType) {
|
||||||
|
@ -174,6 +202,7 @@ export async function upload({
|
||||||
}
|
}
|
||||||
config.Metadata = metadata
|
config.Metadata = metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
return objectStore.upload(config).promise()
|
return objectStore.upload(config).promise()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,14 +210,24 @@ export async function upload({
|
||||||
* Similar to the upload function but can be used to send a file stream
|
* Similar to the upload function but can be used to send a file stream
|
||||||
* through to the object store.
|
* through to the object store.
|
||||||
*/
|
*/
|
||||||
export async function streamUpload(
|
export async function streamUpload({
|
||||||
bucketName: string,
|
bucket: bucketName,
|
||||||
filename: string,
|
stream,
|
||||||
stream: ReadStream | ReadableStream,
|
filename,
|
||||||
extra = {}
|
type,
|
||||||
) {
|
extra,
|
||||||
|
ttl,
|
||||||
|
}: StreamUploadParams) {
|
||||||
|
const extension = filename.split(".").pop()
|
||||||
const objectStore = ObjectStore(bucketName)
|
const objectStore = ObjectStore(bucketName)
|
||||||
await makeSureBucketExists(objectStore, bucketName)
|
const bucketCreated = await createBucketIfNotExists(objectStore, bucketName)
|
||||||
|
|
||||||
|
if (ttl && (bucketCreated.created || bucketCreated.exists)) {
|
||||||
|
let ttlConfig = bucketTTLConfig(bucketName, ttl)
|
||||||
|
if (objectStore.putBucketLifecycleConfiguration) {
|
||||||
|
await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set content type for certain known extensions
|
// Set content type for certain known extensions
|
||||||
if (filename?.endsWith(".js")) {
|
if (filename?.endsWith(".js")) {
|
||||||
|
@ -203,10 +242,18 @@ export async function streamUpload(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let contentType = type
|
||||||
|
if (!contentType) {
|
||||||
|
contentType = extension
|
||||||
|
? CONTENT_TYPE_MAP[extension.toLowerCase()]
|
||||||
|
: CONTENT_TYPE_MAP.txt
|
||||||
|
}
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: sanitizeBucket(bucketName),
|
Bucket: sanitizeBucket(bucketName),
|
||||||
Key: sanitizeKey(filename),
|
Key: sanitizeKey(filename),
|
||||||
Body: stream,
|
Body: stream,
|
||||||
|
ContentType: contentType,
|
||||||
...extra,
|
...extra,
|
||||||
}
|
}
|
||||||
return objectStore.upload(params).promise()
|
return objectStore.upload(params).promise()
|
||||||
|
@ -286,7 +333,7 @@ export function getPresignedUrl(
|
||||||
const signedUrl = new URL(url)
|
const signedUrl = new URL(url)
|
||||||
const path = signedUrl.pathname
|
const path = signedUrl.pathname
|
||||||
const query = signedUrl.search
|
const query = signedUrl.search
|
||||||
return `/files/signed${path}${query}`
|
return `${signedFilePrefix}${path}${query}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -341,7 +388,7 @@ export async function retrieveDirectory(bucketName: string, path: string) {
|
||||||
*/
|
*/
|
||||||
export async function deleteFile(bucketName: string, filepath: string) {
|
export async function deleteFile(bucketName: string, filepath: string) {
|
||||||
const objectStore = ObjectStore(bucketName)
|
const objectStore = ObjectStore(bucketName)
|
||||||
await makeSureBucketExists(objectStore, bucketName)
|
await createBucketIfNotExists(objectStore, bucketName)
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: bucketName,
|
Bucket: bucketName,
|
||||||
Key: sanitizeKey(filepath),
|
Key: sanitizeKey(filepath),
|
||||||
|
@ -351,7 +398,7 @@ export async function deleteFile(bucketName: string, filepath: string) {
|
||||||
|
|
||||||
export async function deleteFiles(bucketName: string, filepaths: string[]) {
|
export async function deleteFiles(bucketName: string, filepaths: string[]) {
|
||||||
const objectStore = ObjectStore(bucketName)
|
const objectStore = ObjectStore(bucketName)
|
||||||
await makeSureBucketExists(objectStore, bucketName)
|
await createBucketIfNotExists(objectStore, bucketName)
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: bucketName,
|
Bucket: bucketName,
|
||||||
Delete: {
|
Delete: {
|
||||||
|
@ -412,7 +459,13 @@ export async function uploadDirectory(
|
||||||
if (file.isDirectory()) {
|
if (file.isDirectory()) {
|
||||||
uploads.push(uploadDirectory(bucketName, local, path))
|
uploads.push(uploadDirectory(bucketName, local, path))
|
||||||
} else {
|
} else {
|
||||||
uploads.push(streamUpload(bucketName, path, fs.createReadStream(local)))
|
uploads.push(
|
||||||
|
streamUpload({
|
||||||
|
bucket: bucketName,
|
||||||
|
filename: path,
|
||||||
|
stream: fs.createReadStream(local),
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await Promise.all(uploads)
|
await Promise.all(uploads)
|
||||||
|
@ -467,3 +520,23 @@ export async function getReadStream(
|
||||||
}
|
}
|
||||||
return client.getObject(params).createReadStream()
|
return client.getObject(params).createReadStream()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Given a signed url like '/files/signed/tmp-files-attachments/app_123456/myfile.txt' extract
|
||||||
|
the bucket and the path from it
|
||||||
|
*/
|
||||||
|
export function extractBucketAndPath(
|
||||||
|
url: string
|
||||||
|
): { bucket: string; path: string } | null {
|
||||||
|
const baseUrl = url.split("?")[0]
|
||||||
|
|
||||||
|
const regex = new RegExp(`^${signedFilePrefix}/(?<bucket>[^/]+)/(?<path>.+)$`)
|
||||||
|
const match = baseUrl.match(regex)
|
||||||
|
|
||||||
|
if (match && match.groups) {
|
||||||
|
const { bucket, path } = match.groups
|
||||||
|
return { bucket, path }
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { join } from "path"
|
||||||
import { tmpdir } from "os"
|
import { tmpdir } from "os"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
|
import { PutBucketLifecycleConfigurationRequest } from "aws-sdk/clients/s3"
|
||||||
|
|
||||||
/****************************************************
|
/****************************************************
|
||||||
* NOTE: When adding a new bucket - name *
|
* NOTE: When adding a new bucket - name *
|
||||||
|
@ -15,6 +16,7 @@ export const ObjectStoreBuckets = {
|
||||||
TEMPLATES: env.TEMPLATES_BUCKET_NAME,
|
TEMPLATES: env.TEMPLATES_BUCKET_NAME,
|
||||||
GLOBAL: env.GLOBAL_BUCKET_NAME,
|
GLOBAL: env.GLOBAL_BUCKET_NAME,
|
||||||
PLUGINS: env.PLUGIN_BUCKET_NAME,
|
PLUGINS: env.PLUGIN_BUCKET_NAME,
|
||||||
|
TEMP: env.TEMP_BUCKET_NAME,
|
||||||
}
|
}
|
||||||
|
|
||||||
const bbTmp = join(tmpdir(), ".budibase")
|
const bbTmp = join(tmpdir(), ".budibase")
|
||||||
|
@ -29,3 +31,27 @@ try {
|
||||||
export function budibaseTempDir() {
|
export function budibaseTempDir() {
|
||||||
return bbTmp
|
return bbTmp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const bucketTTLConfig = (
|
||||||
|
bucketName: string,
|
||||||
|
days: number
|
||||||
|
): PutBucketLifecycleConfigurationRequest => {
|
||||||
|
const lifecycleRule = {
|
||||||
|
ID: `${bucketName}-ExpireAfter${days}days`,
|
||||||
|
Prefix: "",
|
||||||
|
Status: "Enabled",
|
||||||
|
Expiration: {
|
||||||
|
Days: days,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const lifecycleConfiguration = {
|
||||||
|
Rules: [lifecycleRule],
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
Bucket: bucketName,
|
||||||
|
LifecycleConfiguration: lifecycleConfiguration,
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
|
@ -4,3 +4,6 @@ export { generator } from "./structures"
|
||||||
export * as testContainerUtils from "./testContainerUtils"
|
export * as testContainerUtils from "./testContainerUtils"
|
||||||
export * as utils from "./utils"
|
export * as utils from "./utils"
|
||||||
export * from "./jestUtils"
|
export * from "./jestUtils"
|
||||||
|
import * as minio from "./minio"
|
||||||
|
|
||||||
|
export const objectStoreTestProviders = { minio }
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { GenericContainer, Wait, StartedTestContainer } from "testcontainers"
|
||||||
|
import { AbstractWaitStrategy } from "testcontainers/build/wait-strategies/wait-strategy"
|
||||||
|
import env from "../../../src/environment"
|
||||||
|
|
||||||
|
let container: StartedTestContainer | undefined
|
||||||
|
|
||||||
|
class ObjectStoreWaitStrategy extends AbstractWaitStrategy {
|
||||||
|
async waitUntilReady(container: any, boundPorts: any, startTime?: Date) {
|
||||||
|
const logs = Wait.forListeningPorts()
|
||||||
|
await logs.waitUntilReady(container, boundPorts, startTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function start(): Promise<void> {
|
||||||
|
container = await new GenericContainer("minio/minio")
|
||||||
|
.withExposedPorts(9000)
|
||||||
|
.withCommand(["server", "/data"])
|
||||||
|
.withEnvironment({
|
||||||
|
MINIO_ACCESS_KEY: "budibase",
|
||||||
|
MINIO_SECRET_KEY: "budibase",
|
||||||
|
})
|
||||||
|
.withWaitStrategy(new ObjectStoreWaitStrategy().withStartupTimeout(30000))
|
||||||
|
.start()
|
||||||
|
|
||||||
|
const port = container.getMappedPort(9000)
|
||||||
|
env._set("MINIO_URL", `http://0.0.0.0:${port}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function stop() {
|
||||||
|
if (container) {
|
||||||
|
await container.stop()
|
||||||
|
container = undefined
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,7 +28,11 @@ function getTestcontainers(): ContainerInfo[] {
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.filter(x => x.length > 0)
|
.filter(x => x.length > 0)
|
||||||
.map(x => JSON.parse(x) as ContainerInfo)
|
.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) {
|
export function getContainerByImage(image: string) {
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
"@spectrum-css/avatar": "3.0.2",
|
"@spectrum-css/avatar": "3.0.2",
|
||||||
"@spectrum-css/button": "3.0.1",
|
"@spectrum-css/button": "3.0.1",
|
||||||
"@spectrum-css/buttongroup": "3.0.2",
|
"@spectrum-css/buttongroup": "3.0.2",
|
||||||
|
"@spectrum-css/calendar": "3.2.7",
|
||||||
"@spectrum-css/checkbox": "3.0.2",
|
"@spectrum-css/checkbox": "3.0.2",
|
||||||
"@spectrum-css/dialog": "3.0.1",
|
"@spectrum-css/dialog": "3.0.1",
|
||||||
"@spectrum-css/divider": "1.0.3",
|
"@spectrum-css/divider": "1.0.3",
|
||||||
|
@ -82,7 +83,6 @@
|
||||||
"dayjs": "^1.10.8",
|
"dayjs": "^1.10.8",
|
||||||
"easymde": "^2.16.1",
|
"easymde": "^2.16.1",
|
||||||
"svelte-dnd-action": "^0.9.8",
|
"svelte-dnd-action": "^0.9.8",
|
||||||
"svelte-flatpickr": "3.2.3",
|
|
||||||
"svelte-portal": "^1.0.0"
|
"svelte-portal": "^1.0.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
|
|
@ -38,7 +38,15 @@
|
||||||
<div use:getAnchor on:click={openMenu}>
|
<div use:getAnchor on:click={openMenu}>
|
||||||
<slot name="control" />
|
<slot name="control" />
|
||||||
</div>
|
</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>
|
<Menu>
|
||||||
<slot />
|
<slot />
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
|
@ -1,13 +1,24 @@
|
||||||
|
// These class names will never trigger a callback if clicked, no matter what
|
||||||
const ignoredClasses = [
|
const ignoredClasses = [
|
||||||
".flatpickr-calendar",
|
|
||||||
".spectrum-Popover",
|
|
||||||
".download-js-link",
|
".download-js-link",
|
||||||
|
".spectrum-Menu",
|
||||||
|
".date-time-popover",
|
||||||
|
]
|
||||||
|
|
||||||
|
// These class names will only trigger a callback when clicked if the registered
|
||||||
|
// component is not nested inside them. For example, clicking inside a modal
|
||||||
|
// will not close the modal, or clicking inside a popover will not close the
|
||||||
|
// popover.
|
||||||
|
const conditionallyIgnoredClasses = [
|
||||||
|
".spectrum-Underlay",
|
||||||
|
".drawer-wrapper",
|
||||||
|
".spectrum-Popover",
|
||||||
]
|
]
|
||||||
let clickHandlers = []
|
let clickHandlers = []
|
||||||
|
let candidateTarget
|
||||||
|
|
||||||
/**
|
// Processes a "click outside" event and invokes callbacks if our source element
|
||||||
* Handle a body click event
|
// is valid
|
||||||
*/
|
|
||||||
const handleClick = event => {
|
const handleClick = event => {
|
||||||
// Ignore click if this is an ignored class
|
// Ignore click if this is an ignored class
|
||||||
if (event.target.closest('[data-ignore-click-outside="true"]')) {
|
if (event.target.closest('[data-ignore-click-outside="true"]')) {
|
||||||
|
@ -21,41 +32,60 @@ const handleClick = event => {
|
||||||
|
|
||||||
// Process handlers
|
// Process handlers
|
||||||
clickHandlers.forEach(handler => {
|
clickHandlers.forEach(handler => {
|
||||||
|
// Check that the click isn't inside the target
|
||||||
if (handler.element.contains(event.target)) {
|
if (handler.element.contains(event.target)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore clicks for modals, unless the handler is registered from a modal
|
// Ignore clicks for certain classes unless we're nested inside them
|
||||||
const sourceInModal = handler.anchor.closest(".spectrum-Underlay") != null
|
for (let className of conditionallyIgnoredClasses) {
|
||||||
const clickInModal = event.target.closest(".spectrum-Underlay") != null
|
const sourceInside = handler.anchor.closest(className) != null
|
||||||
if (clickInModal && !sourceInModal) {
|
const clickInside = event.target.closest(className) != null
|
||||||
return
|
if (clickInside && !sourceInside) {
|
||||||
}
|
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.callback?.(event)
|
handler.callback?.(event)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
document.documentElement.addEventListener("click", handleClick, true)
|
|
||||||
document.documentElement.addEventListener("mousedown", handleClick, true)
|
// On mouse up we only trigger a "click outside" callback if we targetted the
|
||||||
|
// same element that we did on mouse down. This fixes all sorts of issues where
|
||||||
|
// we get annoying callbacks firing when we drag to select text.
|
||||||
|
const handleMouseUp = e => {
|
||||||
|
if (candidateTarget === e.target) {
|
||||||
|
handleClick(e)
|
||||||
|
}
|
||||||
|
candidateTarget = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// On mouse down we store which element was targetted for comparison later
|
||||||
|
const handleMouseDown = e => {
|
||||||
|
// Only handle the primary mouse button here.
|
||||||
|
// We handle context menu (right click) events in another handler.
|
||||||
|
if (e.button !== 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
candidateTarget = e.target
|
||||||
|
|
||||||
|
// Clear any previous listeners in case of multiple down events, and register
|
||||||
|
// a single mouse up listener
|
||||||
|
document.removeEventListener("mouseup", handleMouseUp)
|
||||||
|
document.addEventListener("mouseup", handleMouseUp, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global singleton listeners for our events
|
||||||
|
document.addEventListener("mousedown", handleMouseDown)
|
||||||
|
document.addEventListener("contextmenu", handleClick)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds or updates a click handler
|
* Adds or updates a click handler
|
||||||
*/
|
*/
|
||||||
const updateHandler = (id, element, anchor, callback, allowedType) => {
|
const updateHandler = (id, element, anchor, callback) => {
|
||||||
let existingHandler = clickHandlers.find(x => x.id === id)
|
let existingHandler = clickHandlers.find(x => x.id === id)
|
||||||
if (!existingHandler) {
|
if (!existingHandler) {
|
||||||
clickHandlers.push({ id, element, anchor, callback, allowedType })
|
clickHandlers.push({ id, element, anchor, callback })
|
||||||
} else {
|
} else {
|
||||||
existingHandler.callback = callback
|
existingHandler.callback = callback
|
||||||
}
|
}
|
||||||
|
@ -82,8 +112,7 @@ export default (element, opts) => {
|
||||||
const callback =
|
const callback =
|
||||||
newOpts?.callback || (typeof newOpts === "function" ? newOpts : null)
|
newOpts?.callback || (typeof newOpts === "function" ? newOpts : null)
|
||||||
const anchor = newOpts?.anchor || element
|
const anchor = newOpts?.anchor || element
|
||||||
const allowedType = newOpts?.allowedType || "click"
|
updateHandler(id, element, anchor, callback)
|
||||||
updateHandler(id, element, anchor, callback, allowedType)
|
|
||||||
}
|
}
|
||||||
update(opts)
|
update(opts)
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -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) {
|
export default function positionDropdown(element, opts) {
|
||||||
let resizeObserver
|
let resizeObserver
|
||||||
let latestOpts = opts
|
let latestOpts = opts
|
||||||
|
@ -19,6 +38,8 @@ export default function positionDropdown(element, opts) {
|
||||||
useAnchorWidth,
|
useAnchorWidth,
|
||||||
offset = 5,
|
offset = 5,
|
||||||
customUpdate,
|
customUpdate,
|
||||||
|
resizable,
|
||||||
|
wrap,
|
||||||
} = opts
|
} = opts
|
||||||
if (!anchor) {
|
if (!anchor) {
|
||||||
return
|
return
|
||||||
|
@ -27,56 +48,159 @@ export default function positionDropdown(element, opts) {
|
||||||
// Compute bounds
|
// Compute bounds
|
||||||
const anchorBounds = anchor.getBoundingClientRect()
|
const anchorBounds = anchor.getBoundingClientRect()
|
||||||
const elementBounds = element.getBoundingClientRect()
|
const elementBounds = element.getBoundingClientRect()
|
||||||
|
const winWidth = window.innerWidth
|
||||||
|
const winHeight = window.innerHeight
|
||||||
|
const screenOffset = 8
|
||||||
let styles = {
|
let styles = {
|
||||||
maxHeight: null,
|
maxHeight,
|
||||||
minWidth,
|
minWidth: useAnchorWidth ? anchorBounds.width : minWidth,
|
||||||
maxWidth,
|
maxWidth: useAnchorWidth ? anchorBounds.width : maxWidth,
|
||||||
left: null,
|
left: null,
|
||||||
top: null,
|
top: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore all our logic for custom logic
|
||||||
if (typeof customUpdate === "function") {
|
if (typeof customUpdate === "function") {
|
||||||
styles = customUpdate(anchorBounds, elementBounds, {
|
styles = customUpdate(anchorBounds, elementBounds, {
|
||||||
...styles,
|
...styles,
|
||||||
offset: opts.offset,
|
offset: opts.offset,
|
||||||
})
|
})
|
||||||
} else {
|
}
|
||||||
// Determine vertical styles
|
|
||||||
if (align === "right-outside" || align === "left-outside") {
|
// Otherwise position ourselves as normal
|
||||||
styles.top =
|
else {
|
||||||
anchorBounds.top + anchorBounds.height / 2 - elementBounds.height / 2
|
// Checks if we overflow off the screen. We only report that we overflow
|
||||||
styles.maxHeight = maxHeight
|
// when the alternative dimension is larger than the one we are checking.
|
||||||
if (styles.top + elementBounds.height > window.innerHeight) {
|
const doesXOverflow = () => {
|
||||||
styles.top = window.innerHeight - elementBounds.height
|
const overflows = styles.left + elementBounds.width > winWidth
|
||||||
}
|
return overflows && anchorBounds.left > winWidth - anchorBounds.right
|
||||||
} else if (
|
}
|
||||||
window.innerHeight - anchorBounds.bottom <
|
const doesYOverflow = () => {
|
||||||
(maxHeight || 100)
|
const overflows = styles.top + elementBounds.height > winHeight
|
||||||
) {
|
return overflows && anchorBounds.top > winHeight - anchorBounds.bottom
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine horizontal styles
|
// Applies a dynamic max height constraint if appropriate
|
||||||
if (!maxWidth && useAnchorWidth) {
|
const applyMaxHeight = height => {
|
||||||
styles.maxWidth = anchorBounds.width
|
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") {
|
if (align === "right") {
|
||||||
styles.left =
|
applyXStrategy(Strategies.EndToEnd)
|
||||||
anchorBounds.left + anchorBounds.width - elementBounds.width
|
|
||||||
} else if (align === "right-outside") {
|
} else if (align === "right-outside") {
|
||||||
styles.left = anchorBounds.right + offset
|
applyXStrategy(Strategies.StartToEnd)
|
||||||
} else if (align === "left-outside") {
|
} else if (align === "left-outside") {
|
||||||
styles.left = anchorBounds.left - elementBounds.width - offset
|
applyXStrategy(Strategies.EndToStart)
|
||||||
} else {
|
} 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>
|
|
|
@ -0,0 +1,252 @@
|
||||||
|
<script>
|
||||||
|
import { cleanInput } from "./utils"
|
||||||
|
import Select from "../../Select.svelte"
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
import NumberInput from "./NumberInput.svelte"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import isoWeek from "dayjs/plugin/isoWeek"
|
||||||
|
|
||||||
|
dayjs.extend(isoWeek)
|
||||||
|
|
||||||
|
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("isoWeek")
|
||||||
|
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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}
|
useAnchorWidth={!autoWidth}
|
||||||
maxWidth={autoWidth ? 400 : null}
|
maxWidth={autoWidth ? 400 : null}
|
||||||
customHeight={customPopoverHeight}
|
customHeight={customPopoverHeight}
|
||||||
|
maxHeight={240}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="popover-content"
|
class="popover-content"
|
||||||
|
|
|
@ -8,7 +8,9 @@ export { default as CoreTextArea } from "./TextArea.svelte"
|
||||||
export { default as CoreCombobox } from "./Combobox.svelte"
|
export { default as CoreCombobox } from "./Combobox.svelte"
|
||||||
export { default as CoreSwitch } from "./Switch.svelte"
|
export { default as CoreSwitch } from "./Switch.svelte"
|
||||||
export { default as CoreSearch } from "./Search.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 CoreDropzone } from "./Dropzone.svelte"
|
||||||
export { default as CoreStepper } from "./Stepper.svelte"
|
export { default as CoreStepper } from "./Stepper.svelte"
|
||||||
export { default as CoreRichTextField } from "./RichTextField.svelte"
|
export { default as CoreRichTextField } from "./RichTextField.svelte"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import Field from "./Field.svelte"
|
import Field from "./Field.svelte"
|
||||||
import DatePicker from "./Core/DatePicker.svelte"
|
import DatePicker from "./Core/DatePicker/DatePicker.svelte"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
export let value = null
|
export let value = null
|
||||||
|
@ -11,22 +11,15 @@
|
||||||
export let error = null
|
export let error = null
|
||||||
export let enableTime = true
|
export let enableTime = true
|
||||||
export let timeOnly = false
|
export let timeOnly = false
|
||||||
export let time24hr = false
|
|
||||||
export let placeholder = null
|
export let placeholder = null
|
||||||
export let appendTo = undefined
|
export let appendTo = undefined
|
||||||
export let ignoreTimezones = false
|
export let ignoreTimezones = false
|
||||||
export let range = false
|
|
||||||
export let helpText = null
|
export let helpText = null
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
if (range) {
|
value = e.detail
|
||||||
// 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
|
|
||||||
}
|
|
||||||
dispatch("change", e.detail)
|
dispatch("change", e.detail)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -40,10 +33,8 @@
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{enableTime}
|
{enableTime}
|
||||||
{timeOnly}
|
{timeOnly}
|
||||||
{time24hr}
|
|
||||||
{appendTo}
|
{appendTo}
|
||||||
{ignoreTimezones}
|
{ignoreTimezones}
|
||||||
{range}
|
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
@ -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>
|
|
@ -7,11 +7,11 @@
|
||||||
export let narrower = false
|
export let narrower = false
|
||||||
export let noPadding = false
|
export let noPadding = false
|
||||||
|
|
||||||
let sidePanelVisble = false
|
let sidePanelVisible = false
|
||||||
|
|
||||||
setContext("side-panel", {
|
setContext("side-panel", {
|
||||||
open: () => (sidePanelVisble = true),
|
open: () => (sidePanelVisible = true),
|
||||||
close: () => (sidePanelVisble = false),
|
close: () => (sidePanelVisible = false),
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -24,9 +24,9 @@
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
id="side-panel"
|
id="side-panel"
|
||||||
class:visible={sidePanelVisble}
|
class:visible={sidePanelVisible}
|
||||||
use:clickOutside={() => {
|
use:clickOutside={() => {
|
||||||
sidePanelVisble = false
|
sidePanelVisible = false
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<slot name="side-panel" />
|
<slot name="side-panel" />
|
||||||
|
|
|
@ -18,13 +18,15 @@
|
||||||
export let open = false
|
export let open = false
|
||||||
export let useAnchorWidth = false
|
export let useAnchorWidth = false
|
||||||
export let dismissible = true
|
export let dismissible = true
|
||||||
export let offset = 5
|
export let offset = 4
|
||||||
export let customHeight
|
export let customHeight
|
||||||
export let animate = true
|
export let animate = true
|
||||||
export let customZindex
|
export let customZindex
|
||||||
export let handlePostionUpdate
|
export let handlePostionUpdate
|
||||||
export let showPopover = true
|
export let showPopover = true
|
||||||
export let clickOutsideOverride = false
|
export let clickOutsideOverride = false
|
||||||
|
export let resizable = true
|
||||||
|
export let wrap = false
|
||||||
|
|
||||||
$: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum"
|
$: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum"
|
||||||
|
|
||||||
|
@ -91,6 +93,8 @@
|
||||||
useAnchorWidth,
|
useAnchorWidth,
|
||||||
offset,
|
offset,
|
||||||
customUpdate: handlePostionUpdate,
|
customUpdate: handlePostionUpdate,
|
||||||
|
resizable,
|
||||||
|
wrap,
|
||||||
}}
|
}}
|
||||||
use:clickOutside={{
|
use:clickOutside={{
|
||||||
callback: dismissible ? handleOutsideClick : () => {},
|
callback: dismissible ? handleOutsideClick : () => {},
|
||||||
|
@ -116,12 +120,11 @@
|
||||||
min-width: var(--spectrum-global-dimension-size-2000);
|
min-width: var(--spectrum-global-dimension-size-2000);
|
||||||
border-color: var(--spectrum-global-color-gray-300);
|
border-color: var(--spectrum-global-color-gray-300);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
transition: opacity 260ms ease-out, transform 260ms ease-out;
|
transition: opacity 260ms ease-out;
|
||||||
}
|
}
|
||||||
.hidden {
|
.hidden {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transform: translateY(-20px);
|
|
||||||
}
|
}
|
||||||
.customZindex {
|
.customZindex {
|
||||||
z-index: var(--customZindex) !important;
|
z-index: var(--customZindex) !important;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { helpers } from "@budibase/shared-core"
|
import { helpers } from "@budibase/shared-core"
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
|
||||||
export const deepGet = helpers.deepGet
|
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
|
// Spectrum icons
|
||||||
import "@spectrum-css/icon/dist/index-vars.css"
|
import "@spectrum-css/icon/dist/index-vars.css"
|
||||||
|
|
||||||
// Components
|
// Form components
|
||||||
export { default as Input } from "./Form/Input.svelte"
|
export { default as Input } from "./Form/Input.svelte"
|
||||||
export { default as Stepper } from "./Form/Stepper.svelte"
|
export { default as Stepper } from "./Form/Stepper.svelte"
|
||||||
export { default as TextArea } from "./Form/TextArea.svelte"
|
export { default as TextArea } from "./Form/TextArea.svelte"
|
||||||
export { default as Select } from "./Form/Select.svelte"
|
export { default as Select } from "./Form/Select.svelte"
|
||||||
export { default as Combobox } from "./Form/Combobox.svelte"
|
export { default as Combobox } from "./Form/Combobox.svelte"
|
||||||
export { default as Dropzone } from "./Form/Dropzone.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 Drawer } from "./Drawer/Drawer.svelte"
|
||||||
export { default as DrawerContent } from "./Drawer/DrawerContent.svelte"
|
export { default as DrawerContent } from "./Drawer/DrawerContent.svelte"
|
||||||
export { default as Avatar } from "./Avatar/Avatar.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 ClearButton } from "./ClearButton/ClearButton.svelte"
|
||||||
export { default as Icon } from "./Icon/Icon.svelte"
|
export { default as Icon } from "./Icon/Icon.svelte"
|
||||||
export { default as IconAvatar } from "./Icon/IconAvatar.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 DetailSummary } from "./DetailSummary/DetailSummary.svelte"
|
||||||
export { default as Popover } from "./Popover/Popover.svelte"
|
export { default as Popover } from "./Popover/Popover.svelte"
|
||||||
export { default as ProgressBar } from "./ProgressBar/ProgressBar.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 Link } from "./Link/Link.svelte"
|
||||||
export { default as Tooltip } from "./Tooltip/Tooltip.svelte"
|
export { default as Tooltip } from "./Tooltip/Tooltip.svelte"
|
||||||
export { default as TempTooltip } from "./Tooltip/TempTooltip.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 TooltipWrapper } from "./Tooltip/TooltipWrapper.svelte"
|
||||||
export { default as Menu } from "./Menu/Menu.svelte"
|
export { default as Menu } from "./Menu/Menu.svelte"
|
||||||
export { default as MenuSection } from "./Menu/Section.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 Notification } from "./Notification/Notification.svelte"
|
||||||
export { default as SideNavigation } from "./SideNavigation/Navigation.svelte"
|
export { default as SideNavigation } from "./SideNavigation/Navigation.svelte"
|
||||||
export { default as SideNavigationItem } from "./SideNavigation/Item.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 Context } from "./context"
|
||||||
export { default as Table } from "./Table/Table.svelte"
|
export { default as Table } from "./Table/Table.svelte"
|
||||||
export { default as Tabs } from "./Tabs/Tabs.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 TreeView } from "./TreeView/Tree.svelte"
|
||||||
export { default as TreeItem } from "./TreeView/Item.svelte"
|
export { default as TreeItem } from "./TreeView/Item.svelte"
|
||||||
export { default as Divider } from "./Divider/Divider.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 Pagination } from "./Pagination/Pagination.svelte"
|
||||||
export { default as Badge } from "./Badge/Badge.svelte"
|
export { default as Badge } from "./Badge/Badge.svelte"
|
||||||
export { default as StatusLight } from "./StatusLight/StatusLight.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 BannerDisplay } from "./Banner/BannerDisplay.svelte"
|
||||||
export { default as MarkdownEditor } from "./Markdown/MarkdownEditor.svelte"
|
export { default as MarkdownEditor } from "./Markdown/MarkdownEditor.svelte"
|
||||||
export { default as MarkdownViewer } from "./Markdown/MarkdownViewer.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 List } from "./List/List.svelte"
|
||||||
export { default as ListItem } from "./List/ListItem.svelte"
|
export { default as ListItem } from "./List/ListItem.svelte"
|
||||||
export { default as IconSideNav } from "./IconSideNav/IconSideNav.svelte"
|
export { default as IconSideNav } from "./IconSideNav/IconSideNav.svelte"
|
||||||
export { default as IconSideNavItem } from "./IconSideNav/IconSideNavItem.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 Accordion } from "./Accordion/Accordion.svelte"
|
||||||
export { default as File } from "./Form/File.svelte"
|
|
||||||
export { default as OptionSelectDnD } from "./OptionSelectDnD/OptionSelectDnD.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
|
// Renderers
|
||||||
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
|
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
|
||||||
export { default as CodeRenderer } from "./Table/CodeRenderer.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 Detail } from "./Typography/Detail.svelte"
|
||||||
export { default as Code } from "./Typography/Code.svelte"
|
export { default as Code } from "./Typography/Code.svelte"
|
||||||
|
|
||||||
// Core form components to be used elsewhere (standard components)
|
|
||||||
export * from "./Form/Core"
|
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
export { default as autoResizeTextArea } from "./Actions/autoresize_textarea"
|
export { default as autoResizeTextArea } from "./Actions/autoresize_textarea"
|
||||||
export { default as positionDropdown } from "./Actions/position_dropdown"
|
export { default as positionDropdown } from "./Actions/position_dropdown"
|
||||||
|
@ -110,6 +114,3 @@ export { banner, BANNER_TYPES } from "./Stores/banner"
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
export * as Helpers from "./helpers"
|
export * as Helpers from "./helpers"
|
||||||
|
|
||||||
// Fancy form components
|
|
||||||
export * from "./FancyForm"
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 804 KiB |
|
@ -32,6 +32,7 @@
|
||||||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
||||||
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
||||||
import BindingSidePanel from "components/common/bindings/BindingSidePanel.svelte"
|
import BindingSidePanel from "components/common/bindings/BindingSidePanel.svelte"
|
||||||
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||||
import { BindingHelpers, BindingType } from "components/common/bindings/utils"
|
import { BindingHelpers, BindingType } from "components/common/bindings/utils"
|
||||||
import {
|
import {
|
||||||
bindingsToCompletions,
|
bindingsToCompletions,
|
||||||
|
@ -356,7 +357,8 @@
|
||||||
value.customType !== "queryParams" &&
|
value.customType !== "queryParams" &&
|
||||||
value.customType !== "cron" &&
|
value.customType !== "cron" &&
|
||||||
value.customType !== "triggerSchema" &&
|
value.customType !== "triggerSchema" &&
|
||||||
value.customType !== "automationFields"
|
value.customType !== "automationFields" &&
|
||||||
|
value.type !== "attachment"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -372,6 +374,15 @@
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const handleAttachmentParams = keyValuObj => {
|
||||||
|
let params = {}
|
||||||
|
if (keyValuObj?.length) {
|
||||||
|
for (let param of keyValuObj) {
|
||||||
|
params[param.url] = param.filename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
|
@ -437,6 +448,33 @@
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
options={Object.keys(table?.schema || {})}
|
options={Object.keys(table?.schema || {})}
|
||||||
/>
|
/>
|
||||||
|
{:else if value.type === "attachment"}
|
||||||
|
<div class="attachment-field-wrapper">
|
||||||
|
<div class="label-wrapper">
|
||||||
|
<Label>{label}</Label>
|
||||||
|
</div>
|
||||||
|
<div class="attachment-field-width">
|
||||||
|
<KeyValueBuilder
|
||||||
|
on:change={e =>
|
||||||
|
onChange(
|
||||||
|
{
|
||||||
|
detail: e.detail.map(({ name, value }) => ({
|
||||||
|
url: name,
|
||||||
|
filename: value,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
key
|
||||||
|
)}
|
||||||
|
object={handleAttachmentParams(inputData[key])}
|
||||||
|
allowJS
|
||||||
|
{bindings}
|
||||||
|
keyBindings
|
||||||
|
customButtonText={"Add attachment"}
|
||||||
|
keyPlaceholder={"URL"}
|
||||||
|
valuePlaceholder={"Filename"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{:else if value.customType === "filters"}
|
{:else if value.customType === "filters"}
|
||||||
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
||||||
<Drawer bind:this={drawer} title="Filtering">
|
<Drawer bind:this={drawer} title="Filtering">
|
||||||
|
@ -651,14 +689,22 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-field {
|
.block-field {
|
||||||
display: flex; /* Use Flexbox */
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex-direction: row; /* Arrange label and field side by side */
|
flex-direction: row;
|
||||||
align-items: center; /* Align vertically in the center */
|
align-items: center;
|
||||||
gap: 10px; /* Add some space between label and field */
|
gap: 10px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.attachment-field-width {
|
||||||
|
margin-top: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-wrapper {
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
.test :global(.drawer) {
|
.test :global(.drawer) {
|
||||||
width: 10000px !important;
|
width: 10000px !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,6 +111,5 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -29,7 +29,11 @@
|
||||||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
||||||
import { getBindings } from "components/backend/DataTable/formula"
|
import { getBindings } from "components/backend/DataTable/formula"
|
||||||
import JSONSchemaModal from "./JSONSchemaModal.svelte"
|
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 RelationshipSelector from "components/common/RelationshipSelector.svelte"
|
||||||
import { RowUtils } from "@budibase/frontend-core"
|
import { RowUtils } from "@budibase/frontend-core"
|
||||||
import ServerBindingPanel from "components/common/bindings/ServerBindingPanel.svelte"
|
import ServerBindingPanel from "components/common/bindings/ServerBindingPanel.svelte"
|
||||||
|
@ -41,8 +45,6 @@
|
||||||
const NUMBER_TYPE = FieldType.NUMBER
|
const NUMBER_TYPE = FieldType.NUMBER
|
||||||
const JSON_TYPE = FieldType.JSON
|
const JSON_TYPE = FieldType.JSON
|
||||||
const DATE_TYPE = FieldType.DATETIME
|
const DATE_TYPE = FieldType.DATETIME
|
||||||
const USER_TYPE = FieldSubtype.USER
|
|
||||||
const USERS_TYPE = FieldSubtype.USERS
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
|
const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
|
||||||
|
@ -263,9 +265,9 @@
|
||||||
delete saveColumn.fieldName
|
delete saveColumn.fieldName
|
||||||
}
|
}
|
||||||
if (isUsersColumn(saveColumn)) {
|
if (isUsersColumn(saveColumn)) {
|
||||||
if (saveColumn.subtype === USER_TYPE) {
|
if (saveColumn.subtype === BBReferenceFieldSubType.USER) {
|
||||||
saveColumn.relationshipType = RelationshipType.ONE_TO_MANY
|
saveColumn.relationshipType = RelationshipType.ONE_TO_MANY
|
||||||
} else if (saveColumn.subtype === USERS_TYPE) {
|
} else if (saveColumn.subtype === BBReferenceFieldSubType.USERS) {
|
||||||
saveColumn.relationshipType = RelationshipType.MANY_TO_MANY
|
saveColumn.relationshipType = RelationshipType.MANY_TO_MANY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -363,19 +365,17 @@
|
||||||
|
|
||||||
function getAllowedTypes() {
|
function getAllowedTypes() {
|
||||||
if (originalName) {
|
if (originalName) {
|
||||||
const possibleTypes = (
|
const possibleTypes = SWITCHABLE_TYPES[field.type] || [
|
||||||
SWITCHABLE_TYPES[field.type] || [editableColumn.type]
|
editableColumn.type,
|
||||||
).map(t => t.toLowerCase())
|
]
|
||||||
return Object.entries(FIELDS)
|
return Object.entries(FIELDS)
|
||||||
.filter(([fieldType]) =>
|
.filter(([_, field]) => possibleTypes.includes(field.type))
|
||||||
possibleTypes.includes(fieldType.toLowerCase())
|
|
||||||
)
|
|
||||||
.map(([_, fieldDefinition]) => fieldDefinition)
|
.map(([_, fieldDefinition]) => fieldDefinition)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUsers =
|
const isUsers =
|
||||||
editableColumn.type === FieldType.BB_REFERENCE &&
|
editableColumn.type === FieldType.BB_REFERENCE &&
|
||||||
editableColumn.subtype === FieldSubtype.USERS
|
editableColumn.subtype === BBReferenceFieldSubType.USERS
|
||||||
|
|
||||||
if (!externalTable) {
|
if (!externalTable) {
|
||||||
return [
|
return [
|
||||||
|
@ -486,7 +486,9 @@
|
||||||
function isUsersColumn(column) {
|
function isUsersColumn(column) {
|
||||||
return (
|
return (
|
||||||
column.type === FieldType.BB_REFERENCE &&
|
column.type === FieldType.BB_REFERENCE &&
|
||||||
[FieldSubtype.USER, FieldSubtype.USERS].includes(column.subtype)
|
[BBReferenceFieldSubType.USER, BBReferenceFieldSubType.USERS].includes(
|
||||||
|
column.subtype
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -514,6 +516,7 @@
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<Select
|
<Select
|
||||||
|
placeholder={null}
|
||||||
disabled={!typeEnabled}
|
disabled={!typeEnabled}
|
||||||
bind:value={editableColumn.fieldId}
|
bind:value={editableColumn.fieldId}
|
||||||
on:change={onHandleTypeChange}
|
on:change={onHandleTypeChange}
|
||||||
|
@ -689,12 +692,14 @@
|
||||||
>
|
>
|
||||||
{:else if isUsersColumn(editableColumn) && datasource?.source !== SourceName.GOOGLE_SHEETS}
|
{:else if isUsersColumn(editableColumn) && datasource?.source !== SourceName.GOOGLE_SHEETS}
|
||||||
<Toggle
|
<Toggle
|
||||||
value={editableColumn.subtype === FieldSubtype.USERS}
|
value={editableColumn.subtype === BBReferenceFieldSubType.USERS}
|
||||||
on:change={e =>
|
on:change={e =>
|
||||||
handleTypeChange(
|
handleTypeChange(
|
||||||
makeFieldId(
|
makeFieldId(
|
||||||
FieldType.BB_REFERENCE,
|
FieldType.BB_REFERENCE,
|
||||||
e.detail ? FieldSubtype.USERS : FieldSubtype.USER
|
e.detail
|
||||||
|
? BBReferenceFieldSubType.USERS
|
||||||
|
: BBReferenceFieldSubType.USER
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
disabled={!isCreating}
|
disabled={!isCreating}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { FieldType, FieldSubtype } from "@budibase/types"
|
import { FieldType, BBReferenceFieldSubType } from "@budibase/types"
|
||||||
import { Select, Toggle, Multiselect } from "@budibase/bbui"
|
import { Select, Toggle, Multiselect } from "@budibase/bbui"
|
||||||
import { DB_TYPE_INTERNAL } from "constants/backend"
|
import { DB_TYPE_INTERNAL } from "constants/backend"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
@ -60,11 +60,11 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "User",
|
label: "User",
|
||||||
value: `${FieldType.BB_REFERENCE}${FieldSubtype.USER}`,
|
value: `${FieldType.BB_REFERENCE}${BBReferenceFieldSubType.USER}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Users",
|
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 {
|
.relationship-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 20px;
|
gap: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
.relationship-part {
|
.relationship-part {
|
||||||
flex-basis: 70%;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.relationship-type {
|
.relationship-type {
|
||||||
flex-basis: 30%;
|
flex: 0 0 128px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import { Body, Label, Input } from "@budibase/bbui"
|
import { Body, Label } from "@budibase/bbui"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
|
|
||||||
export let parameters
|
export let parameters
|
||||||
|
export let bindings
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!parameters.confirm) {
|
if (!parameters.confirm) {
|
||||||
|
@ -15,11 +17,18 @@
|
||||||
<Body size="S">Enter the message you wish to display to the user.</Body>
|
<Body size="S">Enter the message you wish to display to the user.</Body>
|
||||||
<div class="params">
|
<div class="params">
|
||||||
<Label small>Title</Label>
|
<Label small>Title</Label>
|
||||||
<Input placeholder="Prompt User" bind:value={parameters.customTitleText} />
|
<DrawerBindableInput
|
||||||
|
placeholder="Title"
|
||||||
|
value={parameters.customTitleText}
|
||||||
|
on:change={e => (parameters.customTitleText = e.detail)}
|
||||||
|
{bindings}
|
||||||
|
/>
|
||||||
<Label small>Message</Label>
|
<Label small>Message</Label>
|
||||||
<Input
|
<DrawerBindableInput
|
||||||
placeholder="Are you sure you want to continue?"
|
placeholder="Are you sure you want to continue?"
|
||||||
bind:value={parameters.confirmText}
|
value={parameters.confirmText}
|
||||||
|
on:change={e => (parameters.confirmText = e.detail)}
|
||||||
|
{bindings}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -21,26 +21,24 @@
|
||||||
const currentStep = derived(multiStepStore, state => state.currentStep)
|
const currentStep = derived(multiStepStore, state => state.currentStep)
|
||||||
const componentType = "@budibase/standard-components/multistepformblockstep"
|
const componentType = "@budibase/standard-components/multistepformblockstep"
|
||||||
|
|
||||||
|
setContext("multi-step-form-block", multiStepStore)
|
||||||
|
|
||||||
let cachedValue
|
let cachedValue
|
||||||
let cachedInstance = {}
|
let cachedInstance = {}
|
||||||
|
|
||||||
$: if (!isEqual(cachedValue, value)) {
|
$: if (!isEqual(cachedValue, value)) {
|
||||||
cachedValue = value
|
cachedValue = value
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (!isEqual(componentInstance, cachedInstance)) {
|
$: if (!isEqual(componentInstance, cachedInstance)) {
|
||||||
cachedInstance = componentInstance
|
cachedInstance = componentInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
setContext("multi-step-form-block", multiStepStore)
|
|
||||||
|
|
||||||
$: stepCount = cachedValue?.length || 0
|
$: stepCount = cachedValue?.length || 0
|
||||||
$: updateStore(stepCount)
|
$: updateStore(stepCount)
|
||||||
$: dataSource = getDatasourceForProvider($selectedScreen, cachedInstance)
|
$: dataSource = getDatasourceForProvider($selectedScreen, cachedInstance)
|
||||||
$: emitCurrentStep($currentStep)
|
$: emitCurrentStep($currentStep)
|
||||||
$: stepLabel = getStepLabel($multiStepStore)
|
$: stepLabel = getStepLabel($multiStepStore)
|
||||||
$: stepDef = getDefinition(stepLabel)
|
$: stepDef = getDefinition(stepLabel)
|
||||||
$: stepSettings = cachedValue?.[$currentStep] || {}
|
$: savedInstance = cachedValue?.[$currentStep] || {}
|
||||||
$: defaults = Utils.buildMultiStepFormBlockDefaultProps({
|
$: defaults = Utils.buildMultiStepFormBlockDefaultProps({
|
||||||
_id: cachedInstance._id,
|
_id: cachedInstance._id,
|
||||||
stepCount: $multiStepStore.stepCount,
|
stepCount: $multiStepStore.stepCount,
|
||||||
|
@ -48,14 +46,16 @@
|
||||||
actionType: cachedInstance.actionType,
|
actionType: cachedInstance.actionType,
|
||||||
dataSource: cachedInstance.dataSource,
|
dataSource: cachedInstance.dataSource,
|
||||||
})
|
})
|
||||||
|
// For backwards compatibility we need to sometimes manually set base
|
||||||
|
// properties like _id and _component as we didn't used to save these
|
||||||
$: stepInstance = {
|
$: stepInstance = {
|
||||||
_id: Helpers.uuid(),
|
_id: savedInstance._id || Helpers.uuid(),
|
||||||
_component: componentType,
|
_component: savedInstance._component || componentType,
|
||||||
_instanceName: `Step ${currentStep + 1}`,
|
_instanceName: `Step ${currentStep + 1}`,
|
||||||
title: stepSettings.title ?? defaults?.title,
|
title: savedInstance.title ?? defaults?.title,
|
||||||
buttons: stepSettings.buttons || defaults?.buttons,
|
buttons: savedInstance.buttons || defaults?.buttons,
|
||||||
fields: stepSettings.fields,
|
fields: savedInstance.fields,
|
||||||
desc: stepSettings.desc,
|
desc: savedInstance.desc,
|
||||||
|
|
||||||
// Needed for field configuration
|
// Needed for field configuration
|
||||||
dataSource,
|
dataSource,
|
||||||
|
@ -92,7 +92,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const addStep = () => {
|
const addStep = () => {
|
||||||
value = value.toSpliced($currentStep + 1, 0, {})
|
const newInstance = componentStore.createInstance(componentType)
|
||||||
|
value = value.toSpliced($currentStep + 1, 0, newInstance)
|
||||||
dispatch("change", value)
|
dispatch("change", value)
|
||||||
multiStepStore.update(state => ({
|
multiStepStore.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher, getContext } from "svelte"
|
import { createEventDispatcher, getContext } from "svelte"
|
||||||
import { ActionButton } from "@budibase/bbui"
|
import { ActionButton, AbsTooltip } from "@budibase/bbui"
|
||||||
|
|
||||||
const multiStepStore = getContext("multi-step-form-block")
|
const multiStepStore = getContext("multi-step-form-block")
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -28,45 +28,49 @@
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="step-actions">
|
<div class="step-actions">
|
||||||
<ActionButton
|
<AbsTooltip text="Previous step" noWrap>
|
||||||
size="S"
|
<ActionButton
|
||||||
secondary
|
size="S"
|
||||||
icon="ChevronLeft"
|
secondary
|
||||||
disabled={currentStep === 0}
|
icon="ChevronLeft"
|
||||||
on:click={() => {
|
disabled={currentStep === 0}
|
||||||
stepAction("previousStep")
|
on:click={() => {
|
||||||
}}
|
stepAction("previousStep")
|
||||||
tooltip={"Previous step"}
|
}}
|
||||||
/>
|
/>
|
||||||
<ActionButton
|
</AbsTooltip>
|
||||||
size="S"
|
<AbsTooltip text="Next step" noWrap>
|
||||||
secondary
|
<ActionButton
|
||||||
disabled={currentStep === stepCount - 1}
|
size="S"
|
||||||
icon="ChevronRight"
|
secondary
|
||||||
on:click={() => {
|
disabled={currentStep === stepCount - 1}
|
||||||
stepAction("nextStep")
|
icon="ChevronRight"
|
||||||
}}
|
on:click={() => {
|
||||||
tooltip={"Next step"}
|
stepAction("nextStep")
|
||||||
/>
|
}}
|
||||||
<ActionButton
|
/>
|
||||||
size="S"
|
</AbsTooltip>
|
||||||
secondary
|
<AbsTooltip text="Remove step" noWrap>
|
||||||
icon="Close"
|
<ActionButton
|
||||||
disabled={stepCount === 1}
|
size="S"
|
||||||
on:click={() => {
|
secondary
|
||||||
stepAction("removeStep")
|
icon="Close"
|
||||||
}}
|
disabled={stepCount === 1}
|
||||||
tooltip={"Remove step"}
|
on:click={() => {
|
||||||
/>
|
stepAction("removeStep")
|
||||||
<ActionButton
|
}}
|
||||||
size="S"
|
/>
|
||||||
secondary
|
</AbsTooltip>
|
||||||
icon="MultipleAdd"
|
<AbsTooltip text="Add step" noWrap>
|
||||||
on:click={() => {
|
<ActionButton
|
||||||
stepAction("addStep")
|
size="S"
|
||||||
}}
|
secondary
|
||||||
tooltip={"Add step"}
|
icon="MultipleAdd"
|
||||||
/>
|
on:click={() => {
|
||||||
|
stepAction("addStep")
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</AbsTooltip>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
|
@ -75,6 +75,7 @@ const toDraggableListFormat = (gridFormatColumns, createComponent, schema) => {
|
||||||
return createComponent(
|
return createComponent(
|
||||||
"@budibase/standard-components/labelfield",
|
"@budibase/standard-components/labelfield",
|
||||||
{
|
{
|
||||||
|
_id: column.field,
|
||||||
_instanceName: column.field,
|
_instanceName: column.field,
|
||||||
active: column.active,
|
active: column.active,
|
||||||
field: column.field,
|
field: column.field,
|
||||||
|
|
|
@ -65,6 +65,7 @@ describe("getColumns", () => {
|
||||||
it("returns the selected and unselected fields in the modern format, respecting the original order", ctx => {
|
it("returns the selected and unselected fields in the modern format, respecting the original order", ctx => {
|
||||||
expect(ctx.columns.sortable).toEqual([
|
expect(ctx.columns.sortable).toEqual([
|
||||||
{
|
{
|
||||||
|
_id: "three",
|
||||||
_instanceName: "three",
|
_instanceName: "three",
|
||||||
active: true,
|
active: true,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -73,6 +74,7 @@ describe("getColumns", () => {
|
||||||
label: "three label",
|
label: "three label",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
_id: "two",
|
||||||
_instanceName: "two",
|
_instanceName: "two",
|
||||||
active: true,
|
active: true,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -81,6 +83,7 @@ describe("getColumns", () => {
|
||||||
label: "two label",
|
label: "two label",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
_id: "one",
|
||||||
_instanceName: "one",
|
_instanceName: "one",
|
||||||
active: false,
|
active: false,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -91,6 +94,7 @@ describe("getColumns", () => {
|
||||||
])
|
])
|
||||||
|
|
||||||
expect(ctx.columns.primary).toEqual({
|
expect(ctx.columns.primary).toEqual({
|
||||||
|
_id: "four",
|
||||||
_instanceName: "four",
|
_instanceName: "four",
|
||||||
active: true,
|
active: true,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -115,6 +119,7 @@ describe("getColumns", () => {
|
||||||
it("returns all columns, with non-hidden columns automatically selected", ctx => {
|
it("returns all columns, with non-hidden columns automatically selected", ctx => {
|
||||||
expect(ctx.columns.sortable).toEqual([
|
expect(ctx.columns.sortable).toEqual([
|
||||||
{
|
{
|
||||||
|
_id: "two",
|
||||||
_instanceName: "two",
|
_instanceName: "two",
|
||||||
active: true,
|
active: true,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -123,6 +128,7 @@ describe("getColumns", () => {
|
||||||
label: "two",
|
label: "two",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
_id: "three",
|
||||||
_instanceName: "three",
|
_instanceName: "three",
|
||||||
active: true,
|
active: true,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -131,6 +137,7 @@ describe("getColumns", () => {
|
||||||
label: "three",
|
label: "three",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
_id: "one",
|
||||||
_instanceName: "one",
|
_instanceName: "one",
|
||||||
active: false,
|
active: false,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -141,6 +148,7 @@ describe("getColumns", () => {
|
||||||
])
|
])
|
||||||
|
|
||||||
expect(ctx.columns.primary).toEqual({
|
expect(ctx.columns.primary).toEqual({
|
||||||
|
_id: "four",
|
||||||
_instanceName: "four",
|
_instanceName: "four",
|
||||||
active: true,
|
active: true,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -173,6 +181,7 @@ describe("getColumns", () => {
|
||||||
it("returns all columns, including those missing from the initial data", ctx => {
|
it("returns all columns, including those missing from the initial data", ctx => {
|
||||||
expect(ctx.columns.sortable).toEqual([
|
expect(ctx.columns.sortable).toEqual([
|
||||||
{
|
{
|
||||||
|
_id: "three",
|
||||||
_instanceName: "three",
|
_instanceName: "three",
|
||||||
active: true,
|
active: true,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -181,6 +190,7 @@ describe("getColumns", () => {
|
||||||
label: "three label",
|
label: "three label",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
_id: "two",
|
||||||
_instanceName: "two",
|
_instanceName: "two",
|
||||||
active: false,
|
active: false,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -189,6 +199,7 @@ describe("getColumns", () => {
|
||||||
label: "two",
|
label: "two",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
_id: "one",
|
||||||
_instanceName: "one",
|
_instanceName: "one",
|
||||||
active: false,
|
active: false,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -199,6 +210,7 @@ describe("getColumns", () => {
|
||||||
])
|
])
|
||||||
|
|
||||||
expect(ctx.columns.primary).toEqual({
|
expect(ctx.columns.primary).toEqual({
|
||||||
|
_id: "four",
|
||||||
_instanceName: "four",
|
_instanceName: "four",
|
||||||
active: true,
|
active: true,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -228,6 +240,7 @@ describe("getColumns", () => {
|
||||||
it("returns all valid columns, excluding those that aren't valid for the schema", ctx => {
|
it("returns all valid columns, excluding those that aren't valid for the schema", ctx => {
|
||||||
expect(ctx.columns.sortable).toEqual([
|
expect(ctx.columns.sortable).toEqual([
|
||||||
{
|
{
|
||||||
|
_id: "three",
|
||||||
_instanceName: "three",
|
_instanceName: "three",
|
||||||
active: true,
|
active: true,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -236,6 +249,7 @@ describe("getColumns", () => {
|
||||||
label: "three label",
|
label: "three label",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
_id: "two",
|
||||||
_instanceName: "two",
|
_instanceName: "two",
|
||||||
active: false,
|
active: false,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -244,6 +258,7 @@ describe("getColumns", () => {
|
||||||
label: "two",
|
label: "two",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
_id: "one",
|
||||||
_instanceName: "one",
|
_instanceName: "one",
|
||||||
active: false,
|
active: false,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -254,6 +269,7 @@ describe("getColumns", () => {
|
||||||
])
|
])
|
||||||
|
|
||||||
expect(ctx.columns.primary).toEqual({
|
expect(ctx.columns.primary).toEqual({
|
||||||
|
_id: "four",
|
||||||
_instanceName: "four",
|
_instanceName: "four",
|
||||||
active: true,
|
active: true,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -318,6 +334,7 @@ describe("getColumns", () => {
|
||||||
beforeEach(ctx => {
|
beforeEach(ctx => {
|
||||||
ctx.updateSortable([
|
ctx.updateSortable([
|
||||||
{
|
{
|
||||||
|
_id: "three",
|
||||||
_instanceName: "three",
|
_instanceName: "three",
|
||||||
active: true,
|
active: true,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -326,6 +343,7 @@ describe("getColumns", () => {
|
||||||
label: "three",
|
label: "three",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
_id: "one",
|
||||||
_instanceName: "one",
|
_instanceName: "one",
|
||||||
active: true,
|
active: true,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -334,6 +352,7 @@ describe("getColumns", () => {
|
||||||
label: "one",
|
label: "one",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
_id: "two",
|
||||||
_instanceName: "two",
|
_instanceName: "two",
|
||||||
active: false,
|
active: false,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
|
|
@ -35,6 +35,8 @@
|
||||||
export let bindingDrawerLeft
|
export let bindingDrawerLeft
|
||||||
export let allowHelpers = true
|
export let allowHelpers = true
|
||||||
export let customButtonText = null
|
export let customButtonText = null
|
||||||
|
export let keyBindings = false
|
||||||
|
export let allowJS = false
|
||||||
export let compare = (option, value) => option === value
|
export let compare = (option, value) => option === value
|
||||||
|
|
||||||
let fields = Object.entries(object || {}).map(([name, value]) => ({
|
let fields = Object.entries(object || {}).map(([name, value]) => ({
|
||||||
|
@ -116,12 +118,23 @@
|
||||||
class:readOnly-menu={readOnly && showMenu}
|
class:readOnly-menu={readOnly && showMenu}
|
||||||
>
|
>
|
||||||
{#each fields as field, idx}
|
{#each fields as field, idx}
|
||||||
<Input
|
{#if keyBindings}
|
||||||
placeholder={keyPlaceholder}
|
<DrawerBindableInput
|
||||||
readonly={readOnly}
|
{bindings}
|
||||||
bind:value={field.name}
|
placeholder={keyPlaceholder}
|
||||||
on:blur={changed}
|
on:blur={e => {
|
||||||
/>
|
field.name = e.detail
|
||||||
|
changed()
|
||||||
|
}}
|
||||||
|
disabled={readOnly}
|
||||||
|
value={field.name}
|
||||||
|
{allowJS}
|
||||||
|
{allowHelpers}
|
||||||
|
drawerLeft={bindingDrawerLeft}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<Input readonly={readOnly} bind:value={field.name} on:blur={changed} />
|
||||||
|
{/if}
|
||||||
{#if isJsonArray(field.value)}
|
{#if isJsonArray(field.value)}
|
||||||
<Select readonly={true} value="Array" options={["Array"]} />
|
<Select readonly={true} value="Array" options={["Array"]} />
|
||||||
{:else if options}
|
{:else if options}
|
||||||
|
@ -134,14 +147,14 @@
|
||||||
{:else if bindings && bindings.length}
|
{:else if bindings && bindings.length}
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
{bindings}
|
{bindings}
|
||||||
placeholder="Value"
|
placeholder={valuePlaceholder}
|
||||||
on:blur={e => {
|
on:blur={e => {
|
||||||
field.value = e.detail
|
field.value = e.detail
|
||||||
changed()
|
changed()
|
||||||
}}
|
}}
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
allowJS={false}
|
{allowJS}
|
||||||
{allowHelpers}
|
{allowHelpers}
|
||||||
drawerLeft={bindingDrawerLeft}
|
drawerLeft={bindingDrawerLeft}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -25,6 +25,6 @@
|
||||||
name="field"
|
name="field"
|
||||||
headings
|
headings
|
||||||
options={SchemaTypeOptionsExpanded}
|
options={SchemaTypeOptionsExpanded}
|
||||||
compare={(option, value) => option.type === value.type}
|
compare={(option, value) => option.type === value?.type}
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
|
|
|
@ -695,7 +695,7 @@
|
||||||
menuItems={schemaMenuItems}
|
menuItems={schemaMenuItems}
|
||||||
showMenu={!schemaReadOnly}
|
showMenu={!schemaReadOnly}
|
||||||
readOnly={schemaReadOnly}
|
readOnly={schemaReadOnly}
|
||||||
compare={(option, value) => option.type === value.type}
|
compare={(option, value) => option.type === value?.type}
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
<script>
|
||||||
|
import { Modal, ModalContent } from "@budibase/bbui"
|
||||||
|
import FreeTrial from "../../../../assets/FreeTrial.svelte"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import { auth, licensing } from "stores/portal"
|
||||||
|
import { API } from "api"
|
||||||
|
import { PlanType } from "@budibase/types"
|
||||||
|
|
||||||
|
let freeTrialModal
|
||||||
|
|
||||||
|
$: planType = $licensing?.license?.plan?.type
|
||||||
|
$: showFreeTrialModal(planType, freeTrialModal)
|
||||||
|
|
||||||
|
const showFreeTrialModal = (planType, freeTrialModal) => {
|
||||||
|
if (
|
||||||
|
planType === PlanType.ENTERPRISE_BASIC_TRIAL &&
|
||||||
|
!$auth.user?.freeTrialConfirmedAt
|
||||||
|
) {
|
||||||
|
freeTrialModal?.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={freeTrialModal} disableCancel={true}>
|
||||||
|
<ModalContent
|
||||||
|
confirmText="Get started"
|
||||||
|
size="M"
|
||||||
|
showCancelButton={false}
|
||||||
|
showCloseIcon={false}
|
||||||
|
onConfirm={async () => {
|
||||||
|
if (get(auth).user) {
|
||||||
|
try {
|
||||||
|
await API.updateSelf({
|
||||||
|
freeTrialConfirmedAt: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
// Update the cached user
|
||||||
|
await auth.getSelf()
|
||||||
|
} finally {
|
||||||
|
freeTrialModal.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h1>Experience all of Budibase with a free 14-day trial</h1>
|
||||||
|
<div class="free-trial-text">
|
||||||
|
We've upgraded you to a free 14-day trial that allows you to try all our
|
||||||
|
features before deciding which plan is right for you.
|
||||||
|
<p>
|
||||||
|
At the end of your trial, we'll automatically downgrade you to the Free
|
||||||
|
plan unless you choose to upgrade.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<FreeTrial />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
h1 {
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
.free-trial-text {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,6 +1,6 @@
|
||||||
import {
|
import {
|
||||||
FieldType,
|
FieldType,
|
||||||
FieldSubtype,
|
BBReferenceFieldSubType,
|
||||||
INTERNAL_TABLE_SOURCE_ID,
|
INTERNAL_TABLE_SOURCE_ID,
|
||||||
AutoFieldSubType,
|
AutoFieldSubType,
|
||||||
Hosting,
|
Hosting,
|
||||||
|
@ -169,13 +169,13 @@ export const FIELDS = {
|
||||||
USER: {
|
USER: {
|
||||||
name: "User",
|
name: "User",
|
||||||
type: FieldType.BB_REFERENCE,
|
type: FieldType.BB_REFERENCE,
|
||||||
subtype: FieldSubtype.USER,
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
icon: TypeIconMap[FieldType.USER],
|
icon: TypeIconMap[FieldType.USER],
|
||||||
},
|
},
|
||||||
USERS: {
|
USERS: {
|
||||||
name: "Users",
|
name: "Users",
|
||||||
type: FieldType.BB_REFERENCE,
|
type: FieldType.BB_REFERENCE,
|
||||||
subtype: FieldSubtype.USERS,
|
subtype: BBReferenceFieldSubType.USERS,
|
||||||
icon: TypeIconMap[FieldType.USERS],
|
icon: TypeIconMap[FieldType.USERS],
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "array",
|
type: "array",
|
||||||
|
@ -262,6 +262,7 @@ export const SchemaTypeOptions = [
|
||||||
{ label: "Number", value: FieldType.NUMBER },
|
{ label: "Number", value: FieldType.NUMBER },
|
||||||
{ label: "Boolean", value: FieldType.BOOLEAN },
|
{ label: "Boolean", value: FieldType.BOOLEAN },
|
||||||
{ label: "Datetime", value: FieldType.DATETIME },
|
{ label: "Datetime", value: FieldType.DATETIME },
|
||||||
|
{ label: "JSON", value: FieldType.JSON },
|
||||||
]
|
]
|
||||||
|
|
||||||
export const SchemaTypeOptionsExpanded = SchemaTypeOptions.map(el => ({
|
export const SchemaTypeOptionsExpanded = SchemaTypeOptions.map(el => ({
|
||||||
|
|
|
@ -22,6 +22,7 @@ import {
|
||||||
isJSBinding,
|
isJSBinding,
|
||||||
decodeJSBinding,
|
decodeJSBinding,
|
||||||
encodeJSBinding,
|
encodeJSBinding,
|
||||||
|
getJsHelperList,
|
||||||
} from "@budibase/string-templates"
|
} from "@budibase/string-templates"
|
||||||
import { TableNames } from "./constants"
|
import { TableNames } from "./constants"
|
||||||
import { JSONUtils, Constants } from "@budibase/frontend-core"
|
import { JSONUtils, Constants } from "@budibase/frontend-core"
|
||||||
|
@ -1105,50 +1106,51 @@ export const getAllStateVariables = () => {
|
||||||
getAllAssets().forEach(asset => {
|
getAllAssets().forEach(asset => {
|
||||||
findAllMatchingComponents(asset.props, component => {
|
findAllMatchingComponents(asset.props, component => {
|
||||||
const settings = componentStore.getComponentSettings(component._component)
|
const settings = componentStore.getComponentSettings(component._component)
|
||||||
|
const nestedTypes = [
|
||||||
|
"buttonConfiguration",
|
||||||
|
"fieldConfiguration",
|
||||||
|
"stepConfiguration",
|
||||||
|
]
|
||||||
|
|
||||||
|
// Extracts all event settings from a component instance.
|
||||||
|
// Recurses into nested types to find all event-like settings at any
|
||||||
|
// depth.
|
||||||
const parseEventSettings = (settings, comp) => {
|
const parseEventSettings = (settings, comp) => {
|
||||||
|
if (!settings?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract top level event settings
|
||||||
settings
|
settings
|
||||||
.filter(setting => setting.type === "event")
|
.filter(setting => setting.type === "event")
|
||||||
.forEach(setting => {
|
.forEach(setting => {
|
||||||
eventSettings.push(comp[setting.key])
|
eventSettings.push(comp[setting.key])
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
const parseComponentSettings = (settings, component) => {
|
// Recurse into any nested instance types
|
||||||
// Parse the nested button configurations
|
|
||||||
settings
|
settings
|
||||||
.filter(setting => setting.type === "buttonConfiguration")
|
.filter(setting => nestedTypes.includes(setting.type))
|
||||||
.forEach(setting => {
|
.forEach(setting => {
|
||||||
const buttonConfig = component[setting.key]
|
const instances = comp[setting.key]
|
||||||
|
if (Array.isArray(instances) && instances.length) {
|
||||||
|
instances.forEach(instance => {
|
||||||
|
let type = instance?._component
|
||||||
|
|
||||||
if (Array.isArray(buttonConfig)) {
|
// Backwards compatibility for multi-step from blocks which
|
||||||
buttonConfig.forEach(button => {
|
// didn't set a proper component type previously.
|
||||||
const nestedSettings = componentStore.getComponentSettings(
|
if (setting.type === "stepConfiguration" && !type) {
|
||||||
button._component
|
type = "@budibase/standard-components/multistepformblockstep"
|
||||||
)
|
}
|
||||||
parseEventSettings(nestedSettings, button)
|
|
||||||
|
// Parsed nested component instances inside this setting
|
||||||
|
const nestedSettings = componentStore.getComponentSettings(type)
|
||||||
|
parseEventSettings(nestedSettings, instance)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
parseEventSettings(settings, component)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the base component settings
|
parseEventSettings(settings, component)
|
||||||
parseComponentSettings(settings, component)
|
|
||||||
|
|
||||||
// Parse step configuration
|
|
||||||
const stepSetting = settings.find(
|
|
||||||
setting => setting.type === "stepConfiguration"
|
|
||||||
)
|
|
||||||
const steps = stepSetting ? component[stepSetting.key] : []
|
|
||||||
const stepDefinition = componentStore.getComponentSettings(
|
|
||||||
"@budibase/standard-components/multistepformblockstep"
|
|
||||||
)
|
|
||||||
|
|
||||||
steps?.forEach(step => {
|
|
||||||
parseComponentSettings(stepDefinition, step)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1210,9 +1212,32 @@ const shouldReplaceBinding = (currentValue, from, convertTo, binding) => {
|
||||||
if (!currentValue?.includes(from)) {
|
if (!currentValue?.includes(from)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (convertTo === "readableBinding") {
|
// some cases we have the same binding for readable/runtime, specific logic for this
|
||||||
// Dont replace if the value already matches the readable binding
|
const sameBindings = binding.runtimeBinding.includes(binding.readableBinding)
|
||||||
|
const convertingToReadable = convertTo === "readableBinding"
|
||||||
|
const helperNames = Object.keys(getJsHelperList())
|
||||||
|
const matchedHelperNames = helperNames.filter(
|
||||||
|
name => name.includes(from) && currentValue.includes(name)
|
||||||
|
)
|
||||||
|
// edge case - if the binding is part of a helper it may accidentally replace it
|
||||||
|
if (matchedHelperNames.length > 0) {
|
||||||
|
const indexStart = currentValue.indexOf(from),
|
||||||
|
indexEnd = indexStart + from.length
|
||||||
|
for (let helperName of matchedHelperNames) {
|
||||||
|
const helperIndexStart = currentValue.indexOf(helperName),
|
||||||
|
helperIndexEnd = helperIndexStart + helperName.length
|
||||||
|
if (indexStart >= helperIndexStart && indexEnd <= helperIndexEnd) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (convertingToReadable && !sameBindings) {
|
||||||
|
// Don't replace if the value already matches the readable binding
|
||||||
return currentValue.indexOf(binding.readableBinding) === -1
|
return currentValue.indexOf(binding.readableBinding) === -1
|
||||||
|
} else if (convertingToReadable) {
|
||||||
|
// if the runtime and readable bindings are very similar we have to assume it should be replaced
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
// remove all the spaces, if the input is surrounded by spaces e.g. [ Auto ID ] then
|
// remove all the spaces, if the input is surrounded by spaces e.g. [ Auto ID ] then
|
||||||
// this makes sure it is detected
|
// this makes sure it is detected
|
||||||
|
|
|
@ -20,6 +20,9 @@ export function getFormattedPlanName(userPlanType) {
|
||||||
case PlanType.ENTERPRISE:
|
case PlanType.ENTERPRISE:
|
||||||
planName = "Enterprise"
|
planName = "Enterprise"
|
||||||
break
|
break
|
||||||
|
case PlanType.ENTERPRISE_BASIC_TRIAL:
|
||||||
|
planName = "Trial"
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
planName = "Free" // Default to "Free" if the type is not explicitly handled
|
planName = "Free" // Default to "Free" if the type is not explicitly handled
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
import { UserAvatars } from "@budibase/frontend-core"
|
import { UserAvatars } from "@budibase/frontend-core"
|
||||||
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
||||||
import PreviewOverlay from "./_components/PreviewOverlay.svelte"
|
import PreviewOverlay from "./_components/PreviewOverlay.svelte"
|
||||||
|
import EnterpriseBasicTrialModal from "components/portal/onboarding/EnterpriseBasicTrialModal.svelte"
|
||||||
|
|
||||||
export let application
|
export let application
|
||||||
|
|
||||||
|
@ -192,6 +193,8 @@
|
||||||
<CommandPalette />
|
<CommandPalette />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<EnterpriseBasicTrialModal />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.back-to-apps {
|
.back-to-apps {
|
||||||
display: contents;
|
display: contents;
|
||||||
|
|
|
@ -189,6 +189,7 @@
|
||||||
<Select
|
<Select
|
||||||
options={settingOptions}
|
options={settingOptions}
|
||||||
bind:value={condition.setting}
|
bind:value={condition.setting}
|
||||||
|
on:change={() => delete condition.settingValue}
|
||||||
/>
|
/>
|
||||||
<div>TO</div>
|
<div>TO</div>
|
||||||
{#if definition}
|
{#if definition}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DatePicker,
|
|
||||||
Divider,
|
Divider,
|
||||||
Layout,
|
Layout,
|
||||||
notifications,
|
notifications,
|
||||||
|
@ -25,13 +24,13 @@
|
||||||
import BackupsDefault from "assets/backups-default.png"
|
import BackupsDefault from "assets/backups-default.png"
|
||||||
import { BackupTrigger, BackupType } from "constants/backend/backups"
|
import { BackupTrigger, BackupType } from "constants/backend/backups"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
import DateRangePicker from "components/common/DateRangePicker.svelte"
|
||||||
|
|
||||||
let loading = true
|
let loading = true
|
||||||
let backupData = null
|
let backupData = null
|
||||||
let pageInfo = createPaginationStore()
|
let pageInfo = createPaginationStore()
|
||||||
let filterOpt = null
|
let filterOpt = null
|
||||||
let startDate = null
|
let dateRange = []
|
||||||
let endDate = null
|
|
||||||
let filters = [
|
let filters = [
|
||||||
{
|
{
|
||||||
label: "Manual backup",
|
label: "Manual backup",
|
||||||
|
@ -52,7 +51,7 @@
|
||||||
]
|
]
|
||||||
|
|
||||||
$: page = $pageInfo.page
|
$: page = $pageInfo.page
|
||||||
$: fetchBackups(filterOpt, page, startDate, endDate)
|
$: fetchBackups(filterOpt, page, dateRange)
|
||||||
|
|
||||||
let schema = {
|
let schema = {
|
||||||
type: {
|
type: {
|
||||||
|
@ -99,13 +98,13 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchBackups(filters, page, startDate, endDate) {
|
async function fetchBackups(filters, page, dateRange) {
|
||||||
const response = await backups.searchBackups({
|
const response = await backups.searchBackups({
|
||||||
appId: $appStore.appId,
|
appId: $appStore.appId,
|
||||||
...filters,
|
...filters,
|
||||||
page,
|
page,
|
||||||
startDate,
|
startDate: dateRange[0],
|
||||||
endDate,
|
endDate: dateRange[1],
|
||||||
})
|
})
|
||||||
pageInfo.fetched(response.hasNextPage, response.nextPage)
|
pageInfo.fetched(response.hasNextPage, response.nextPage)
|
||||||
|
|
||||||
|
@ -165,7 +164,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await fetchBackups(filterOpt, page, startDate, endDate)
|
await fetchBackups(filterOpt, page, dateRange)
|
||||||
loading = false
|
loading = false
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -207,7 +206,7 @@
|
||||||
View plans
|
View plans
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{:else if !backupData?.length && !loading && !filterOpt && !startDate}
|
{:else if !backupData?.length && !loading && !filterOpt && !dateRange?.length}
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<Layout noPadding gap="S" justifyItems="center">
|
<Layout noPadding gap="S" justifyItems="center">
|
||||||
<img height="130px" src={BackupsDefault} alt="BackupsDefault" />
|
<img height="130px" src={BackupsDefault} alt="BackupsDefault" />
|
||||||
|
@ -236,21 +235,15 @@
|
||||||
bind:value={filterOpt}
|
bind:value={filterOpt}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DatePicker
|
<DateRangePicker
|
||||||
range={true}
|
value={dateRange}
|
||||||
label="Date Range"
|
on:change={e => (dateRange = e.detail)}
|
||||||
on:change={e => {
|
|
||||||
if (e.detail[0].length > 1) {
|
|
||||||
startDate = e.detail[0][0].toISOString()
|
|
||||||
endDate = e.detail[0][1].toISOString()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button cta disabled={loading} on:click={createManualBackup}
|
<Button cta disabled={loading} on:click={createManualBackup}>
|
||||||
>Create new backup</Button
|
Create new backup
|
||||||
>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table">
|
<div class="table">
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
Icon,
|
Icon,
|
||||||
clickOutside,
|
clickOutside,
|
||||||
CoreTextArea,
|
CoreTextArea,
|
||||||
DatePicker,
|
|
||||||
Pagination,
|
Pagination,
|
||||||
Helpers,
|
Helpers,
|
||||||
Divider,
|
Divider,
|
||||||
|
@ -27,6 +26,8 @@
|
||||||
import TimeRenderer from "./_components/TimeRenderer.svelte"
|
import TimeRenderer from "./_components/TimeRenderer.svelte"
|
||||||
import AppColumnRenderer from "./_components/AppColumnRenderer.svelte"
|
import AppColumnRenderer from "./_components/AppColumnRenderer.svelte"
|
||||||
import { cloneDeep } from "lodash"
|
import { cloneDeep } from "lodash"
|
||||||
|
import DateRangePicker from "components/common/DateRangePicker.svelte"
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
date: { width: "0.8fr" },
|
date: { width: "0.8fr" },
|
||||||
|
@ -69,16 +70,13 @@
|
||||||
let sidePanelVisible = false
|
let sidePanelVisible = false
|
||||||
let wideSidePanel = false
|
let wideSidePanel = false
|
||||||
let timer
|
let timer
|
||||||
let startDate = new Date()
|
let dateRange = [dayjs().subtract(30, "days"), dayjs()]
|
||||||
startDate.setDate(startDate.getDate() - 30)
|
|
||||||
let endDate = new Date()
|
|
||||||
|
|
||||||
$: fetchUsers(userPage, userSearchTerm)
|
$: fetchUsers(userPage, userSearchTerm)
|
||||||
$: fetchLogs({
|
$: fetchLogs({
|
||||||
logsPage,
|
logsPage,
|
||||||
logSearchTerm,
|
logSearchTerm,
|
||||||
startDate,
|
dateRange,
|
||||||
endDate,
|
|
||||||
selectedUsers,
|
selectedUsers,
|
||||||
selectedApps,
|
selectedApps,
|
||||||
selectedEvents,
|
selectedEvents,
|
||||||
|
@ -136,8 +134,7 @@
|
||||||
const fetchLogs = async ({
|
const fetchLogs = async ({
|
||||||
logsPage,
|
logsPage,
|
||||||
logSearchTerm,
|
logSearchTerm,
|
||||||
startDate,
|
dateRange,
|
||||||
endDate,
|
|
||||||
selectedUsers,
|
selectedUsers,
|
||||||
selectedApps,
|
selectedApps,
|
||||||
selectedEvents,
|
selectedEvents,
|
||||||
|
@ -155,8 +152,8 @@
|
||||||
logsPageInfo.loading()
|
logsPageInfo.loading()
|
||||||
await auditLogs.search({
|
await auditLogs.search({
|
||||||
bookmark: logsPage,
|
bookmark: logsPage,
|
||||||
startDate,
|
startDate: dateRange[0],
|
||||||
endDate,
|
endDate: dateRange[1],
|
||||||
fullSearch: logSearchTerm,
|
fullSearch: logSearchTerm,
|
||||||
userIds: selectedUsers,
|
userIds: selectedUsers,
|
||||||
appIds: selectedApps,
|
appIds: selectedApps,
|
||||||
|
@ -214,8 +211,8 @@
|
||||||
const downloadLogs = async () => {
|
const downloadLogs = async () => {
|
||||||
try {
|
try {
|
||||||
window.location = auditLogs.getDownloadUrl({
|
window.location = auditLogs.getDownloadUrl({
|
||||||
startDate,
|
startDate: dateRange[0],
|
||||||
endDate,
|
endDate: dateRange[1],
|
||||||
fullSearch: logSearchTerm,
|
fullSearch: logSearchTerm,
|
||||||
userIds: selectedUsers,
|
userIds: selectedUsers,
|
||||||
appIds: selectedApps,
|
appIds: selectedApps,
|
||||||
|
@ -302,22 +299,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="date-picker">
|
<div class="date-picker">
|
||||||
<DatePicker
|
<DateRangePicker
|
||||||
value={[startDate, endDate]}
|
value={dateRange}
|
||||||
placeholder="Choose date range"
|
on:change={e => (dateRange = e.detail)}
|
||||||
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 = ""
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="freeSearch">
|
<div class="freeSearch">
|
||||||
|
@ -488,7 +472,7 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: var(--spacing-l);
|
gap: var(--spacing-l);
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-panel-icons {
|
.side-panel-icons {
|
||||||
|
@ -505,6 +489,13 @@
|
||||||
.date-picker {
|
.date-picker {
|
||||||
flex-basis: calc(70% - 32px);
|
flex-basis: calc(70% - 32px);
|
||||||
min-width: 100px;
|
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 {
|
.freeSearch {
|
||||||
|
|
|
@ -103,6 +103,8 @@ export const createLicensingStore = () => {
|
||||||
const isEnterprisePlan = planType === Constants.PlanType.ENTERPRISE
|
const isEnterprisePlan = planType === Constants.PlanType.ENTERPRISE
|
||||||
const isFreePlan = planType === Constants.PlanType.FREE
|
const isFreePlan = planType === Constants.PlanType.FREE
|
||||||
const isBusinessPlan = planType === Constants.PlanType.BUSINESS
|
const isBusinessPlan = planType === Constants.PlanType.BUSINESS
|
||||||
|
const isEnterpriseTrial =
|
||||||
|
planType === Constants.PlanType.ENTERPRISE_BASIC_TRIAL
|
||||||
const groupsEnabled = license.features.includes(
|
const groupsEnabled = license.features.includes(
|
||||||
Constants.Features.USER_GROUPS
|
Constants.Features.USER_GROUPS
|
||||||
)
|
)
|
||||||
|
@ -143,6 +145,7 @@ export const createLicensingStore = () => {
|
||||||
isEnterprisePlan,
|
isEnterprisePlan,
|
||||||
isFreePlan,
|
isFreePlan,
|
||||||
isBusinessPlan,
|
isBusinessPlan,
|
||||||
|
isEnterpriseTrial,
|
||||||
groupsEnabled,
|
groupsEnabled,
|
||||||
backupsEnabled,
|
backupsEnabled,
|
||||||
brandingEnabled,
|
brandingEnabled,
|
||||||
|
|
|
@ -9,7 +9,7 @@ const {
|
||||||
ObjectStore,
|
ObjectStore,
|
||||||
retrieve,
|
retrieve,
|
||||||
uploadDirectory,
|
uploadDirectory,
|
||||||
makeSureBucketExists,
|
createBucketIfNotExists,
|
||||||
} = objectStore
|
} = objectStore
|
||||||
|
|
||||||
const bucketList = Object.values(ObjectStoreBuckets)
|
const bucketList = Object.values(ObjectStoreBuckets)
|
||||||
|
@ -61,7 +61,7 @@ export async function importObjects() {
|
||||||
let count = 0
|
let count = 0
|
||||||
for (let bucket of buckets) {
|
for (let bucket of buckets) {
|
||||||
const client = ObjectStore(bucket)
|
const client = ObjectStore(bucket)
|
||||||
await makeSureBucketExists(client, bucket)
|
await createBucketIfNotExists(client, bucket)
|
||||||
const files = await uploadDirectory(bucket, join(path, bucket), "/")
|
const files = await uploadDirectory(bucket, join(path, bucket), "/")
|
||||||
count += files.length
|
count += files.length
|
||||||
bar.update(count)
|
bar.update(count)
|
||||||
|
|
|
@ -54,11 +54,9 @@ export async function downloadDockerCompose() {
|
||||||
|
|
||||||
export async function checkDockerConfigured() {
|
export async function checkDockerConfigured() {
|
||||||
const error =
|
const error =
|
||||||
"docker/docker-compose has not been installed, please follow instructions at: https://docs.budibase.com/docs/docker-compose"
|
"docker has not been installed, please follow instructions at: https://docs.budibase.com/docs/docker-compose"
|
||||||
const docker = await lookpath("docker")
|
const docker = await lookpath("docker")
|
||||||
const compose = await lookpath("docker-compose")
|
if (!docker) {
|
||||||
const composeV2 = await lookpath("docker compose")
|
|
||||||
if (!docker || (!compose && !composeV2)) {
|
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3869,12 +3869,6 @@
|
||||||
"key": "timeOnly",
|
"key": "timeOnly",
|
||||||
"defaultValue": false
|
"defaultValue": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"type": "boolean",
|
|
||||||
"label": "24-hour time",
|
|
||||||
"key": "time24hr",
|
|
||||||
"defaultValue": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Ignore time zones",
|
"label": "Ignore time zones",
|
||||||
|
@ -4275,8 +4269,8 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"attachmentfield": {
|
"attachmentfield": {
|
||||||
"name": "Attachment list",
|
"name": "Attachment List",
|
||||||
"icon": "Attach",
|
"icon": "DocumentFragmentGroup",
|
||||||
"styles": ["size"],
|
"styles": ["size"],
|
||||||
"requiredAncestors": ["form"],
|
"requiredAncestors": ["form"],
|
||||||
"editable": true,
|
"editable": true,
|
||||||
|
@ -4373,7 +4367,7 @@
|
||||||
},
|
},
|
||||||
"attachmentsinglefield": {
|
"attachmentsinglefield": {
|
||||||
"name": "Single Attachment",
|
"name": "Single Attachment",
|
||||||
"icon": "Attach",
|
"icon": "DocumentFragment",
|
||||||
"styles": ["size"],
|
"styles": ["size"],
|
||||||
"requiredAncestors": ["form"],
|
"requiredAncestors": ["form"],
|
||||||
"editable": true,
|
"editable": true,
|
||||||
|
@ -6772,7 +6766,20 @@
|
||||||
"illegalChildren": ["section", "sidepanel"],
|
"illegalChildren": ["section", "sidepanel"],
|
||||||
"showEmptyState": false,
|
"showEmptyState": false,
|
||||||
"draggable": false,
|
"draggable": false,
|
||||||
"info": "Side panels are hidden by default. They will only be revealed when triggered by the 'Open Side Panel' action."
|
"info": "Side panels are hidden by default. They will only be revealed when triggered by the 'Open Side Panel' action.",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"key": "ignoreClicksOutside",
|
||||||
|
"label": "Ignore clicks outside",
|
||||||
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"key": "onClose",
|
||||||
|
"label": "On close"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"rowexplorer": {
|
"rowexplorer": {
|
||||||
"block": true,
|
"block": true,
|
||||||
|
@ -7022,6 +7029,12 @@
|
||||||
"key": "stripeRows",
|
"key": "stripeRows",
|
||||||
"defaultValue": false
|
"defaultValue": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Quiet",
|
||||||
|
"key": "quiet",
|
||||||
|
"defaultValue": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": true,
|
"section": true,
|
||||||
"name": "Columns",
|
"name": "Columns",
|
||||||
|
|
|
@ -24,14 +24,7 @@
|
||||||
"@budibase/shared-core": "0.0.0",
|
"@budibase/shared-core": "0.0.0",
|
||||||
"@budibase/string-templates": "0.0.0",
|
"@budibase/string-templates": "0.0.0",
|
||||||
"@budibase/types": "0.0.0",
|
"@budibase/types": "0.0.0",
|
||||||
"@spectrum-css/button": "^3.0.3",
|
"@spectrum-css/card": "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",
|
|
||||||
"apexcharts": "^3.22.1",
|
"apexcharts": "^3.22.1",
|
||||||
"dayjs": "^1.10.8",
|
"dayjs": "^1.10.8",
|
||||||
"downloadjs": "1.4.7",
|
"downloadjs": "1.4.7",
|
||||||
|
@ -42,7 +35,6 @@
|
||||||
"screenfull": "^6.0.1",
|
"screenfull": "^6.0.1",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"svelte-apexcharts": "^1.0.2",
|
"svelte-apexcharts": "^1.0.2",
|
||||||
"svelte-flatpickr": "^3.3.4",
|
|
||||||
"svelte-spa-router": "^4.0.1"
|
"svelte-spa-router": "^4.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -119,140 +119,135 @@
|
||||||
{/if}
|
{/if}
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div
|
{#if dataLoaded}
|
||||||
id="spectrum-root"
|
<div
|
||||||
lang="en"
|
id="spectrum-root"
|
||||||
dir="ltr"
|
lang="en"
|
||||||
class="spectrum spectrum--medium {$themeStore.baseTheme} {$themeStore.theme}"
|
dir="ltr"
|
||||||
class:builder={$builderStore.inBuilder}
|
class="spectrum spectrum--medium {$themeStore.baseTheme} {$themeStore.theme}"
|
||||||
class:show={fontsLoaded && dataLoaded}
|
class:builder={$builderStore.inBuilder}
|
||||||
>
|
class:show={fontsLoaded && dataLoaded}
|
||||||
{#if $environmentStore.maintenance.length > 0}
|
>
|
||||||
<MaintenanceScreen maintenanceList={$environmentStore.maintenance} />
|
{#if $environmentStore.maintenance.length > 0}
|
||||||
{:else}
|
<MaintenanceScreen maintenanceList={$environmentStore.maintenance} />
|
||||||
<DeviceBindingsProvider>
|
{:else}
|
||||||
<UserBindingsProvider>
|
<DeviceBindingsProvider>
|
||||||
<StateBindingsProvider>
|
<UserBindingsProvider>
|
||||||
<RowSelectionProvider>
|
<StateBindingsProvider>
|
||||||
<QueryParamsProvider>
|
<RowSelectionProvider>
|
||||||
<SnippetsProvider>
|
<QueryParamsProvider>
|
||||||
<!-- Settings bar can be rendered outside of device preview -->
|
<SnippetsProvider>
|
||||||
<!-- Key block needs to be outside the if statement or it breaks -->
|
<!-- Settings bar can be rendered outside of device preview -->
|
||||||
{#key $builderStore.selectedComponentId}
|
<!-- Key block needs to be outside the if statement or it breaks -->
|
||||||
{#if $builderStore.inBuilder}
|
{#key $builderStore.selectedComponentId}
|
||||||
<SettingsBar />
|
{#if $builderStore.inBuilder}
|
||||||
{/if}
|
<SettingsBar />
|
||||||
{/key}
|
|
||||||
|
|
||||||
<!-- Clip boundary for selection indicators -->
|
|
||||||
<div
|
|
||||||
id="clip-root"
|
|
||||||
class:preview={$builderStore.inBuilder}
|
|
||||||
class:tablet-preview={$builderStore.previewDevice ===
|
|
||||||
"tablet"}
|
|
||||||
class:mobile-preview={$builderStore.previewDevice ===
|
|
||||||
"mobile"}
|
|
||||||
>
|
|
||||||
<!-- Actual app -->
|
|
||||||
<div id="app-root">
|
|
||||||
{#if showDevTools}
|
|
||||||
<DevToolsHeader />
|
|
||||||
{/if}
|
{/if}
|
||||||
|
{/key}
|
||||||
|
|
||||||
<div id="app-body">
|
<!-- Clip boundary for selection indicators -->
|
||||||
{#if permissionError}
|
<div
|
||||||
<div class="error">
|
id="clip-root"
|
||||||
<Layout justifyItems="center" gap="S">
|
class:preview={$builderStore.inBuilder}
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
class:tablet-preview={$builderStore.previewDevice ===
|
||||||
{@html ErrorSVG}
|
"tablet"}
|
||||||
<Heading size="L">
|
class:mobile-preview={$builderStore.previewDevice ===
|
||||||
You don't have permission to use this app
|
"mobile"}
|
||||||
</Heading>
|
>
|
||||||
<Body size="S">
|
<!-- Actual app -->
|
||||||
Ask your administrator to grant you access
|
<div id="app-root">
|
||||||
</Body>
|
{#if showDevTools}
|
||||||
</Layout>
|
<DevToolsHeader />
|
||||||
</div>
|
|
||||||
{:else if !$screenStore.activeLayout}
|
|
||||||
<div class="error">
|
|
||||||
<Layout justifyItems="center" gap="S">
|
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
||||||
{@html ErrorSVG}
|
|
||||||
<Heading size="L">
|
|
||||||
Something went wrong rendering your app
|
|
||||||
</Heading>
|
|
||||||
<Body size="S">
|
|
||||||
Get in touch with support if this issue persists
|
|
||||||
</Body>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
{:else if embedNoScreens}
|
|
||||||
<div class="error">
|
|
||||||
<Layout justifyItems="center" gap="S">
|
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
||||||
{@html ErrorSVG}
|
|
||||||
<Heading size="L">
|
|
||||||
This Budibase app is not publicly accessible
|
|
||||||
</Heading>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<CustomThemeWrapper>
|
|
||||||
{#key $screenStore.activeLayout._id}
|
|
||||||
<Component
|
|
||||||
isLayout
|
|
||||||
instance={$screenStore.activeLayout.props}
|
|
||||||
/>
|
|
||||||
{/key}
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Flatpickr needs to be inside the theme wrapper.
|
|
||||||
It also needs its own container because otherwise it hijacks
|
|
||||||
key events on the whole page. It is painful to work with.
|
|
||||||
-->
|
|
||||||
<div id="flatpickr-root" />
|
|
||||||
|
|
||||||
<!-- Modal container to ensure they sit on top -->
|
|
||||||
<div class="modal-container" />
|
|
||||||
|
|
||||||
<!-- Layers on top of app -->
|
|
||||||
<NotificationDisplay />
|
|
||||||
<ConfirmationDisplay />
|
|
||||||
<PeekScreenDisplay />
|
|
||||||
</CustomThemeWrapper>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showDevTools}
|
<div id="app-body">
|
||||||
<DevTools />
|
{#if permissionError}
|
||||||
|
<div class="error">
|
||||||
|
<Layout justifyItems="center" gap="S">
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
|
{@html ErrorSVG}
|
||||||
|
<Heading size="L">
|
||||||
|
You don't have permission to use this app
|
||||||
|
</Heading>
|
||||||
|
<Body size="S">
|
||||||
|
Ask your administrator to grant you access
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
{:else if !$screenStore.activeLayout}
|
||||||
|
<div class="error">
|
||||||
|
<Layout justifyItems="center" gap="S">
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
|
{@html ErrorSVG}
|
||||||
|
<Heading size="L">
|
||||||
|
Something went wrong rendering your app
|
||||||
|
</Heading>
|
||||||
|
<Body size="S">
|
||||||
|
Get in touch with support if this issue persists
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
{:else if embedNoScreens}
|
||||||
|
<div class="error">
|
||||||
|
<Layout justifyItems="center" gap="S">
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
|
{@html ErrorSVG}
|
||||||
|
<Heading size="L">
|
||||||
|
This Budibase app is not publicly accessible
|
||||||
|
</Heading>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<CustomThemeWrapper>
|
||||||
|
{#key $screenStore.activeLayout._id}
|
||||||
|
<Component
|
||||||
|
isLayout
|
||||||
|
instance={$screenStore.activeLayout.props}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
|
||||||
|
<!-- Modal container to ensure they sit on top -->
|
||||||
|
<div class="modal-container" />
|
||||||
|
|
||||||
|
<!-- Layers on top of app -->
|
||||||
|
<NotificationDisplay />
|
||||||
|
<ConfirmationDisplay />
|
||||||
|
<PeekScreenDisplay />
|
||||||
|
</CustomThemeWrapper>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if showDevTools}
|
||||||
|
<DevTools />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !$builderStore.inBuilder && $featuresStore.logoEnabled}
|
||||||
|
<FreeFooter />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !$builderStore.inBuilder && $featuresStore.logoEnabled}
|
<!-- Preview and dev tools utilities -->
|
||||||
<FreeFooter />
|
{#if $appStore.isDevApp}
|
||||||
|
<SelectionIndicator />
|
||||||
|
{/if}
|
||||||
|
{#if $builderStore.inBuilder || $devToolsStore.allowSelection}
|
||||||
|
<HoverIndicator />
|
||||||
|
{/if}
|
||||||
|
{#if $builderStore.inBuilder}
|
||||||
|
<DNDHandler />
|
||||||
|
<GridDNDHandler />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</SnippetsProvider>
|
||||||
<!-- Preview and dev tools utilities -->
|
</QueryParamsProvider>
|
||||||
{#if $appStore.isDevApp}
|
</RowSelectionProvider>
|
||||||
<SelectionIndicator />
|
</StateBindingsProvider>
|
||||||
{/if}
|
</UserBindingsProvider>
|
||||||
{#if $builderStore.inBuilder || $devToolsStore.allowSelection}
|
</DeviceBindingsProvider>
|
||||||
<HoverIndicator />
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
{#if $builderStore.inBuilder}
|
<KeyboardManager />
|
||||||
<DNDHandler />
|
{/if}
|
||||||
<GridDNDHandler />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</SnippetsProvider>
|
|
||||||
</QueryParamsProvider>
|
|
||||||
</RowSelectionProvider>
|
|
||||||
</StateBindingsProvider>
|
|
||||||
</UserBindingsProvider>
|
|
||||||
</DeviceBindingsProvider>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<KeyboardManager />
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#spectrum-root {
|
#spectrum-root {
|
||||||
|
|
|
@ -60,16 +60,6 @@
|
||||||
--spectrum-link-primary-m-text-color-hover: var(--primaryColorHover);
|
--spectrum-link-primary-m-text-color-hover: var(--primaryColorHover);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Theme flatpickr */
|
|
||||||
:global(.flatpickr-day.selected) {
|
|
||||||
background: var(--primaryColor);
|
|
||||||
border-color: var(--primaryColor);
|
|
||||||
}
|
|
||||||
:global(.flatpickr-day.selected:hover) {
|
|
||||||
background: var(--primaryColorHover);
|
|
||||||
border-color: var(--primaryColorHover);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom scrollbars */
|
/* Custom scrollbars */
|
||||||
:global(::-webkit-scrollbar) {
|
:global(::-webkit-scrollbar) {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
|
|
|
@ -38,10 +38,8 @@
|
||||||
if (!field || !value) {
|
if (!field || !value) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
let low = dayjs.utc().subtract(1, "year")
|
let low = dayjs.utc().subtract(1, "year")
|
||||||
let high = dayjs.utc().add(1, "day")
|
let high = dayjs.utc().add(1, "day")
|
||||||
|
|
||||||
if (value === "Last 1 day") {
|
if (value === "Last 1 day") {
|
||||||
low = dayjs.utc().subtract(1, "day")
|
low = dayjs.utc().subtract(1, "day")
|
||||||
} else if (value === "Last 7 days") {
|
} else if (value === "Last 7 days") {
|
||||||
|
@ -53,7 +51,6 @@
|
||||||
} else if (value === "Last 6 months") {
|
} else if (value === "Last 6 months") {
|
||||||
low = dayjs.utc().subtract(6, "months")
|
low = dayjs.utc().subtract(6, "months")
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
range: {
|
range: {
|
||||||
[field]: {
|
[field]: {
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
export let allowEditRows = true
|
export let allowEditRows = true
|
||||||
export let allowDeleteRows = true
|
export let allowDeleteRows = true
|
||||||
export let stripeRows = false
|
export let stripeRows = false
|
||||||
|
export let quiet = false
|
||||||
export let initialFilter = null
|
export let initialFilter = null
|
||||||
export let initialSortColumn = null
|
export let initialSortColumn = null
|
||||||
export let initialSortOrder = null
|
export let initialSortOrder = null
|
||||||
|
@ -51,6 +52,8 @@
|
||||||
metadata: { dataSource: table },
|
metadata: { dataSource: table },
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
$: height = $component.styles?.normal?.height || "408px"
|
||||||
|
$: styles = getSanitisedStyles($component.styles)
|
||||||
|
|
||||||
// Provide additional data context for live binding eval
|
// Provide additional data context for live binding eval
|
||||||
export const getAdditionalDataContext = () => {
|
export const getAdditionalDataContext = () => {
|
||||||
|
@ -107,39 +110,48 @@
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getSanitisedStyles = styles => {
|
||||||
|
return {
|
||||||
|
...styles,
|
||||||
|
normal: {
|
||||||
|
...styles?.normal,
|
||||||
|
height: undefined,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div use:styleable={styles} class:in-builder={$builderStore.inBuilder}>
|
||||||
use:styleable={$component.styles}
|
<span style="--height:{height};">
|
||||||
class:in-builder={$builderStore.inBuilder}
|
<Provider {actions}>
|
||||||
>
|
<Grid
|
||||||
<Provider {actions}>
|
bind:this={grid}
|
||||||
<Grid
|
datasource={table}
|
||||||
bind:this={grid}
|
{API}
|
||||||
datasource={table}
|
{stripeRows}
|
||||||
{darkMode}
|
{quiet}
|
||||||
{API}
|
{initialFilter}
|
||||||
{stripeRows}
|
{initialSortColumn}
|
||||||
{initialFilter}
|
{initialSortOrder}
|
||||||
{initialSortColumn}
|
{fixedRowHeight}
|
||||||
{initialSortOrder}
|
{columnWhitelist}
|
||||||
{fixedRowHeight}
|
{schemaOverrides}
|
||||||
{columnWhitelist}
|
{repeat}
|
||||||
{schemaOverrides}
|
canAddRows={allowAddRows}
|
||||||
{repeat}
|
canEditRows={allowEditRows}
|
||||||
canAddRows={allowAddRows}
|
canDeleteRows={allowDeleteRows}
|
||||||
canEditRows={allowEditRows}
|
canEditColumns={false}
|
||||||
canDeleteRows={allowDeleteRows}
|
canExpandRows={false}
|
||||||
canEditColumns={false}
|
canSaveSchema={false}
|
||||||
canExpandRows={false}
|
showControls={false}
|
||||||
canSaveSchema={false}
|
notifySuccess={notificationStore.actions.success}
|
||||||
showControls={false}
|
notifyError={notificationStore.actions.error}
|
||||||
notifySuccess={notificationStore.actions.success}
|
buttons={enrichedButtons}
|
||||||
notifyError={notificationStore.actions.error}
|
on:rowclick={e => onRowClick?.({ row: e.detail })}
|
||||||
buttons={enrichedButtons}
|
/>
|
||||||
on:rowclick={e => onRowClick?.({ row: e.detail })}
|
</Provider>
|
||||||
/>
|
</span>
|
||||||
</Provider>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -150,10 +162,14 @@
|
||||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-height: 230px;
|
|
||||||
height: 410px;
|
|
||||||
}
|
}
|
||||||
div.in-builder :global(*) {
|
div.in-builder :global(*) {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
span {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
span :global(.grid) {
|
||||||
|
height: var(--height);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -73,7 +73,10 @@
|
||||||
$context.device.width,
|
$context.device.width,
|
||||||
$context.device.height
|
$context.device.height
|
||||||
)
|
)
|
||||||
$: autoCloseSidePanel = !$builderStore.inBuilder && $sidePanelStore.open
|
$: autoCloseSidePanel =
|
||||||
|
!$builderStore.inBuilder &&
|
||||||
|
$sidePanelStore.open &&
|
||||||
|
!$sidePanelStore.ignoreClicksOutside
|
||||||
$: screenId = $builderStore.inBuilder
|
$: screenId = $builderStore.inBuilder
|
||||||
? `${$builderStore.screen?._id}-screen`
|
? `${$builderStore.screen?._id}-screen`
|
||||||
: "screen"
|
: "screen"
|
||||||
|
@ -191,6 +194,11 @@
|
||||||
}
|
}
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleClickLink = () => {
|
||||||
|
mobileOpen = false
|
||||||
|
sidePanelStore.actions.close()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
@ -281,7 +289,7 @@
|
||||||
url={navItem.url}
|
url={navItem.url}
|
||||||
subLinks={navItem.subLinks}
|
subLinks={navItem.subLinks}
|
||||||
internalLink={navItem.internalLink}
|
internalLink={navItem.internalLink}
|
||||||
on:clickLink={() => (mobileOpen = false)}
|
on:clickLink={handleClickLink}
|
||||||
leftNav={navigation === "Left"}
|
leftNav={navigation === "Left"}
|
||||||
{mobile}
|
{mobile}
|
||||||
{navStateStore}
|
{navStateStore}
|
||||||
|
@ -316,10 +324,7 @@
|
||||||
<div
|
<div
|
||||||
id="side-panel-container"
|
id="side-panel-container"
|
||||||
class:open={$sidePanelStore.open}
|
class:open={$sidePanelStore.open}
|
||||||
use:clickOutside={{
|
use:clickOutside={autoCloseSidePanel ? sidePanelStore.actions.close : null}
|
||||||
callback: autoCloseSidePanel ? sidePanelStore.actions.close : null,
|
|
||||||
allowedType: "mousedown",
|
|
||||||
}}
|
|
||||||
class:builder={$builderStore.inBuilder}
|
class:builder={$builderStore.inBuilder}
|
||||||
>
|
>
|
||||||
<div class="side-panel-header">
|
<div class="side-panel-header">
|
||||||
|
|
|
@ -5,6 +5,9 @@
|
||||||
const { styleable, sidePanelStore, builderStore, dndIsDragging } =
|
const { styleable, sidePanelStore, builderStore, dndIsDragging } =
|
||||||
getContext("sdk")
|
getContext("sdk")
|
||||||
|
|
||||||
|
export let onClose
|
||||||
|
export let ignoreClicksOutside
|
||||||
|
|
||||||
// Automatically show and hide the side panel when inside the builder.
|
// Automatically show and hide the side panel when inside the builder.
|
||||||
// For some unknown reason, svelte reactivity breaks if we reference the
|
// For some unknown reason, svelte reactivity breaks if we reference the
|
||||||
// reactive variable "open" inside the following expression, or if we define
|
// reactive variable "open" inside the following expression, or if we define
|
||||||
|
@ -26,6 +29,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// $: {
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
// Derive visibility
|
// Derive visibility
|
||||||
$: open = $sidePanelStore.contentId === $component.id
|
$: open = $sidePanelStore.contentId === $component.id
|
||||||
|
|
||||||
|
@ -36,10 +43,17 @@
|
||||||
let renderKey = null
|
let renderKey = null
|
||||||
$: {
|
$: {
|
||||||
if (open) {
|
if (open) {
|
||||||
|
sidePanelStore.actions.setIgnoreClicksOutside(ignoreClicksOutside)
|
||||||
renderKey = Math.random()
|
renderKey = Math.random()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSidePanelClose = async () => {
|
||||||
|
if (onClose) {
|
||||||
|
await onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const showInSidePanel = (el, visible) => {
|
const showInSidePanel = (el, visible) => {
|
||||||
const update = visible => {
|
const update = visible => {
|
||||||
const target = document.getElementById("side-panel-container")
|
const target = document.getElementById("side-panel-container")
|
||||||
|
@ -51,6 +65,7 @@
|
||||||
} else {
|
} else {
|
||||||
if (target.contains(node)) {
|
if (target.contains(node)) {
|
||||||
target.removeChild(node)
|
target.removeChild(node)
|
||||||
|
handleSidePanelClose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,6 @@
|
||||||
readonly={fieldState.readonly}
|
readonly={fieldState.readonly}
|
||||||
error={fieldState.error}
|
error={fieldState.error}
|
||||||
id={fieldState.fieldId}
|
id={fieldState.fieldId}
|
||||||
appendTo={document.getElementById("flatpickr-root")}
|
|
||||||
{enableTime}
|
{enableTime}
|
||||||
{timeOnly}
|
{timeOnly}
|
||||||
{time24hr}
|
{time24hr}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import flatpickr from "flatpickr"
|
import dayjs from "dayjs"
|
||||||
import { FieldTypes } from "../../../constants"
|
import { FieldTypes } from "../../../constants"
|
||||||
|
import { Helpers } from "@budibase/bbui"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a validation function from a combination of schema-level constraints
|
* Creates a validation function from a combination of schema-level constraints
|
||||||
|
@ -81,7 +82,7 @@ export const createValidatorFromConstraints = (
|
||||||
// Date constraint
|
// Date constraint
|
||||||
if (exists(schemaConstraints.datetime?.earliest)) {
|
if (exists(schemaConstraints.datetime?.earliest)) {
|
||||||
const limit = schemaConstraints.datetime.earliest
|
const limit = schemaConstraints.datetime.earliest
|
||||||
const limitString = flatpickr.formatDate(new Date(limit), "F j Y, H:i")
|
const limitString = Helpers.getDateDisplayValue(dayjs(limit))
|
||||||
rules.push({
|
rules.push({
|
||||||
type: "datetime",
|
type: "datetime",
|
||||||
constraint: "minValue",
|
constraint: "minValue",
|
||||||
|
@ -91,7 +92,7 @@ export const createValidatorFromConstraints = (
|
||||||
}
|
}
|
||||||
if (exists(schemaConstraints.datetime?.latest)) {
|
if (exists(schemaConstraints.datetime?.latest)) {
|
||||||
const limit = schemaConstraints.datetime.latest
|
const limit = schemaConstraints.datetime.latest
|
||||||
const limitString = flatpickr.formatDate(new Date(limit), "F j Y, H:i")
|
const limitString = Helpers.getDateDisplayValue(dayjs(limit))
|
||||||
rules.push({
|
rules.push({
|
||||||
type: "datetime",
|
type: "datetime",
|
||||||
constraint: "maxValue",
|
constraint: "maxValue",
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { writable, derived } from "svelte/store"
|
||||||
export const createSidePanelStore = () => {
|
export const createSidePanelStore = () => {
|
||||||
const initialState = {
|
const initialState = {
|
||||||
contentId: null,
|
contentId: null,
|
||||||
|
ignoreClicksOutside: true,
|
||||||
}
|
}
|
||||||
const store = writable(initialState)
|
const store = writable(initialState)
|
||||||
const derivedStore = derived(store, $store => {
|
const derivedStore = derived(store, $store => {
|
||||||
|
@ -32,11 +33,18 @@ export const createSidePanelStore = () => {
|
||||||
}, 50)
|
}, 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setIgnoreClicksOutside = bool => {
|
||||||
|
store.update(state => {
|
||||||
|
state.ignoreClicksOutside = bool
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
subscribe: derivedStore.subscribe,
|
subscribe: derivedStore.subscribe,
|
||||||
actions: {
|
actions: {
|
||||||
open,
|
open,
|
||||||
close,
|
close,
|
||||||
|
setIgnoreClicksOutside,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -240,6 +240,7 @@ const triggerAutomationHandler = async action => {
|
||||||
const navigationHandler = action => {
|
const navigationHandler = action => {
|
||||||
const { url, peek, externalNewTab } = action.parameters
|
const { url, peek, externalNewTab } = action.parameters
|
||||||
routeStore.actions.navigate(url, peek, externalNewTab)
|
routeStore.actions.navigate(url, peek, externalNewTab)
|
||||||
|
closeSidePanelHandler()
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryExecutionHandler = async action => {
|
const queryExecutionHandler = async action => {
|
||||||
|
@ -541,16 +542,22 @@ export const enrichButtonActions = (actions, context) => {
|
||||||
// then execute the rest of the actions in the chain
|
// then execute the rest of the actions in the chain
|
||||||
const result = await callback()
|
const result = await callback()
|
||||||
if (result !== false) {
|
if (result !== false) {
|
||||||
// Generate a new total context to pass into the next enrichment
|
// Generate a new total context for the next enrichment
|
||||||
buttonContext.push(result)
|
buttonContext.push(result)
|
||||||
const newContext = { ...context, actions: buttonContext }
|
const newContext = { ...context, actions: buttonContext }
|
||||||
|
|
||||||
// Enrich and call the next button action if there is more than one action remaining
|
// Enrich and call the next button action if there is more
|
||||||
|
// than one action remaining
|
||||||
const next = enrichButtonActions(
|
const next = enrichButtonActions(
|
||||||
actions.slice(i + 1),
|
actions.slice(i + 1),
|
||||||
newContext
|
newContext
|
||||||
)
|
)
|
||||||
resolve(typeof next === "function" ? await next() : true)
|
if (typeof next === "function") {
|
||||||
|
// Pass the event context back into the new action chain
|
||||||
|
resolve(await next(eventContext))
|
||||||
|
} else {
|
||||||
|
resolve(true)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
resolve(false)
|
resolve(false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
export const buildBackupsEndpoints = API => ({
|
export const buildBackupsEndpoints = API => ({
|
||||||
/**
|
|
||||||
* Gets a list of users in the current tenant.
|
|
||||||
*/
|
|
||||||
searchBackups: async ({ appId, trigger, type, page, startDate, endDate }) => {
|
searchBackups: async ({ appId, trigger, type, page, startDate, endDate }) => {
|
||||||
const opts = {}
|
const opts = {}
|
||||||
if (page) {
|
if (page) {
|
||||||
|
|
|
@ -67,6 +67,11 @@
|
||||||
|
|
||||||
const removeFilter = id => {
|
const removeFilter = id => {
|
||||||
filters = filters.filter(field => field.id !== id)
|
filters = filters.filter(field => field.id !== id)
|
||||||
|
|
||||||
|
// Clear all filters when no fields are specified
|
||||||
|
if (filters.length === 1 && filters[0].onEmptyFilter) {
|
||||||
|
filters = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const duplicateFilter = id => {
|
const duplicateFilter = id => {
|
||||||
|
@ -119,6 +124,7 @@
|
||||||
const fieldSchema = schemaFields.find(x => x.name === filter.field)
|
const fieldSchema = schemaFields.find(x => x.name === filter.field)
|
||||||
filter.type = fieldSchema?.type
|
filter.type = fieldSchema?.type
|
||||||
filter.subtype = fieldSchema?.subtype
|
filter.subtype = fieldSchema?.subtype
|
||||||
|
filter.formulaType = fieldSchema?.formulaType
|
||||||
|
|
||||||
// Update external type based on field
|
// Update external type based on field
|
||||||
filter.externalType = getSchema(filter)?.externalType
|
filter.externalType = getSchema(filter)?.externalType
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount, getContext } from "svelte"
|
import { onMount, getContext } from "svelte"
|
||||||
import { Dropzone } from "@budibase/bbui"
|
import { Dropzone } from "@budibase/bbui"
|
||||||
|
import GridPopover from "../overlays/GridPopover.svelte"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let focused = false
|
export let focused = false
|
||||||
|
@ -8,7 +9,6 @@
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let api
|
export let api
|
||||||
export let invertX = false
|
export let invertX = false
|
||||||
export let invertY = false
|
|
||||||
export let schema
|
export let schema
|
||||||
export let maximum
|
export let maximum
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@
|
||||||
const imageExtensions = ["png", "tiff", "gif", "raw", "jpg", "jpeg"]
|
const imageExtensions = ["png", "tiff", "gif", "raw", "jpg", "jpeg"]
|
||||||
|
|
||||||
let isOpen = false
|
let isOpen = false
|
||||||
|
let anchor
|
||||||
|
|
||||||
$: editable = focused && !readonly
|
$: editable = focused && !readonly
|
||||||
$: {
|
$: {
|
||||||
|
@ -73,7 +74,12 @@
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- 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}
|
{#each value || [] as attachment}
|
||||||
{#if isImage(attachment.extension)}
|
{#if isImage(attachment.extension)}
|
||||||
<img src={attachment.url} alt={attachment.extension} />
|
<img src={attachment.url} alt={attachment.extension} />
|
||||||
|
@ -86,16 +92,24 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<div class="dropzone" class:invertX class:invertY>
|
<GridPopover
|
||||||
<Dropzone
|
open={isOpen}
|
||||||
{value}
|
{anchor}
|
||||||
compact
|
{invertX}
|
||||||
on:change={e => onChange(e.detail)}
|
maxHeight={null}
|
||||||
maximum={maximum || schema.constraints?.length?.maximum}
|
on:close={close}
|
||||||
{processFiles}
|
>
|
||||||
{handleFileTooLarge}
|
<div class="dropzone">
|
||||||
/>
|
<Dropzone
|
||||||
</div>
|
{value}
|
||||||
|
compact
|
||||||
|
on:change={e => onChange(e.detail)}
|
||||||
|
maximum={maximum || schema.constraints?.length?.maximum}
|
||||||
|
{processFiles}
|
||||||
|
{handleFileTooLarge}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</GridPopover>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -129,23 +143,8 @@
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
.dropzone {
|
.dropzone {
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
left: 0;
|
|
||||||
width: 320px;
|
|
||||||
background: var(--grid-background-alt);
|
background: var(--grid-background-alt);
|
||||||
border: var(--cell-border);
|
width: 320px;
|
||||||
padding: var(--cell-padding);
|
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>
|
</style>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import RelationshipCell from "./RelationshipCell.svelte"
|
import RelationshipCell from "./RelationshipCell.svelte"
|
||||||
import { FieldSubtype, RelationshipType } from "@budibase/types"
|
import { BBReferenceFieldSubType, RelationshipType } from "@budibase/types"
|
||||||
|
|
||||||
export let api
|
export let api
|
||||||
|
|
||||||
|
@ -13,13 +13,16 @@
|
||||||
// This is not really used, just adding some content to be able to render the relationship cell
|
// This is not really used, just adding some content to be able to render the relationship cell
|
||||||
tableId: "external",
|
tableId: "external",
|
||||||
relationshipType:
|
relationshipType:
|
||||||
subtype === FieldSubtype.USER
|
subtype === BBReferenceFieldSubType.USER
|
||||||
? RelationshipType.ONE_TO_MANY
|
? RelationshipType.ONE_TO_MANY
|
||||||
: RelationshipType.MANY_TO_MANY,
|
: RelationshipType.MANY_TO_MANY,
|
||||||
}
|
}
|
||||||
|
|
||||||
async function searchFunction(searchParams) {
|
async function searchFunction(searchParams) {
|
||||||
if (subtype !== FieldSubtype.USER && subtype !== FieldSubtype.USERS) {
|
if (
|
||||||
|
subtype !== BBReferenceFieldSubType.USER &&
|
||||||
|
subtype !== BBReferenceFieldSubType.USERS
|
||||||
|
) {
|
||||||
throw `Search for '${subtype}' not implemented`
|
throw `Search for '${subtype}' not implemented`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import dayjs from "dayjs"
|
import { CoreDatePickerPopoverContents, Icon, Helpers } from "@budibase/bbui"
|
||||||
import { CoreDatePicker, Icon } from "@budibase/bbui"
|
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
import GridPopover from "../overlays/GridPopover.svelte"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let schema
|
export let schema
|
||||||
|
@ -9,83 +10,117 @@
|
||||||
export let focused = false
|
export let focused = false
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let api
|
export let api
|
||||||
|
export let invertX = false
|
||||||
|
|
||||||
let flatpickr
|
|
||||||
let isOpen
|
let isOpen
|
||||||
|
let anchor
|
||||||
|
|
||||||
// Adding the 0- will turn a string like 00:00:00 into a valid ISO
|
$: timeOnly = schema?.timeOnly
|
||||||
// date, but will make actual ISO dates invalid
|
$: enableTime = !schema?.dateOnly
|
||||||
$: isTimeValue = !isNaN(new Date(`0-${value}`))
|
$: ignoreTimezones = schema?.ignoreTimezones
|
||||||
$: timeOnly = isTimeValue || schema?.timeOnly
|
|
||||||
$: dateOnly = schema?.dateOnly
|
|
||||||
$: format = timeOnly
|
|
||||||
? "HH:mm:ss"
|
|
||||||
: dateOnly
|
|
||||||
? "MMM D YYYY"
|
|
||||||
: "MMM D YYYY, HH:mm"
|
|
||||||
$: editable = focused && !readonly
|
$: editable = focused && !readonly
|
||||||
$: displayValue = getDisplayValue(value, format, timeOnly, isTimeValue)
|
$: parsedValue = Helpers.parseDate(value, {
|
||||||
|
timeOnly,
|
||||||
const getDisplayValue = (value, format, timeOnly, isTimeValue) => {
|
enableTime,
|
||||||
if (!value) {
|
ignoreTimezones,
|
||||||
return ""
|
})
|
||||||
}
|
$: displayValue = getDisplayValue(parsedValue, timeOnly, enableTime)
|
||||||
// Parse full date strings
|
// Ensure open state matches desired state
|
||||||
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
|
|
||||||
$: {
|
$: {
|
||||||
if (!focused) {
|
if (!focused && isOpen) {
|
||||||
flatpickr?.close()
|
close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onKeyDown = () => {
|
const getDisplayValue = (value, timeOnly, enableTime) => {
|
||||||
return isOpen
|
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(() => {
|
onMount(() => {
|
||||||
api = {
|
api = {
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
focus: () => flatpickr?.open(),
|
focus: open,
|
||||||
blur: () => flatpickr?.close(),
|
blur: close,
|
||||||
isActive: () => isOpen,
|
isActive: () => isOpen,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</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">
|
<div class="value">
|
||||||
{#if value}
|
{displayValue}
|
||||||
{displayValue}
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{#if editable}
|
{#if editable}
|
||||||
<Icon name="Calendar" />
|
<Icon name="Calendar" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if editable}
|
{#if isOpen}
|
||||||
<div class="picker">
|
<GridPopover {anchor} {invertX} maxHeight={null} on:close={close}>
|
||||||
<CoreDatePicker
|
<CoreDatePickerPopoverContents
|
||||||
{value}
|
value={parsedValue}
|
||||||
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)}
|
|
||||||
useKeyboardShortcuts={false}
|
useKeyboardShortcuts={false}
|
||||||
|
on:change={e => (value = e.detail)}
|
||||||
|
{enableTime}
|
||||||
|
{timeOnly}
|
||||||
|
{ignoreTimezones}
|
||||||
/>
|
/>
|
||||||
</div>
|
</GridPopover>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -97,6 +132,10 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
gap: var(--cell-spacing);
|
gap: var(--cell-spacing);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.container.editable:hover {
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.value {
|
.value {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
@ -105,15 +144,6 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
height: 20px;
|
||||||
.picker {
|
|
||||||
position: absolute;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
.picker :global(.flatpickr) {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.picker :global(.spectrum-Textfield-input) {
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -43,6 +43,9 @@
|
||||||
on:mouseup
|
on:mouseup
|
||||||
on:click
|
on:click
|
||||||
on:contextmenu
|
on:contextmenu
|
||||||
|
on:touchstart
|
||||||
|
on:touchend
|
||||||
|
on:touchcancel
|
||||||
{style}
|
{style}
|
||||||
>
|
>
|
||||||
{#if error}
|
{#if error}
|
||||||
|
|
|
@ -1,30 +1,22 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext, onMount, tick } from "svelte"
|
import { getContext, onMount, tick } from "svelte"
|
||||||
import { canBeDisplayColumn, canBeSortColumn } from "@budibase/shared-core"
|
import { canBeDisplayColumn, canBeSortColumn } from "@budibase/shared-core"
|
||||||
import {
|
import { Icon, Menu, MenuItem, Modal } from "@budibase/bbui"
|
||||||
Icon,
|
|
||||||
Popover,
|
|
||||||
Menu,
|
|
||||||
MenuItem,
|
|
||||||
clickOutside,
|
|
||||||
Modal,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import GridCell from "./GridCell.svelte"
|
import GridCell from "./GridCell.svelte"
|
||||||
import { getColumnIcon } from "../lib/utils"
|
import { getColumnIcon } from "../lib/utils"
|
||||||
import MigrationModal from "../controls/MigrationModal.svelte"
|
import MigrationModal from "../controls/MigrationModal.svelte"
|
||||||
import { debounce } from "../../../utils/utils"
|
import { debounce } from "../../../utils/utils"
|
||||||
import { FieldType, FormulaType } from "@budibase/types"
|
import { FieldType, FormulaType } from "@budibase/types"
|
||||||
import { TableNames } from "../../../constants"
|
import { TableNames } from "../../../constants"
|
||||||
|
import GridPopover from "../overlays/GridPopover.svelte"
|
||||||
|
|
||||||
export let column
|
export let column
|
||||||
export let idx
|
export let idx
|
||||||
export let orderable = true
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
reorder,
|
reorder,
|
||||||
isReordering,
|
isReordering,
|
||||||
isResizing,
|
isResizing,
|
||||||
rand,
|
|
||||||
sort,
|
sort,
|
||||||
visibleColumns,
|
visibleColumns,
|
||||||
dispatch,
|
dispatch,
|
||||||
|
@ -53,7 +45,6 @@
|
||||||
let open = false
|
let open = false
|
||||||
let editIsOpen = false
|
let editIsOpen = false
|
||||||
let timeout
|
let timeout
|
||||||
let popover
|
|
||||||
let migrationModal
|
let migrationModal
|
||||||
let searchValue
|
let searchValue
|
||||||
let input
|
let input
|
||||||
|
@ -66,6 +57,12 @@
|
||||||
$: resetSearchValue(column.name)
|
$: resetSearchValue(column.name)
|
||||||
$: searching = searchValue != null
|
$: searching = searchValue != null
|
||||||
$: debouncedUpdateFilter(searchValue)
|
$: debouncedUpdateFilter(searchValue)
|
||||||
|
$: orderable = !column.primaryDisplay
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
open = false
|
||||||
|
editIsOpen = false
|
||||||
|
}
|
||||||
|
|
||||||
const getSortingLabels = type => {
|
const getSortingLabels = type => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
@ -106,29 +103,32 @@
|
||||||
dispatch("edit-column", column.schema)
|
dispatch("edit-column", column.schema)
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancelEdit = () => {
|
|
||||||
popover.hide()
|
|
||||||
editIsOpen = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const onMouseDown = e => {
|
const onMouseDown = e => {
|
||||||
if (e.button === 0 && orderable) {
|
ui.actions.blur()
|
||||||
|
if ((e.touches?.length || e.button === 0) && orderable) {
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
reorder.actions.startReordering(column.name, e)
|
reorder.actions.startReordering(column.name, e)
|
||||||
}, 200)
|
}, 200)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onMouseUp = e => {
|
const onMouseUp = () => {
|
||||||
if (e.button === 0 && orderable) {
|
if (timeout) {
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
|
timeout = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onContextMenu = e => {
|
const onContextMenu = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
ui.actions.blur()
|
|
||||||
open = !open
|
// The timeout allows time for clickoutside to close other open popvers
|
||||||
|
// before we show this one. Without the timeout, this popover closes again
|
||||||
|
// before it's even visible as clickoutside closes it.
|
||||||
|
setTimeout(() => {
|
||||||
|
ui.actions.blur()
|
||||||
|
open = !open
|
||||||
|
}, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortAscending = () => {
|
const sortAscending = () => {
|
||||||
|
@ -236,7 +236,7 @@
|
||||||
}
|
}
|
||||||
const debouncedUpdateFilter = debounce(updateFilter, 250)
|
const debouncedUpdateFilter = debounce(updateFilter, 250)
|
||||||
|
|
||||||
onMount(() => subscribe("close-edit-column", cancelEdit))
|
onMount(() => subscribe("close-edit-column", close))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={migrationModal}>
|
<Modal bind:this={migrationModal}>
|
||||||
|
@ -258,6 +258,9 @@
|
||||||
<GridCell
|
<GridCell
|
||||||
on:mousedown={onMouseDown}
|
on:mousedown={onMouseDown}
|
||||||
on:mouseup={onMouseUp}
|
on:mouseup={onMouseUp}
|
||||||
|
on:touchstart={onMouseDown}
|
||||||
|
on:touchend={onMouseUp}
|
||||||
|
on:touchcancel={onMouseUp}
|
||||||
on:contextmenu={onContextMenu}
|
on:contextmenu={onContextMenu}
|
||||||
width={column.width}
|
width={column.width}
|
||||||
left={column.left}
|
left={column.left}
|
||||||
|
@ -310,88 +313,88 @@
|
||||||
</GridCell>
|
</GridCell>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Popover
|
{#if open}
|
||||||
bind:open
|
<GridPopover
|
||||||
bind:this={popover}
|
{anchor}
|
||||||
{anchor}
|
align="right"
|
||||||
align="right"
|
on:close={close}
|
||||||
offset={0}
|
maxHeight={null}
|
||||||
popoverTarget={document.getElementById(`grid-${rand}`)}
|
resizable
|
||||||
customZindex={50}
|
>
|
||||||
>
|
{#if editIsOpen}
|
||||||
{#if editIsOpen}
|
<div class="content">
|
||||||
<div
|
<slot />
|
||||||
use:clickOutside={() => {
|
</div>
|
||||||
editIsOpen = false
|
{:else}
|
||||||
}}
|
<Menu>
|
||||||
class="content"
|
<MenuItem
|
||||||
>
|
icon="Edit"
|
||||||
<slot />
|
on:click={editColumn}
|
||||||
</div>
|
disabled={!$config.canEditColumns || column.schema.disabled}
|
||||||
{:else}
|
>
|
||||||
<Menu>
|
Edit column
|
||||||
<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
|
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{/if}
|
<MenuItem
|
||||||
</Menu>
|
icon="Duplicate"
|
||||||
{/if}
|
on:click={duplicateColumn}
|
||||||
</Popover>
|
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>
|
<style>
|
||||||
.header-cell {
|
.header-cell {
|
||||||
|
@ -485,7 +488,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
width: 300px;
|
width: 360px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount, tick } from "svelte"
|
import { onMount, tick } from "svelte"
|
||||||
import { clickOutside } from "@budibase/bbui"
|
import { clickOutside } from "@budibase/bbui"
|
||||||
|
import GridPopover from "../overlays/GridPopover.svelte"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let focused = false
|
export let focused = false
|
||||||
|
@ -8,10 +9,10 @@
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let api
|
export let api
|
||||||
export let invertX = false
|
export let invertX = false
|
||||||
export let invertY = false
|
|
||||||
|
|
||||||
let textarea
|
let textarea
|
||||||
let isOpen = false
|
let isOpen = false
|
||||||
|
let anchor
|
||||||
|
|
||||||
$: editable = focused && !readonly
|
$: editable = focused && !readonly
|
||||||
$: {
|
$: {
|
||||||
|
@ -52,25 +53,30 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isOpen}
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<textarea
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
class:invertX
|
<div
|
||||||
class:invertY
|
class="long-form-cell"
|
||||||
bind:this={textarea}
|
on:click={editable ? open : null}
|
||||||
value={value || ""}
|
class:editable
|
||||||
on:change={handleChange}
|
bind:this={anchor}
|
||||||
on:wheel|stopPropagation
|
>
|
||||||
spellcheck="false"
|
<div class="value">
|
||||||
use:clickOutside={close}
|
{value || ""}
|
||||||
/>
|
|
||||||
{: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>
|
|
||||||
</div>
|
</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}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -93,30 +99,20 @@
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
textarea {
|
textarea {
|
||||||
|
border: none;
|
||||||
|
width: 320px;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
height: var(--max-cell-render-overflow);
|
||||||
padding: var(--cell-padding);
|
padding: var(--cell-padding);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border: 2px solid var(--cell-color);
|
|
||||||
background: var(--cell-background);
|
background: var(--cell-background);
|
||||||
font-size: var(--cell-font-size);
|
font-size: var(--cell-font-size);
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
color: inherit;
|
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;
|
z-index: 1;
|
||||||
border-radius: 2px;
|
|
||||||
resize: none;
|
resize: none;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
overflow: auto;
|
||||||
textarea.invertX {
|
|
||||||
left: auto;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
textarea.invertY {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
top: calc(100% + 1px);
|
|
||||||
}
|
}
|
||||||
textarea:focus {
|
textarea:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon, clickOutside } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import { getColor } from "../lib/utils"
|
import { getColor } from "../lib/utils"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
import GridPopover from "../overlays/GridPopover.svelte"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let schema
|
export let schema
|
||||||
|
@ -10,12 +11,12 @@
|
||||||
export let multi = false
|
export let multi = false
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let api
|
export let api
|
||||||
export let invertX = false
|
export let invertX
|
||||||
export let invertY = false
|
|
||||||
export let contentLines = 1
|
export let contentLines = 1
|
||||||
|
|
||||||
let isOpen = false
|
let isOpen = false
|
||||||
let focusedOptionIdx = null
|
let focusedOptionIdx = null
|
||||||
|
let anchor
|
||||||
|
|
||||||
$: options = schema?.constraints?.inclusion || []
|
$: options = schema?.constraints?.inclusion || []
|
||||||
$: optionColors = schema?.optionColors || {}
|
$: optionColors = schema?.optionColors || {}
|
||||||
|
@ -23,7 +24,7 @@
|
||||||
$: values = Array.isArray(value) ? value : [value].filter(x => x != null)
|
$: values = Array.isArray(value) ? value : [value].filter(x => x != null)
|
||||||
$: {
|
$: {
|
||||||
// Close when deselected
|
// Close when deselected
|
||||||
if (!focused) {
|
if (!focused && isOpen) {
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,6 +90,7 @@
|
||||||
class:editable
|
class:editable
|
||||||
class:open
|
class:open
|
||||||
on:click|self={editable ? open : null}
|
on:click|self={editable ? open : null}
|
||||||
|
bind:this={anchor}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="values"
|
class="values"
|
||||||
|
@ -115,16 +117,15 @@
|
||||||
<Icon name="ChevronDown" />
|
<Icon name="ChevronDown" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if isOpen}
|
</div>
|
||||||
<div
|
|
||||||
class="options"
|
{#if isOpen}
|
||||||
class:invertX
|
<GridPopover {anchor} {invertX} on:close={close}>
|
||||||
class:invertY
|
<div class="options">
|
||||||
on:wheel={e => e.stopPropagation()}
|
|
||||||
use:clickOutside={close}
|
|
||||||
>
|
|
||||||
{#each options as option, idx}
|
{#each options as option, idx}
|
||||||
{@const color = optionColors[option] || getOptionColor(option)}
|
{@const color = optionColors[option] || getOptionColor(option)}
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div
|
<div
|
||||||
class="option"
|
class="option"
|
||||||
on:click={() => toggleOption(option)}
|
on:click={() => toggleOption(option)}
|
||||||
|
@ -132,7 +133,9 @@
|
||||||
on:mouseenter={() => (focusedOptionIdx = idx)}
|
on:mouseenter={() => (focusedOptionIdx = idx)}
|
||||||
>
|
>
|
||||||
<div class="badge text" style="--color: {color}">
|
<div class="badge text" style="--color: {color}">
|
||||||
{option}
|
<span>
|
||||||
|
{option}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{#if values.includes(option)}
|
{#if values.includes(option)}
|
||||||
<Icon name="Checkmark" color="var(--accent-color)" />
|
<Icon name="Checkmark" color="var(--accent-color)" />
|
||||||
|
@ -140,8 +143,8 @@
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</GridPopover>
|
||||||
</div>
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.container {
|
.container {
|
||||||
|
@ -211,28 +214,10 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
.options {
|
.options {
|
||||||
min-width: calc(100% + 2px);
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
left: -1px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
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 {
|
.option {
|
||||||
flex: 0 0 var(--default-row-height);
|
flex: 0 0 var(--default-row-height);
|
||||||
|
@ -242,10 +227,10 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--cell-spacing);
|
gap: var(--cell-spacing);
|
||||||
background-color: var(--grid-background-alt);
|
|
||||||
}
|
}
|
||||||
.option:hover,
|
.option:hover,
|
||||||
.option.focused {
|
.option.focused {
|
||||||
background-color: var(--spectrum-global-color-gray-200);
|
background-color: var(--grid-background-alt);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { getColor } from "../lib/utils"
|
import { getColor } from "../lib/utils"
|
||||||
import { onMount, getContext } from "svelte"
|
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 { 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 value
|
||||||
export let api
|
export let api
|
||||||
|
@ -13,7 +14,6 @@
|
||||||
export let schema
|
export let schema
|
||||||
export let onChange
|
export let onChange
|
||||||
export let invertX = false
|
export let invertX = false
|
||||||
export let invertY = false
|
|
||||||
export let contentLines = 1
|
export let contentLines = 1
|
||||||
export let searchFunction = API.searchTable
|
export let searchFunction = API.searchTable
|
||||||
export let primaryDisplay
|
export let primaryDisplay
|
||||||
|
@ -27,15 +27,15 @@
|
||||||
let candidateIndex
|
let candidateIndex
|
||||||
let lastSearchId
|
let lastSearchId
|
||||||
let searching = false
|
let searching = false
|
||||||
let valuesHeight = 0
|
|
||||||
let container
|
let container
|
||||||
|
let anchor
|
||||||
|
|
||||||
$: oneRowOnly = schema?.relationshipType === "one-to-many"
|
$: oneRowOnly = schema?.relationshipType === "one-to-many"
|
||||||
$: editable = focused && !readonly
|
$: editable = focused && !readonly
|
||||||
$: lookupMap = buildLookupMap(value, isOpen)
|
$: lookupMap = buildLookupMap(value, isOpen)
|
||||||
$: debouncedSearch(searchString)
|
$: debouncedSearch(searchString)
|
||||||
$: {
|
$: {
|
||||||
if (!focused) {
|
if (!focused && isOpen) {
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -125,7 +125,6 @@
|
||||||
|
|
||||||
const open = async () => {
|
const open = async () => {
|
||||||
isOpen = true
|
isOpen = true
|
||||||
valuesHeight = container.getBoundingClientRect().height
|
|
||||||
|
|
||||||
// Find the primary display for the related table
|
// Find the primary display for the related table
|
||||||
if (!primaryDisplay) {
|
if (!primaryDisplay) {
|
||||||
|
@ -204,14 +203,6 @@
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
|
|
||||||
const showRelationship = async id => {
|
|
||||||
const relatedRow = await API.fetchRow({
|
|
||||||
tableId: schema.tableId,
|
|
||||||
rowId: id,
|
|
||||||
})
|
|
||||||
dispatch("edit-row", relatedRow)
|
|
||||||
}
|
|
||||||
|
|
||||||
const readable = value => {
|
const readable = value => {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return ""
|
return ""
|
||||||
|
@ -238,8 +229,8 @@
|
||||||
class="wrapper"
|
class="wrapper"
|
||||||
class:editable
|
class:editable
|
||||||
class:focused
|
class:focused
|
||||||
class:invertY
|
|
||||||
style="--color:{color};"
|
style="--color:{color};"
|
||||||
|
bind:this={anchor}
|
||||||
>
|
>
|
||||||
<div class="container" bind:this={container}>
|
<div class="container" bind:this={container}>
|
||||||
<div
|
<div
|
||||||
|
@ -250,11 +241,7 @@
|
||||||
{#each value || [] as relationship}
|
{#each value || [] as relationship}
|
||||||
{#if relationship[primaryDisplay] || relationship.primaryDisplay}
|
{#if relationship[primaryDisplay] || relationship.primaryDisplay}
|
||||||
<div class="badge">
|
<div class="badge">
|
||||||
<span
|
<span>
|
||||||
on:click={editable
|
|
||||||
? () => showRelationship(relationship._id)
|
|
||||||
: null}
|
|
||||||
>
|
|
||||||
{readable(
|
{readable(
|
||||||
relationship[primaryDisplay] || relationship.primaryDisplay
|
relationship[primaryDisplay] || relationship.primaryDisplay
|
||||||
)}
|
)}
|
||||||
|
@ -282,16 +269,13 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if isOpen}
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
class="dropdown"
|
{#if isOpen}
|
||||||
class:invertX
|
<GridPopover open={isOpen} {anchor} {invertX} on:close={close}>
|
||||||
class:invertY
|
<div class="dropdown" on:wheel|stopPropagation>
|
||||||
on:wheel|stopPropagation
|
|
||||||
use:clickOutside={close}
|
|
||||||
style="--values-height:{valuesHeight}px;"
|
|
||||||
>
|
|
||||||
<div class="search">
|
<div class="search">
|
||||||
<Input
|
<Input
|
||||||
autofocus
|
autofocus
|
||||||
|
@ -327,8 +311,8 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</GridPopover>
|
||||||
</div>
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.wrapper {
|
.wrapper {
|
||||||
|
@ -337,7 +321,6 @@
|
||||||
min-height: var(--row-height);
|
min-height: var(--row-height);
|
||||||
max-height: var(--row-height);
|
max-height: var(--row-height);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
--max-relationship-height: 96px;
|
|
||||||
}
|
}
|
||||||
.wrapper.focused {
|
.wrapper.focused {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -349,10 +332,6 @@
|
||||||
max-height: none;
|
max-height: none;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
.wrapper.invertY {
|
|
||||||
top: auto;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
min-height: var(--row-height);
|
min-height: var(--row-height);
|
||||||
|
@ -363,7 +342,6 @@
|
||||||
.focused .container {
|
.focused .container {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
max-height: var(--max-relationship-height);
|
|
||||||
}
|
}
|
||||||
.focused .container:after {
|
.focused .container:after {
|
||||||
content: " ";
|
content: " ";
|
||||||
|
@ -426,10 +404,6 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
.editable .values .badge span:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add {
|
.add {
|
||||||
background: var(--spectrum-global-color-gray-200);
|
background: var(--spectrum-global-color-gray-200);
|
||||||
|
@ -446,30 +420,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown {
|
.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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
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 {
|
.searching {
|
||||||
|
@ -497,7 +450,8 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.result .badge {
|
.result .badge {
|
||||||
max-width: calc(100% - 30px);
|
flex: 1 1 auto;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
|
@ -505,7 +459,6 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 4px var(--cell-padding);
|
margin: 4px var(--cell-padding);
|
||||||
width: calc(100% - 2 * var(--cell-padding));
|
|
||||||
}
|
}
|
||||||
.search :global(.spectrum-Textfield) {
|
.search :global(.spectrum-Textfield) {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
|
@ -81,6 +81,7 @@
|
||||||
size="S"
|
size="S"
|
||||||
value={column.visible}
|
value={column.visible}
|
||||||
on:change={e => toggleVisibility(column, e.detail)}
|
on:change={e => toggleVisibility(column, e.detail)}
|
||||||
|
disabled={column.primaryDisplay}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,7 +7,11 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { ValidColumnNameRegex } from "@budibase/shared-core"
|
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")
|
const { API, definition, rows } = getContext("grid")
|
||||||
|
|
||||||
|
@ -29,9 +33,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const migrateUserColumn = async () => {
|
const migrateUserColumn = async () => {
|
||||||
let subtype = FieldSubtype.USERS
|
let subtype = BBReferenceFieldSubType.USERS
|
||||||
if (column.schema.relationshipType === RelationshipType.ONE_TO_MANY) {
|
if (column.schema.relationshipType === RelationshipType.ONE_TO_MANY) {
|
||||||
subtype = FieldSubtype.USER
|
subtype = BBReferenceFieldSubType.USER
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
import GridBody from "./GridBody.svelte"
|
import GridBody from "./GridBody.svelte"
|
||||||
import ResizeOverlay from "../overlays/ResizeOverlay.svelte"
|
import ResizeOverlay from "../overlays/ResizeOverlay.svelte"
|
||||||
import ReorderOverlay from "../overlays/ReorderOverlay.svelte"
|
import ReorderOverlay from "../overlays/ReorderOverlay.svelte"
|
||||||
|
import PopoverOverlay from "../overlays/PopoverOverlay.svelte"
|
||||||
import HeaderRow from "./HeaderRow.svelte"
|
import HeaderRow from "./HeaderRow.svelte"
|
||||||
import ScrollOverlay from "../overlays/ScrollOverlay.svelte"
|
import ScrollOverlay from "../overlays/ScrollOverlay.svelte"
|
||||||
import MenuOverlay from "../overlays/MenuOverlay.svelte"
|
import MenuOverlay from "../overlays/MenuOverlay.svelte"
|
||||||
|
@ -22,10 +23,12 @@
|
||||||
import NewRow from "./NewRow.svelte"
|
import NewRow from "./NewRow.svelte"
|
||||||
import { createGridWebsocket } from "../lib/websocket"
|
import { createGridWebsocket } from "../lib/websocket"
|
||||||
import {
|
import {
|
||||||
MaxCellRenderHeight,
|
MaxCellRenderOverflow,
|
||||||
MaxCellRenderWidthOverflow,
|
|
||||||
GutterWidth,
|
GutterWidth,
|
||||||
DefaultRowHeight,
|
DefaultRowHeight,
|
||||||
|
Padding,
|
||||||
|
SmallRowHeight,
|
||||||
|
ControlsHeight,
|
||||||
} from "../lib/constants"
|
} from "../lib/constants"
|
||||||
|
|
||||||
export let API = null
|
export let API = null
|
||||||
|
@ -39,6 +42,7 @@
|
||||||
export let canEditColumns = true
|
export let canEditColumns = true
|
||||||
export let canSaveSchema = true
|
export let canSaveSchema = true
|
||||||
export let stripeRows = false
|
export let stripeRows = false
|
||||||
|
export let quiet = false
|
||||||
export let collaboration = true
|
export let collaboration = true
|
||||||
export let showAvatars = true
|
export let showAvatars = true
|
||||||
export let showControls = true
|
export let showControls = true
|
||||||
|
@ -52,7 +56,7 @@
|
||||||
export let darkMode
|
export let darkMode
|
||||||
|
|
||||||
// Unique identifier for DOM nodes inside this instance
|
// 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
|
// Store props in a store for reference in other stores
|
||||||
const props = writable($$props)
|
const props = writable($$props)
|
||||||
|
@ -60,7 +64,7 @@
|
||||||
// Build up context
|
// Build up context
|
||||||
let context = {
|
let context = {
|
||||||
API: API || createAPIClient(),
|
API: API || createAPIClient(),
|
||||||
rand,
|
gridID,
|
||||||
props,
|
props,
|
||||||
}
|
}
|
||||||
context = { ...context, ...createEventManagers() }
|
context = { ...context, ...createEventManagers() }
|
||||||
|
@ -92,6 +96,7 @@
|
||||||
canEditColumns,
|
canEditColumns,
|
||||||
canSaveSchema,
|
canSaveSchema,
|
||||||
stripeRows,
|
stripeRows,
|
||||||
|
quiet,
|
||||||
collaboration,
|
collaboration,
|
||||||
showAvatars,
|
showAvatars,
|
||||||
showControls,
|
showControls,
|
||||||
|
@ -104,6 +109,8 @@
|
||||||
buttons,
|
buttons,
|
||||||
darkMode,
|
darkMode,
|
||||||
})
|
})
|
||||||
|
$: minHeight =
|
||||||
|
Padding + SmallRowHeight + $rowHeight + (showControls ? ControlsHeight : 0)
|
||||||
|
|
||||||
// Set context for children to consume
|
// Set context for children to consume
|
||||||
setContext("grid", context)
|
setContext("grid", context)
|
||||||
|
@ -122,13 +129,14 @@
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div
|
<div
|
||||||
class="grid"
|
class="grid"
|
||||||
id="grid-{rand}"
|
id={gridID}
|
||||||
class:is-resizing={$isResizing}
|
class:is-resizing={$isResizing}
|
||||||
class:is-reordering={$isReordering}
|
class:is-reordering={$isReordering}
|
||||||
class:stripe={stripeRows}
|
class:stripe={stripeRows}
|
||||||
|
class:quiet
|
||||||
on:mouseenter={() => gridFocused.set(true)}
|
on:mouseenter={() => gridFocused.set(true)}
|
||||||
on:mouseleave={() => gridFocused.set(false)}
|
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}
|
{#if showControls}
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
@ -180,6 +188,7 @@
|
||||||
<ReorderOverlay />
|
<ReorderOverlay />
|
||||||
<ScrollOverlay />
|
<ScrollOverlay />
|
||||||
<MenuOverlay />
|
<MenuOverlay />
|
||||||
|
<PopoverOverlay />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -209,7 +218,6 @@
|
||||||
--cell-spacing: 4px;
|
--cell-spacing: 4px;
|
||||||
--cell-border: 1px solid var(--spectrum-global-color-gray-200);
|
--cell-border: 1px solid var(--spectrum-global-color-gray-200);
|
||||||
--cell-font-size: 14px;
|
--cell-font-size: 14px;
|
||||||
--controls-height: 50px;
|
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -218,6 +226,7 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--grid-background);
|
background: var(--grid-background);
|
||||||
|
min-height: var(--min-height);
|
||||||
}
|
}
|
||||||
.grid,
|
.grid,
|
||||||
.grid :global(*) {
|
.grid :global(*) {
|
||||||
|
@ -333,4 +342,9 @@
|
||||||
.grid-data-outer :global(.spectrum-Checkbox-partialCheckmark) {
|
.grid-data-outer :global(.spectrum-Checkbox-partialCheckmark) {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Overrides */
|
||||||
|
.grid.quiet :global(.grid-data-content .row > .cell:not(:last-child)) {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
bounds,
|
bounds,
|
||||||
hoveredRowId,
|
hoveredRowId,
|
||||||
menu,
|
menu,
|
||||||
|
focusedCellAPI,
|
||||||
} = getContext("grid")
|
} = getContext("grid")
|
||||||
|
|
||||||
export let scrollVertically = false
|
export let scrollVertically = false
|
||||||
|
@ -35,6 +36,9 @@
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
updateScroll(e.deltaX, e.deltaY, e.clientY)
|
updateScroll(e.deltaX, e.deltaY, e.clientY)
|
||||||
|
|
||||||
|
// Close any open popovers when scrolling
|
||||||
|
$focusedCellAPI?.blur()
|
||||||
|
|
||||||
// If a context menu was visible, hide it
|
// If a context menu was visible, hide it
|
||||||
if ($menu.visible) {
|
if ($menu.visible) {
|
||||||
menu.actions.close()
|
menu.actions.close()
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext, onMount } from "svelte"
|
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 anchor
|
||||||
let open = false
|
let isOpen = false
|
||||||
|
|
||||||
$: columnsWidth = $visibleColumns.reduce(
|
$: columnsWidth = $visibleColumns.reduce(
|
||||||
(total, col) => (total += col.width),
|
(total, col) => (total += col.width),
|
||||||
|
@ -14,8 +15,13 @@
|
||||||
$: end = columnsWidth - 1 - $scroll.left
|
$: end = columnsWidth - 1 - $scroll.left
|
||||||
$: left = Math.min($width - 40, end)
|
$: left = Math.min($width - 40, end)
|
||||||
|
|
||||||
|
const open = () => {
|
||||||
|
ui.actions.blur()
|
||||||
|
isOpen = true
|
||||||
|
}
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
open = false
|
isOpen = false
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => subscribe("close-edit-column", close))
|
onMount(() => subscribe("close-edit-column", close))
|
||||||
|
@ -28,27 +34,23 @@
|
||||||
bind:this={anchor}
|
bind:this={anchor}
|
||||||
class="add"
|
class="add"
|
||||||
style="left:{left}px"
|
style="left:{left}px"
|
||||||
on:click={() => (open = true)}
|
on:click={open}
|
||||||
>
|
>
|
||||||
<Icon name="Add" />
|
<Icon name="Add" />
|
||||||
</div>
|
</div>
|
||||||
<Popover
|
{#if isOpen}
|
||||||
bind:open
|
<GridPopover
|
||||||
{anchor}
|
{anchor}
|
||||||
align={$visibleColumns.length ? "right" : "left"}
|
align={$visibleColumns.length ? "right" : "left"}
|
||||||
offset={0}
|
on:close={close}
|
||||||
popoverTarget={document.getElementById(`add-column-button`)}
|
maxHeight={null}
|
||||||
customZindex={50}
|
resizable
|
||||||
>
|
|
||||||
<div
|
|
||||||
use:clickOutside={() => {
|
|
||||||
open = false
|
|
||||||
}}
|
|
||||||
class="content"
|
|
||||||
>
|
>
|
||||||
<slot />
|
<div class="content">
|
||||||
</div>
|
<slot />
|
||||||
</Popover>
|
</div>
|
||||||
|
</GridPopover>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.add {
|
.add {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue