Merge branch 'master' into BUDI-7580/account_portal_submodule
This commit is contained in:
commit
9c9f45436f
|
@ -99,11 +99,6 @@ jobs:
|
||||||
else
|
else
|
||||||
yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro
|
yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro
|
||||||
fi
|
fi
|
||||||
- uses: codecov/codecov-action@v3
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
|
||||||
name: codecov-umbrella
|
|
||||||
verbose: true
|
|
||||||
|
|
||||||
test-worker:
|
test-worker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -129,12 +124,6 @@ jobs:
|
||||||
yarn test --scope=@budibase/worker
|
yarn test --scope=@budibase/worker
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- uses: codecov/codecov-action@v3
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN || github.token }} # not required for public repos
|
|
||||||
name: codecov-umbrella
|
|
||||||
verbose: true
|
|
||||||
|
|
||||||
test-server:
|
test-server:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
@ -159,12 +148,6 @@ jobs:
|
||||||
yarn test --scope=@budibase/server
|
yarn test --scope=@budibase/server
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- uses: codecov/codecov-action@v3
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN || github.token }} # not required for public repos
|
|
||||||
name: codecov-umbrella
|
|
||||||
verbose: true
|
|
||||||
|
|
||||||
test-pro:
|
test-pro:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||||
|
|
|
@ -10,6 +10,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v8
|
- uses: actions/stale@v8
|
||||||
with:
|
with:
|
||||||
|
days-before-stale: 330
|
||||||
operations-per-run: 1
|
operations-per-run: 1
|
||||||
# stale rules for PRs
|
# stale rules for PRs
|
||||||
days-before-pr-stale: 7
|
days-before-pr-stale: 7
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
/packages/server @Budibase/backend
|
||||||
|
/packages/worker @Budibase/backend
|
||||||
|
/packages/backend-core @Budibase/backend
|
|
@ -7,8 +7,8 @@ metadata:
|
||||||
kubernetes.io/ingress.class: alb
|
kubernetes.io/ingress.class: alb
|
||||||
alb.ingress.kubernetes.io/scheme: internet-facing
|
alb.ingress.kubernetes.io/scheme: internet-facing
|
||||||
alb.ingress.kubernetes.io/target-type: ip
|
alb.ingress.kubernetes.io/target-type: ip
|
||||||
alb.ingress.kubernetes.io/success-codes: 200,301
|
alb.ingress.kubernetes.io/success-codes: '200'
|
||||||
alb.ingress.kubernetes.io/healthcheck-path: /
|
alb.ingress.kubernetes.io/healthcheck-path: '/health'
|
||||||
{{- if .Values.ingress.certificateArn }}
|
{{- if .Values.ingress.certificateArn }}
|
||||||
alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}'
|
alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}'
|
||||||
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
|
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
|
||||||
|
|
|
@ -26,27 +26,48 @@ if [[ "${TARGETBUILD}" = "aas" ]]; then
|
||||||
sed -i "s#DATA_DIR#/home#g" /opt/clouseau/clouseau.ini
|
sed -i "s#DATA_DIR#/home#g" /opt/clouseau/clouseau.ini
|
||||||
sed -i "s#DATA_DIR#/home#g" /opt/couchdb/etc/local.ini
|
sed -i "s#DATA_DIR#/home#g" /opt/couchdb/etc/local.ini
|
||||||
elif [[ "${TARGETBUILD}" = "single" ]]; then
|
elif [[ "${TARGETBUILD}" = "single" ]]; then
|
||||||
|
# In the single image build, the Dockerfile specifies /data as a volume
|
||||||
|
# mount, so we use that for all persistent data.
|
||||||
sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini
|
sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini
|
||||||
sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini
|
sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini
|
||||||
elif [[ -n $KUBERNETES_SERVICE_HOST ]]; then
|
elif [[ -n $KUBERNETES_SERVICE_HOST ]]; then
|
||||||
# In Kubernetes the directory /opt/couchdb/data has a persistent volume
|
# In Kubernetes the directory /opt/couchdb/data has a persistent volume
|
||||||
# mount for storing database data.
|
# mount for storing database data.
|
||||||
sed -i "s#DATA_DIR#/opt/couchdb/data#g" /opt/clouseau/clouseau.ini
|
sed -i "s#DATA_DIR#/opt/couchdb/data#g" /opt/clouseau/clouseau.ini
|
||||||
sed -i "s#DATA_DIR#/opt/couchdb/data#g" /opt/couchdb/etc/local.ini
|
|
||||||
|
# We remove the database_dir and view_index_dir settings from the local.ini
|
||||||
|
# in Kubernetes because it will default to /opt/couchdb/data which is what
|
||||||
|
# our Helm chart was using prior to us switching to using our own CouchDB
|
||||||
|
# image.
|
||||||
|
sed -i "s#^database_dir.*\$##g" /opt/couchdb/etc/local.ini
|
||||||
|
sed -i "s#^view_index_dir.*\$##g" /opt/couchdb/etc/local.ini
|
||||||
|
|
||||||
|
# We remove the -name setting from the vm.args file in Kubernetes because
|
||||||
|
# it will default to the pod FQDN, which is what's required for clustering
|
||||||
|
# to work.
|
||||||
sed -i "s/^-name .*$//g" /opt/couchdb/etc/vm.args
|
sed -i "s/^-name .*$//g" /opt/couchdb/etc/vm.args
|
||||||
else
|
else
|
||||||
|
# For all other builds, we use /data for persistent data.
|
||||||
sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini
|
sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini
|
||||||
sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini
|
sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Start Clouseau. Budibase won't function correctly without Clouseau running, it
|
||||||
|
# powers the search API endpoints which are used to do all sorts, including
|
||||||
|
# populating app grids.
|
||||||
/opt/clouseau/bin/clouseau > /dev/stdout 2>&1 &
|
/opt/clouseau/bin/clouseau > /dev/stdout 2>&1 &
|
||||||
|
|
||||||
|
# Start CouchDB.
|
||||||
/docker-entrypoint.sh /opt/couchdb/bin/couchdb &
|
/docker-entrypoint.sh /opt/couchdb/bin/couchdb &
|
||||||
|
|
||||||
|
# Wati 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
|
||||||
echo 'Waiting for CouchDB to start...';
|
echo 'Waiting for CouchDB to start...';
|
||||||
sleep 5;
|
sleep 5;
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# CouchDB needs the `_users` and `_replicator` databases to exist before it will
|
||||||
|
# function correctly, so we create them here.
|
||||||
curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_users
|
curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_users
|
||||||
curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_replicator
|
curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_replicator
|
||||||
sleep infinity
|
sleep infinity
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.13.17",
|
"version": "2.13.30",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
|
@ -72,7 +72,7 @@
|
||||||
"@types/tar-fs": "2.0.1",
|
"@types/tar-fs": "2.0.1",
|
||||||
"@types/uuid": "8.3.4",
|
"@types/uuid": "8.3.4",
|
||||||
"chance": "1.1.8",
|
"chance": "1.1.8",
|
||||||
"ioredis-mock": "8.7.0",
|
"ioredis-mock": "8.9.0",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-environment-node": "29.7.0",
|
"jest-environment-node": "29.7.0",
|
||||||
"jest-serial-runner": "1.2.1",
|
"jest-serial-runner": "1.2.1",
|
||||||
|
|
|
@ -260,12 +260,12 @@ export async function listAllObjects(bucketName: string, path: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a presigned url with a default TTL of 1 hour
|
* Generate a presigned url with a default TTL of 36 hours
|
||||||
*/
|
*/
|
||||||
export function getPresignedUrl(
|
export function getPresignedUrl(
|
||||||
bucketName: string,
|
bucketName: string,
|
||||||
key: string,
|
key: string,
|
||||||
durationSeconds: number = 3600
|
durationSeconds: number = 129600
|
||||||
) {
|
) {
|
||||||
const objectStore = ObjectStore(bucketName, { presigning: true })
|
const objectStore = ObjectStore(bucketName, { presigning: true })
|
||||||
const params = {
|
const params = {
|
||||||
|
|
|
@ -2,8 +2,9 @@ import Redlock from "redlock"
|
||||||
import { getLockClient } from "./init"
|
import { getLockClient } from "./init"
|
||||||
import { LockOptions, LockType } from "@budibase/types"
|
import { LockOptions, LockType } from "@budibase/types"
|
||||||
import * as context from "../context"
|
import * as context from "../context"
|
||||||
import env from "../environment"
|
|
||||||
import { logWarn } from "../logging"
|
import { logWarn } from "../logging"
|
||||||
|
import { utils } from "@budibase/shared-core"
|
||||||
|
import { Duration } from "../utils"
|
||||||
|
|
||||||
async function getClient(
|
async function getClient(
|
||||||
type: LockType,
|
type: LockType,
|
||||||
|
@ -12,9 +13,7 @@ async function getClient(
|
||||||
if (type === LockType.CUSTOM) {
|
if (type === LockType.CUSTOM) {
|
||||||
return newRedlock(opts)
|
return newRedlock(opts)
|
||||||
}
|
}
|
||||||
if (env.isTest() && type !== LockType.TRY_ONCE) {
|
|
||||||
return newRedlock(OPTIONS.TEST)
|
|
||||||
}
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case LockType.TRY_ONCE: {
|
case LockType.TRY_ONCE: {
|
||||||
return newRedlock(OPTIONS.TRY_ONCE)
|
return newRedlock(OPTIONS.TRY_ONCE)
|
||||||
|
@ -28,13 +27,16 @@ async function getClient(
|
||||||
case LockType.DELAY_500: {
|
case LockType.DELAY_500: {
|
||||||
return newRedlock(OPTIONS.DELAY_500)
|
return newRedlock(OPTIONS.DELAY_500)
|
||||||
}
|
}
|
||||||
|
case LockType.AUTO_EXTEND: {
|
||||||
|
return newRedlock(OPTIONS.AUTO_EXTEND)
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
throw new Error(`Could not get redlock client: ${type}`)
|
throw utils.unreachable(type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const OPTIONS = {
|
const OPTIONS: Record<keyof typeof LockType, Redlock.Options> = {
|
||||||
TRY_ONCE: {
|
TRY_ONCE: {
|
||||||
// immediately throws an error if the lock is already held
|
// immediately throws an error if the lock is already held
|
||||||
retryCount: 0,
|
retryCount: 0,
|
||||||
|
@ -42,11 +44,6 @@ const OPTIONS = {
|
||||||
TRY_TWICE: {
|
TRY_TWICE: {
|
||||||
retryCount: 1,
|
retryCount: 1,
|
||||||
},
|
},
|
||||||
TEST: {
|
|
||||||
// higher retry count in unit tests
|
|
||||||
// due to high contention.
|
|
||||||
retryCount: 100,
|
|
||||||
},
|
|
||||||
DEFAULT: {
|
DEFAULT: {
|
||||||
// the expected clock drift; for more details
|
// the expected clock drift; for more details
|
||||||
// see http://redis.io/topics/distlock
|
// see http://redis.io/topics/distlock
|
||||||
|
@ -67,10 +64,14 @@ const OPTIONS = {
|
||||||
DELAY_500: {
|
DELAY_500: {
|
||||||
retryDelay: 500,
|
retryDelay: 500,
|
||||||
},
|
},
|
||||||
|
CUSTOM: {},
|
||||||
|
AUTO_EXTEND: {
|
||||||
|
retryCount: -1,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function newRedlock(opts: Redlock.Options = {}) {
|
export async function newRedlock(opts: Redlock.Options = {}) {
|
||||||
let options = { ...OPTIONS.DEFAULT, ...opts }
|
const options = { ...OPTIONS.DEFAULT, ...opts }
|
||||||
const redisWrapper = await getLockClient()
|
const redisWrapper = await getLockClient()
|
||||||
const client = redisWrapper.getClient()
|
const client = redisWrapper.getClient()
|
||||||
return new Redlock([client], options)
|
return new Redlock([client], options)
|
||||||
|
@ -100,17 +101,36 @@ function getLockName(opts: LockOptions) {
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const AUTO_EXTEND_POLLING_MS = Duration.fromSeconds(10).toMs()
|
||||||
|
|
||||||
export async function doWithLock<T>(
|
export async function doWithLock<T>(
|
||||||
opts: LockOptions,
|
opts: LockOptions,
|
||||||
task: () => Promise<T>
|
task: () => Promise<T>
|
||||||
): Promise<RedlockExecution<T>> {
|
): Promise<RedlockExecution<T>> {
|
||||||
const redlock = await getClient(opts.type, opts.customOptions)
|
const redlock = await getClient(opts.type, opts.customOptions)
|
||||||
let lock
|
let lock: Redlock.Lock | undefined
|
||||||
|
let timeout: NodeJS.Timeout | undefined
|
||||||
try {
|
try {
|
||||||
const name = getLockName(opts)
|
const name = getLockName(opts)
|
||||||
|
|
||||||
|
const ttl =
|
||||||
|
opts.type === LockType.AUTO_EXTEND ? AUTO_EXTEND_POLLING_MS : opts.ttl
|
||||||
|
|
||||||
// create the lock
|
// create the lock
|
||||||
lock = await redlock.lock(name, opts.ttl)
|
lock = await redlock.lock(name, ttl)
|
||||||
|
|
||||||
|
if (opts.type === LockType.AUTO_EXTEND) {
|
||||||
|
// We keep extending the lock while the task is running
|
||||||
|
const extendInIntervals = (): void => {
|
||||||
|
timeout = setTimeout(async () => {
|
||||||
|
lock = await lock!.extend(ttl, () => opts.onExtend && opts.onExtend())
|
||||||
|
|
||||||
|
extendInIntervals()
|
||||||
|
}, ttl / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
extendInIntervals()
|
||||||
|
}
|
||||||
|
|
||||||
// perform locked task
|
// perform locked task
|
||||||
// need to await to ensure completion before unlocking
|
// need to await to ensure completion before unlocking
|
||||||
|
@ -131,8 +151,7 @@ export async function doWithLock<T>(
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (lock) {
|
clearTimeout(timeout)
|
||||||
await lock.unlock()
|
await lock?.unlock()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { LockName, LockType, LockOptions } from "@budibase/types"
|
||||||
|
import { AUTO_EXTEND_POLLING_MS, doWithLock } from "../redlockImpl"
|
||||||
|
import { DBTestConfiguration, generator } from "../../../tests"
|
||||||
|
|
||||||
|
describe("redlockImpl", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("doWithLock", () => {
|
||||||
|
const config = new DBTestConfiguration()
|
||||||
|
const lockTtl = AUTO_EXTEND_POLLING_MS
|
||||||
|
|
||||||
|
function runLockWithExecutionTime({
|
||||||
|
opts,
|
||||||
|
task,
|
||||||
|
executionTimeMs,
|
||||||
|
}: {
|
||||||
|
opts: LockOptions
|
||||||
|
task: () => Promise<string>
|
||||||
|
executionTimeMs: number
|
||||||
|
}) {
|
||||||
|
return config.doInTenant(() =>
|
||||||
|
doWithLock(opts, async () => {
|
||||||
|
// Run in multiple intervals until hitting the expected time
|
||||||
|
const interval = lockTtl / 10
|
||||||
|
for (let i = executionTimeMs; i > 0; i -= interval) {
|
||||||
|
await jest.advanceTimersByTimeAsync(interval)
|
||||||
|
}
|
||||||
|
return task()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
it.each(Object.values(LockType))(
|
||||||
|
"should return the task value and release the lock",
|
||||||
|
async (lockType: LockType) => {
|
||||||
|
const expectedResult = generator.guid()
|
||||||
|
const mockTask = jest.fn().mockResolvedValue(expectedResult)
|
||||||
|
|
||||||
|
const opts: LockOptions = {
|
||||||
|
name: LockName.PERSIST_WRITETHROUGH,
|
||||||
|
type: lockType,
|
||||||
|
ttl: lockTtl,
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await runLockWithExecutionTime({
|
||||||
|
opts,
|
||||||
|
task: mockTask,
|
||||||
|
executionTimeMs: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.executed).toBe(true)
|
||||||
|
expect(result.executed && result.result).toBe(expectedResult)
|
||||||
|
expect(mockTask).toHaveBeenCalledTimes(1)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
it("should extend when type is autoextend", async () => {
|
||||||
|
const expectedResult = generator.guid()
|
||||||
|
const mockTask = jest.fn().mockResolvedValue(expectedResult)
|
||||||
|
const mockOnExtend = jest.fn()
|
||||||
|
|
||||||
|
const opts: LockOptions = {
|
||||||
|
name: LockName.PERSIST_WRITETHROUGH,
|
||||||
|
type: LockType.AUTO_EXTEND,
|
||||||
|
onExtend: mockOnExtend,
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await runLockWithExecutionTime({
|
||||||
|
opts,
|
||||||
|
task: mockTask,
|
||||||
|
executionTimeMs: lockTtl * 2.5,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.executed).toBe(true)
|
||||||
|
expect(result.executed && result.result).toBe(expectedResult)
|
||||||
|
expect(mockTask).toHaveBeenCalledTimes(1)
|
||||||
|
expect(mockOnExtend).toHaveBeenCalledTimes(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each(Object.values(LockType).filter(t => t !== LockType.AUTO_EXTEND))(
|
||||||
|
"should timeout when type is %s",
|
||||||
|
async (lockType: LockType) => {
|
||||||
|
const mockTask = jest.fn().mockResolvedValue("mockResult")
|
||||||
|
|
||||||
|
const opts: LockOptions = {
|
||||||
|
name: LockName.PERSIST_WRITETHROUGH,
|
||||||
|
type: lockType,
|
||||||
|
ttl: lockTtl,
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
runLockWithExecutionTime({
|
||||||
|
opts,
|
||||||
|
task: mockTask,
|
||||||
|
executionTimeMs: lockTtl * 2,
|
||||||
|
})
|
||||||
|
).rejects.toThrowError(
|
||||||
|
`Unable to fully release the lock on resource \"lock:${config.tenantId}_persist_writethrough\".`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
|
@ -18,6 +18,7 @@
|
||||||
checked={value}
|
checked={value}
|
||||||
{disabled}
|
{disabled}
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
|
on:click
|
||||||
{id}
|
{id}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="spectrum-Switch-input"
|
class="spectrum-Switch-input"
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
let focus = false
|
let focus = false
|
||||||
|
|
||||||
const updateValue = newValue => {
|
const updateValue = newValue => {
|
||||||
if (readonly) {
|
if (readonly || disabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (type === "number") {
|
if (type === "number") {
|
||||||
|
@ -31,14 +31,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const onFocus = () => {
|
const onFocus = () => {
|
||||||
if (readonly) {
|
if (readonly || disabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
focus = true
|
focus = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const onBlur = event => {
|
const onBlur = event => {
|
||||||
if (readonly) {
|
if (readonly || disabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
focus = false
|
focus = false
|
||||||
|
@ -46,14 +46,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const onInput = event => {
|
const onInput = event => {
|
||||||
if (readonly || !updateOnChange) {
|
if (readonly || !updateOnChange || disabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
updateValue(event.target.value)
|
updateValue(event.target.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateValueOnEnter = event => {
|
const updateValueOnEnter = event => {
|
||||||
if (readonly) {
|
if (readonly || disabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
|
@ -69,6 +69,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
if (disabled) return
|
||||||
focus = autofocus
|
focus = autofocus
|
||||||
if (focus) field.focus()
|
if (focus) field.focus()
|
||||||
})
|
})
|
||||||
|
@ -108,4 +109,16 @@
|
||||||
.spectrum-Textfield {
|
.spectrum-Textfield {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
color: var(--grey-7);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:hover::placeholder {
|
||||||
|
color: var(--grey-7) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus::placeholder {
|
||||||
|
color: var(--grey-7) !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -19,5 +19,5 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field {helpText} {label} {labelPosition} {error}>
|
<Field {helpText} {label} {labelPosition} {error}>
|
||||||
<Switch {error} {disabled} {text} {value} on:change={onChange} />
|
<Switch {error} {disabled} {text} {value} on:change={onChange} on:click />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { store } from "./index"
|
import { store } from "./index"
|
||||||
|
import { get } from "svelte/store"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import {
|
import {
|
||||||
decodeJSBinding,
|
decodeJSBinding,
|
||||||
|
@ -238,6 +239,10 @@ export const makeComponentUnique = component => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getComponentText = component => {
|
export const getComponentText = component => {
|
||||||
|
if (component == null) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
if (component?._instanceName) {
|
if (component?._instanceName) {
|
||||||
return component._instanceName
|
return component._instanceName
|
||||||
}
|
}
|
||||||
|
@ -246,3 +251,16 @@ export const getComponentText = component => {
|
||||||
"component"
|
"component"
|
||||||
return capitalise(type)
|
return capitalise(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getComponentName = component => {
|
||||||
|
if (component == null) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const components = get(store)?.components || {}
|
||||||
|
const componentDefinition = components[component._component] || {}
|
||||||
|
const name =
|
||||||
|
componentDefinition.friendlyName || componentDefinition.name || ""
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
|
@ -29,6 +29,12 @@ const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
|
||||||
const CAPTURE_VAR_INSIDE_JS = /\$\("([^")]+)"\)/g
|
const CAPTURE_VAR_INSIDE_JS = /\$\("([^")]+)"\)/g
|
||||||
const CAPTURE_HBS_TEMPLATE = /{{[\S\s]*?}}/g
|
const CAPTURE_HBS_TEMPLATE = /{{[\S\s]*?}}/g
|
||||||
|
|
||||||
|
const UpdateReferenceAction = {
|
||||||
|
ADD: "add",
|
||||||
|
DELETE: "delete",
|
||||||
|
MOVE: "move",
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all bindable data context fields and instance fields.
|
* Gets all bindable data context fields and instance fields.
|
||||||
*/
|
*/
|
||||||
|
@ -1226,3 +1232,81 @@ export const runtimeToReadableBinding = (
|
||||||
"readableBinding"
|
"readableBinding"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to update binding references for automation or action steps
|
||||||
|
*
|
||||||
|
* @param obj - The object to be updated
|
||||||
|
* @param originalIndex - The original index of the step being moved. Not applicable to add/delete.
|
||||||
|
* @param modifiedIndex - The new index of the step being modified
|
||||||
|
* @param action - Used to determine if a step is being added, deleted or moved
|
||||||
|
* @param label - The binding text that describes the steps
|
||||||
|
*/
|
||||||
|
export const updateReferencesInObject = ({
|
||||||
|
obj,
|
||||||
|
modifiedIndex,
|
||||||
|
action,
|
||||||
|
label,
|
||||||
|
originalIndex,
|
||||||
|
}) => {
|
||||||
|
const stepIndexRegex = new RegExp(`{{\\s*${label}\\.(\\d+)\\.`, "g")
|
||||||
|
const updateActionStep = (str, index, replaceWith) =>
|
||||||
|
str.replace(`{{ ${label}.${index}.`, `{{ ${label}.${replaceWith}.`)
|
||||||
|
for (const key in obj) {
|
||||||
|
if (typeof obj[key] === "string") {
|
||||||
|
let matches
|
||||||
|
while ((matches = stepIndexRegex.exec(obj[key])) !== null) {
|
||||||
|
const referencedStep = parseInt(matches[1])
|
||||||
|
if (
|
||||||
|
action === UpdateReferenceAction.ADD &&
|
||||||
|
referencedStep >= modifiedIndex
|
||||||
|
) {
|
||||||
|
obj[key] = updateActionStep(
|
||||||
|
obj[key],
|
||||||
|
referencedStep,
|
||||||
|
referencedStep + 1
|
||||||
|
)
|
||||||
|
} else if (
|
||||||
|
action === UpdateReferenceAction.DELETE &&
|
||||||
|
referencedStep > modifiedIndex
|
||||||
|
) {
|
||||||
|
obj[key] = updateActionStep(
|
||||||
|
obj[key],
|
||||||
|
referencedStep,
|
||||||
|
referencedStep - 1
|
||||||
|
)
|
||||||
|
} else if (action === UpdateReferenceAction.MOVE) {
|
||||||
|
if (referencedStep === originalIndex) {
|
||||||
|
obj[key] = updateActionStep(obj[key], referencedStep, modifiedIndex)
|
||||||
|
} else if (
|
||||||
|
modifiedIndex <= referencedStep &&
|
||||||
|
modifiedIndex < originalIndex
|
||||||
|
) {
|
||||||
|
obj[key] = updateActionStep(
|
||||||
|
obj[key],
|
||||||
|
referencedStep,
|
||||||
|
referencedStep + 1
|
||||||
|
)
|
||||||
|
} else if (
|
||||||
|
modifiedIndex >= referencedStep &&
|
||||||
|
modifiedIndex > originalIndex
|
||||||
|
) {
|
||||||
|
obj[key] = updateActionStep(
|
||||||
|
obj[key],
|
||||||
|
referencedStep,
|
||||||
|
referencedStep - 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (typeof obj[key] === "object" && obj[key] !== null) {
|
||||||
|
updateReferencesInObject({
|
||||||
|
obj: obj[key],
|
||||||
|
modifiedIndex,
|
||||||
|
action,
|
||||||
|
label,
|
||||||
|
originalIndex,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { getTemporalStore } from "./store/temporal"
|
||||||
import { getThemeStore } from "./store/theme"
|
import { getThemeStore } from "./store/theme"
|
||||||
import { getUserStore } from "./store/users"
|
import { getUserStore } from "./store/users"
|
||||||
import { getDeploymentStore } from "./store/deployments"
|
import { getDeploymentStore } from "./store/deployments"
|
||||||
import { derived, writable, get } from "svelte/store"
|
import { derived, get } from "svelte/store"
|
||||||
import { findComponent, findComponentPath } from "./componentUtils"
|
import { findComponent, findComponentPath } from "./componentUtils"
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
import { createHistoryStore } from "builderStore/store/history"
|
import { createHistoryStore } from "builderStore/store/history"
|
||||||
|
@ -146,5 +146,3 @@ export const userSelectedResourceMap = derived(userStore, $userStore => {
|
||||||
export const isOnlyUser = derived(userStore, $userStore => {
|
export const isOnlyUser = derived(userStore, $userStore => {
|
||||||
return $userStore.length < 2
|
return $userStore.length < 2
|
||||||
})
|
})
|
||||||
|
|
||||||
export const screensHeight = writable("210px")
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { cloneDeep } from "lodash/fp"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
import { selectedAutomation } from "builderStore"
|
import { selectedAutomation } from "builderStore"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
|
import { updateReferencesInObject } from "builderStore/dataBinding"
|
||||||
|
|
||||||
const initialAutomationState = {
|
const initialAutomationState = {
|
||||||
automations: [],
|
automations: [],
|
||||||
|
@ -22,34 +23,14 @@ export const getAutomationStore = () => {
|
||||||
return store
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateReferencesInObject = (obj, modifiedIndex, action) => {
|
|
||||||
const regex = /{{\s*steps\.(\d+)\./g
|
|
||||||
for (const key in obj) {
|
|
||||||
if (typeof obj[key] === "string") {
|
|
||||||
let matches
|
|
||||||
while ((matches = regex.exec(obj[key])) !== null) {
|
|
||||||
const referencedStep = parseInt(matches[1])
|
|
||||||
if (action === "add" && referencedStep >= modifiedIndex) {
|
|
||||||
obj[key] = obj[key].replace(
|
|
||||||
`{{ steps.${referencedStep}.`,
|
|
||||||
`{{ steps.${referencedStep + 1}.`
|
|
||||||
)
|
|
||||||
} else if (action === "delete" && referencedStep > modifiedIndex) {
|
|
||||||
obj[key] = obj[key].replace(
|
|
||||||
`{{ steps.${referencedStep}.`,
|
|
||||||
`{{ steps.${referencedStep - 1}.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (typeof obj[key] === "object" && obj[key] !== null) {
|
|
||||||
updateReferencesInObject(obj[key], modifiedIndex, action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateStepReferences = (steps, modifiedIndex, action) => {
|
const updateStepReferences = (steps, modifiedIndex, action) => {
|
||||||
steps.forEach(step => {
|
steps.forEach(step => {
|
||||||
updateReferencesInObject(step.inputs, modifiedIndex, action)
|
updateReferencesInObject({
|
||||||
|
obj: step.inputs,
|
||||||
|
modifiedIndex,
|
||||||
|
action,
|
||||||
|
label: "steps",
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { expect, describe, it, vi } from "vitest"
|
||||||
import {
|
import {
|
||||||
runtimeToReadableBinding,
|
runtimeToReadableBinding,
|
||||||
readableToRuntimeBinding,
|
readableToRuntimeBinding,
|
||||||
|
updateReferencesInObject,
|
||||||
} from "../dataBinding"
|
} from "../dataBinding"
|
||||||
|
|
||||||
vi.mock("@budibase/frontend-core")
|
vi.mock("@budibase/frontend-core")
|
||||||
|
@ -84,3 +85,461 @@ describe("readableToRuntimeBinding", () => {
|
||||||
).toEqual(`Hello {{ [user].[firstName] }}! The count is {{ count }}.`)
|
).toEqual(`Hello {{ [user].[firstName] }}! The count is {{ count }}.`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("updateReferencesInObject", () => {
|
||||||
|
it("should increment steps in sequence on 'add'", () => {
|
||||||
|
let obj = [
|
||||||
|
{
|
||||||
|
id: "a0",
|
||||||
|
parameters: {
|
||||||
|
text: "Alpha",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "a1",
|
||||||
|
parameters: {
|
||||||
|
text: "Apple",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b2",
|
||||||
|
parameters: {
|
||||||
|
text: "Banana {{ actions.1.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "c3",
|
||||||
|
parameters: {
|
||||||
|
text: "Carrot {{ actions.1.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "d4",
|
||||||
|
parameters: {
|
||||||
|
text: "Dog {{ actions.3.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "e5",
|
||||||
|
parameters: {
|
||||||
|
text: "Eagle {{ actions.4.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
updateReferencesInObject({
|
||||||
|
obj,
|
||||||
|
modifiedIndex: 0,
|
||||||
|
action: "add",
|
||||||
|
label: "actions",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(obj).toEqual([
|
||||||
|
{
|
||||||
|
id: "a0",
|
||||||
|
parameters: {
|
||||||
|
text: "Alpha",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "a1",
|
||||||
|
parameters: {
|
||||||
|
text: "Apple",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b2",
|
||||||
|
parameters: {
|
||||||
|
text: "Banana {{ actions.2.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "c3",
|
||||||
|
parameters: {
|
||||||
|
text: "Carrot {{ actions.2.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "d4",
|
||||||
|
parameters: {
|
||||||
|
text: "Dog {{ actions.4.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "e5",
|
||||||
|
parameters: {
|
||||||
|
text: "Eagle {{ actions.5.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should decrement steps in sequence on 'delete'", () => {
|
||||||
|
let obj = [
|
||||||
|
{
|
||||||
|
id: "a1",
|
||||||
|
parameters: {
|
||||||
|
text: "Apple",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b2",
|
||||||
|
parameters: {
|
||||||
|
text: "Banana {{ actions.1.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "d4",
|
||||||
|
parameters: {
|
||||||
|
text: "Dog {{ actions.3.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "e5",
|
||||||
|
parameters: {
|
||||||
|
text: "Eagle {{ actions.4.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
updateReferencesInObject({
|
||||||
|
obj,
|
||||||
|
modifiedIndex: 2,
|
||||||
|
action: "delete",
|
||||||
|
label: "actions",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(obj).toEqual([
|
||||||
|
{
|
||||||
|
id: "a1",
|
||||||
|
parameters: {
|
||||||
|
text: "Apple",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b2",
|
||||||
|
parameters: {
|
||||||
|
text: "Banana {{ actions.1.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "d4",
|
||||||
|
parameters: {
|
||||||
|
text: "Dog {{ actions.2.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "e5",
|
||||||
|
parameters: {
|
||||||
|
text: "Eagle {{ actions.3.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle on 'move' to a lower index", () => {
|
||||||
|
let obj = [
|
||||||
|
{
|
||||||
|
id: "a1",
|
||||||
|
parameters: {
|
||||||
|
text: "Apple",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b2",
|
||||||
|
parameters: {
|
||||||
|
text: "Banana {{ actions.0.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "e5",
|
||||||
|
parameters: {
|
||||||
|
text: "Eagle {{ actions.3.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "c3",
|
||||||
|
parameters: {
|
||||||
|
text: "Carrot {{ actions.0.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "d4",
|
||||||
|
parameters: {
|
||||||
|
text: "Dog {{ actions.2.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
updateReferencesInObject({
|
||||||
|
obj,
|
||||||
|
modifiedIndex: 2,
|
||||||
|
action: "move",
|
||||||
|
label: "actions",
|
||||||
|
originalIndex: 4,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(obj).toEqual([
|
||||||
|
{
|
||||||
|
id: "a1",
|
||||||
|
parameters: {
|
||||||
|
text: "Apple",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b2",
|
||||||
|
parameters: {
|
||||||
|
text: "Banana {{ actions.0.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "e5",
|
||||||
|
parameters: {
|
||||||
|
text: "Eagle {{ actions.4.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "c3",
|
||||||
|
parameters: {
|
||||||
|
text: "Carrot {{ actions.0.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "d4",
|
||||||
|
parameters: {
|
||||||
|
text: "Dog {{ actions.3.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle on 'move' to a higher index", () => {
|
||||||
|
let obj = [
|
||||||
|
{
|
||||||
|
id: "b2",
|
||||||
|
parameters: {
|
||||||
|
text: "Banana {{ actions.0.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "c3",
|
||||||
|
parameters: {
|
||||||
|
text: "Carrot {{ actions.0.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "a1",
|
||||||
|
parameters: {
|
||||||
|
text: "Apple",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "d4",
|
||||||
|
parameters: {
|
||||||
|
text: "Dog {{ actions.2.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "e5",
|
||||||
|
parameters: {
|
||||||
|
text: "Eagle {{ actions.3.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
updateReferencesInObject({
|
||||||
|
obj,
|
||||||
|
modifiedIndex: 2,
|
||||||
|
action: "move",
|
||||||
|
label: "actions",
|
||||||
|
originalIndex: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(obj).toEqual([
|
||||||
|
{
|
||||||
|
id: "b2",
|
||||||
|
parameters: {
|
||||||
|
text: "Banana {{ actions.2.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "c3",
|
||||||
|
parameters: {
|
||||||
|
text: "Carrot {{ actions.2.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "a1",
|
||||||
|
parameters: {
|
||||||
|
text: "Apple",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "d4",
|
||||||
|
parameters: {
|
||||||
|
text: "Dog {{ actions.1.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "e5",
|
||||||
|
parameters: {
|
||||||
|
text: "Eagle {{ actions.3.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle on 'move' of action being referenced, dragged to a higher index", () => {
|
||||||
|
let obj = [
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Validate Form",
|
||||||
|
id: "cCD0Dwcnq",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Close Screen Modal",
|
||||||
|
id: "3fbbIOfN0H",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Save Row",
|
||||||
|
parameters: {
|
||||||
|
tableId: "ta_bb_employee",
|
||||||
|
},
|
||||||
|
id: "aehg5cTmhR",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Close Side Panel",
|
||||||
|
id: "mzkpf86cxo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Navigate To",
|
||||||
|
id: "h0uDFeJa8A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
autoDismiss: true,
|
||||||
|
type: "success",
|
||||||
|
message: "{{ actions.1.row }}",
|
||||||
|
},
|
||||||
|
"##eventHandlerType": "Show Notification",
|
||||||
|
id: "JEI5lAyJZ",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
updateReferencesInObject({
|
||||||
|
obj,
|
||||||
|
modifiedIndex: 2,
|
||||||
|
action: "move",
|
||||||
|
label: "actions",
|
||||||
|
originalIndex: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(obj).toEqual([
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Validate Form",
|
||||||
|
id: "cCD0Dwcnq",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Close Screen Modal",
|
||||||
|
id: "3fbbIOfN0H",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Save Row",
|
||||||
|
parameters: {
|
||||||
|
tableId: "ta_bb_employee",
|
||||||
|
},
|
||||||
|
id: "aehg5cTmhR",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Close Side Panel",
|
||||||
|
id: "mzkpf86cxo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Navigate To",
|
||||||
|
id: "h0uDFeJa8A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
autoDismiss: true,
|
||||||
|
type: "success",
|
||||||
|
message: "{{ actions.2.row }}",
|
||||||
|
},
|
||||||
|
"##eventHandlerType": "Show Notification",
|
||||||
|
id: "JEI5lAyJZ",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle on 'move' of action being referenced, dragged to a lower index", () => {
|
||||||
|
let obj = [
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Save Row",
|
||||||
|
parameters: {
|
||||||
|
tableId: "ta_bb_employee",
|
||||||
|
},
|
||||||
|
id: "aehg5cTmhR",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Validate Form",
|
||||||
|
id: "cCD0Dwcnq",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Close Screen Modal",
|
||||||
|
id: "3fbbIOfN0H",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Close Side Panel",
|
||||||
|
id: "mzkpf86cxo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Navigate To",
|
||||||
|
id: "h0uDFeJa8A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
autoDismiss: true,
|
||||||
|
type: "success",
|
||||||
|
message: "{{ actions.4.row }}",
|
||||||
|
},
|
||||||
|
"##eventHandlerType": "Show Notification",
|
||||||
|
id: "JEI5lAyJZ",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
updateReferencesInObject({
|
||||||
|
obj,
|
||||||
|
modifiedIndex: 0,
|
||||||
|
action: "move",
|
||||||
|
label: "actions",
|
||||||
|
originalIndex: 4,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(obj).toEqual([
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Save Row",
|
||||||
|
parameters: {
|
||||||
|
tableId: "ta_bb_employee",
|
||||||
|
},
|
||||||
|
id: "aehg5cTmhR",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Validate Form",
|
||||||
|
id: "cCD0Dwcnq",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Close Screen Modal",
|
||||||
|
id: "3fbbIOfN0H",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Close Side Panel",
|
||||||
|
id: "mzkpf86cxo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Navigate To",
|
||||||
|
id: "h0uDFeJa8A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
autoDismiss: true,
|
||||||
|
type: "success",
|
||||||
|
message: "{{ actions.0.row }}",
|
||||||
|
},
|
||||||
|
"##eventHandlerType": "Show Notification",
|
||||||
|
id: "JEI5lAyJZ",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
@ -64,7 +64,7 @@
|
||||||
</span>
|
</span>
|
||||||
{:else if schema.type === "link"}
|
{:else if schema.type === "link"}
|
||||||
<LinkedRowSelector
|
<LinkedRowSelector
|
||||||
bind:linkedRows={value[field]}
|
linkedRows={value[field]}
|
||||||
{schema}
|
{schema}
|
||||||
on:change={e => onChange(e, field)}
|
on:change={e => onChange(e, field)}
|
||||||
useLabel={false}
|
useLabel={false}
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
<Select
|
<Select
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
bind:value
|
bind:value
|
||||||
options={filteredTables.filter(table => table._id !== TableNames.USERS)}
|
options={filteredTables}
|
||||||
getOptionLabel={table => table.name}
|
getOptionLabel={table => table.name}
|
||||||
getOptionValue={table => table._id}
|
getOptionValue={table => table._id}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -70,7 +70,12 @@
|
||||||
options={meta.constraints.inclusion}
|
options={meta.constraints.inclusion}
|
||||||
/>
|
/>
|
||||||
{:else if type === "link"}
|
{:else if type === "link"}
|
||||||
<LinkedRowSelector {error} bind:linkedRows={value} schema={meta} />
|
<LinkedRowSelector
|
||||||
|
{error}
|
||||||
|
linkedRows={value}
|
||||||
|
schema={meta}
|
||||||
|
on:change={e => (value = e.detail)}
|
||||||
|
/>
|
||||||
{:else if type === "longform"}
|
{:else if type === "longform"}
|
||||||
{#if meta.useRichText}
|
{#if meta.useRichText}
|
||||||
<RichTextField {error} {label} height="150px" bind:value />
|
<RichTextField {error} {label} height="150px" bind:value />
|
||||||
|
|
|
@ -56,12 +56,12 @@
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Multiselect
|
<Multiselect
|
||||||
bind:value={linkedIds}
|
value={linkedIds}
|
||||||
{label}
|
{label}
|
||||||
options={rows}
|
options={rows}
|
||||||
getOptionLabel={getPrettyName}
|
getOptionLabel={getPrettyName}
|
||||||
getOptionValue={row => row._id}
|
getOptionValue={row => row._id}
|
||||||
sort
|
sort
|
||||||
on:change={() => dispatch("change", linkedIds)}
|
on:change
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon } from "@budibase/bbui"
|
import { AbsTooltip, Icon } from "@budibase/bbui"
|
||||||
import { createEventDispatcher, getContext } from "svelte"
|
import { createEventDispatcher, getContext } from "svelte"
|
||||||
import { helpers } from "@budibase/shared-core"
|
import { helpers } from "@budibase/shared-core"
|
||||||
import { UserAvatars } from "@budibase/frontend-core"
|
import { UserAvatars } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export let icon
|
export let icon
|
||||||
|
export let iconTooltip
|
||||||
export let withArrow = false
|
export let withArrow = false
|
||||||
export let withActions = true
|
export let withActions = true
|
||||||
export let indentLevel = 0
|
export let indentLevel = 0
|
||||||
|
@ -77,7 +78,11 @@
|
||||||
{style}
|
{style}
|
||||||
{draggable}
|
{draggable}
|
||||||
>
|
>
|
||||||
<div class="nav-item-content" bind:this={contentRef}>
|
<div
|
||||||
|
class="nav-item-content"
|
||||||
|
bind:this={contentRef}
|
||||||
|
class:right={rightAlignIcon}
|
||||||
|
>
|
||||||
{#if withArrow}
|
{#if withArrow}
|
||||||
<div
|
<div
|
||||||
class:opened
|
class:opened
|
||||||
|
@ -98,7 +103,9 @@
|
||||||
</div>
|
</div>
|
||||||
{:else if icon}
|
{:else if icon}
|
||||||
<div class="icon" class:right={rightAlignIcon}>
|
<div class="icon" class:right={rightAlignIcon}>
|
||||||
<Icon color={iconColor} size="S" name={icon} />
|
<AbsTooltip type="info" position="right" text={iconTooltip}>
|
||||||
|
<Icon color={iconColor} size="S" name={icon} />
|
||||||
|
</AbsTooltip>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="text" title={showTooltip ? text : null}>
|
<div class="text" title={showTooltip ? text : null}>
|
||||||
|
@ -166,6 +173,11 @@
|
||||||
width: max-content;
|
width: max-content;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-left: var(--spacing-l);
|
padding-left: var(--spacing-l);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item-content.right {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Needed to fully display the actions icon */
|
/* Needed to fully display the actions icon */
|
||||||
|
@ -264,6 +276,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.right {
|
.right {
|
||||||
|
margin-left: auto;
|
||||||
order: 10;
|
order: 10;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
const getResizeActions = (
|
||||||
|
cssProperty,
|
||||||
|
mouseMoveEventProperty,
|
||||||
|
elementProperty,
|
||||||
|
initialValue,
|
||||||
|
setValue = () => {}
|
||||||
|
) => {
|
||||||
|
let element = null
|
||||||
|
|
||||||
|
const elementAction = node => {
|
||||||
|
element = node
|
||||||
|
|
||||||
|
if (initialValue != null) {
|
||||||
|
element.style[cssProperty] = `${initialValue}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
destroy() {
|
||||||
|
element = null
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dragHandleAction = node => {
|
||||||
|
let startProperty = null
|
||||||
|
let startPosition = null
|
||||||
|
|
||||||
|
const handleMouseMove = e => {
|
||||||
|
e.preventDefault() // Prevent highlighting while dragging
|
||||||
|
const change = e[mouseMoveEventProperty] - startPosition
|
||||||
|
element.style[cssProperty] = `${startProperty + change}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseUp = e => {
|
||||||
|
e.preventDefault() // Prevent highlighting while dragging
|
||||||
|
window.removeEventListener("mousemove", handleMouseMove)
|
||||||
|
window.removeEventListener("mouseup", handleMouseUp)
|
||||||
|
|
||||||
|
element.style.removeProperty("transition") // remove temporary transition override
|
||||||
|
for (let item of document.getElementsByTagName("iframe")) {
|
||||||
|
item.style.removeProperty("pointer-events")
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(element[elementProperty])
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseDown = e => {
|
||||||
|
if (e.detail > 1) {
|
||||||
|
// e.detail is the number of rapid clicks, so e.detail = 2 is
|
||||||
|
// a double click. We want to prevent default behaviour in
|
||||||
|
// this case as it highlights nearby selectable elements, which
|
||||||
|
// then interferes with the resizing mousemove.
|
||||||
|
// Putting this on the double click handler doesn't seem to
|
||||||
|
// work, so it must go here.
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
e.target.hasAttribute("disabled") &&
|
||||||
|
e.target.getAttribute("disabled") !== "false"
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
element.style.transition = `${cssProperty} 0ms` // temporarily override any height transitions
|
||||||
|
|
||||||
|
// iframes swallow mouseup events if your cursor ends up over it during a drag, so make them
|
||||||
|
// temporarily non-interactive
|
||||||
|
for (let item of document.getElementsByTagName("iframe")) {
|
||||||
|
item.style.pointerEvents = "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
startProperty = element[elementProperty]
|
||||||
|
startPosition = e[mouseMoveEventProperty]
|
||||||
|
|
||||||
|
window.addEventListener("mousemove", handleMouseMove)
|
||||||
|
window.addEventListener("mouseup", handleMouseUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDoubleClick = () => {
|
||||||
|
element.style.removeProperty(cssProperty)
|
||||||
|
}
|
||||||
|
|
||||||
|
node.addEventListener("mousedown", handleMouseDown)
|
||||||
|
node.addEventListener("dblclick", handleDoubleClick)
|
||||||
|
|
||||||
|
return {
|
||||||
|
destroy() {
|
||||||
|
node.removeEventListener("mousedown", handleMouseDown)
|
||||||
|
node.removeEventListener("dblclick", handleDoubleClick)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [elementAction, dragHandleAction]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getVerticalResizeActions = (initialValue, setValue = () => {}) => {
|
||||||
|
return getResizeActions(
|
||||||
|
"height",
|
||||||
|
"pageY",
|
||||||
|
"clientHeight",
|
||||||
|
initialValue,
|
||||||
|
setValue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getHorizontalResizeActions = (
|
||||||
|
initialValue,
|
||||||
|
setValue = () => {}
|
||||||
|
) => {
|
||||||
|
return getResizeActions(
|
||||||
|
"width",
|
||||||
|
"pageX",
|
||||||
|
"clientWidth",
|
||||||
|
initialValue,
|
||||||
|
setValue
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon, Body } from "@budibase/bbui"
|
import { AbsTooltip, Icon, Body } from "@budibase/bbui"
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let icon
|
export let icon
|
||||||
|
export let iconTooltip
|
||||||
export let showAddButton = false
|
export let showAddButton = false
|
||||||
export let showBackButton = false
|
export let showBackButton = false
|
||||||
export let showCloseButton = false
|
export let showCloseButton = false
|
||||||
|
@ -36,7 +37,9 @@
|
||||||
<Icon name="ArrowLeft" hoverable on:click={onClickBackButton} />
|
<Icon name="ArrowLeft" hoverable on:click={onClickBackButton} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if icon}
|
{#if icon}
|
||||||
<Icon name={icon} />
|
<AbsTooltip type="info" text={iconTooltip}>
|
||||||
|
<Icon name={icon} />
|
||||||
|
</AbsTooltip>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="title">
|
<div class="title">
|
||||||
{#if customTitleContent}
|
{#if customTitleContent}
|
||||||
|
@ -68,6 +71,7 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.panel {
|
.panel {
|
||||||
|
min-width: 260px;
|
||||||
width: 260px;
|
width: 260px;
|
||||||
flex: 0 0 260px;
|
flex: 0 0 260px;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
|
@ -85,6 +89,7 @@
|
||||||
border-right: var(--border-light);
|
border-right: var(--border-light);
|
||||||
}
|
}
|
||||||
.panel.wide {
|
.panel.wide {
|
||||||
|
min-width: 310px;
|
||||||
width: 310px;
|
width: 310px;
|
||||||
flex: 0 0 310px;
|
flex: 0 0 310px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
getEventContextBindings,
|
getEventContextBindings,
|
||||||
getActionBindings,
|
getActionBindings,
|
||||||
makeStateBinding,
|
makeStateBinding,
|
||||||
|
updateReferencesInObject,
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
|
@ -30,6 +31,7 @@
|
||||||
|
|
||||||
let actionQuery
|
let actionQuery
|
||||||
let selectedAction = actions?.length ? actions[0] : null
|
let selectedAction = actions?.length ? actions[0] : null
|
||||||
|
let originalActionIndex
|
||||||
|
|
||||||
const setUpdateActions = actions => {
|
const setUpdateActions = actions => {
|
||||||
return actions
|
return actions
|
||||||
|
@ -115,6 +117,14 @@
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
selectedAction = actions?.length ? actions[0] : null
|
selectedAction = actions?.length ? actions[0] : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update action binding references
|
||||||
|
updateReferencesInObject({
|
||||||
|
obj: actions,
|
||||||
|
modifiedIndex: index,
|
||||||
|
action: "delete",
|
||||||
|
label: "actions",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleActionList = () => {
|
const toggleActionList = () => {
|
||||||
|
@ -137,6 +147,7 @@
|
||||||
|
|
||||||
const selectAction = action => () => {
|
const selectAction = action => () => {
|
||||||
selectedAction = action
|
selectedAction = action
|
||||||
|
originalActionIndex = actions.findIndex(item => item.id === action.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onAddAction = actionType => {
|
const onAddAction = actionType => {
|
||||||
|
@ -146,9 +157,29 @@
|
||||||
|
|
||||||
function handleDndConsider(e) {
|
function handleDndConsider(e) {
|
||||||
actions = e.detail.items
|
actions = e.detail.items
|
||||||
|
|
||||||
|
// set the initial index of the action being dragged
|
||||||
|
if (e.detail.info.trigger === "draggedEntered") {
|
||||||
|
originalActionIndex = actions.findIndex(
|
||||||
|
action => action.id === e.detail.info.id
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function handleDndFinalize(e) {
|
function handleDndFinalize(e) {
|
||||||
actions = e.detail.items
|
actions = e.detail.items
|
||||||
|
|
||||||
|
// Update action binding references
|
||||||
|
updateReferencesInObject({
|
||||||
|
obj: actions,
|
||||||
|
modifiedIndex: actions.findIndex(
|
||||||
|
action => action.id === e.detail.info.id
|
||||||
|
),
|
||||||
|
action: "move",
|
||||||
|
label: "actions",
|
||||||
|
originalIndex: originalActionIndex,
|
||||||
|
})
|
||||||
|
|
||||||
|
originalActionIndex = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAllBindings = (actionBindings, eventContextBindings, actions) => {
|
const getAllBindings = (actionBindings, eventContextBindings, actions) => {
|
||||||
|
@ -289,7 +320,7 @@
|
||||||
</Layout>
|
</Layout>
|
||||||
<Layout noPadding>
|
<Layout noPadding>
|
||||||
{#if selectedActionComponent && !showAvailableActions}
|
{#if selectedActionComponent && !showAvailableActions}
|
||||||
{#key selectedAction.id}
|
{#key (selectedAction.id, originalActionIndex)}
|
||||||
<div class="selected-action-container">
|
<div class="selected-action-container">
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={selectedActionComponent}
|
this={selectedActionComponent}
|
||||||
|
|
|
@ -55,7 +55,10 @@
|
||||||
size="S"
|
size="S"
|
||||||
name="Close"
|
name="Close"
|
||||||
hoverable
|
hoverable
|
||||||
on:click={() => removeButton(item._id)}
|
on:click={e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
removeButton(item._id)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -32,11 +32,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const flipDurationMs = 150
|
|
||||||
|
|
||||||
let anchors = {}
|
let anchors = {}
|
||||||
let draggableItems = []
|
let draggableItems = []
|
||||||
|
|
||||||
|
// Used for controlling cursor behaviour in order to limit drag behaviour
|
||||||
|
// to the drag handle
|
||||||
|
let inactive = true
|
||||||
|
|
||||||
const buildDraggable = items => {
|
const buildDraggable = items => {
|
||||||
return items
|
return items
|
||||||
.map(item => {
|
.map(item => {
|
||||||
|
@ -64,6 +67,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFinalize = e => {
|
const handleFinalize = e => {
|
||||||
|
inactive = true
|
||||||
updateRowOrder(e)
|
updateRowOrder(e)
|
||||||
dispatch("change", serialiseUpdate())
|
dispatch("change", serialiseUpdate())
|
||||||
}
|
}
|
||||||
|
@ -77,24 +81,36 @@
|
||||||
class="list-wrap"
|
class="list-wrap"
|
||||||
use:dndzone={{
|
use:dndzone={{
|
||||||
items: draggableItems,
|
items: draggableItems,
|
||||||
flipDurationMs,
|
|
||||||
dropTargetStyle: { outline: "none" },
|
dropTargetStyle: { outline: "none" },
|
||||||
dragDisabled: !draggable,
|
dragDisabled: !draggable || inactive,
|
||||||
}}
|
}}
|
||||||
on:finalize={handleFinalize}
|
on:finalize={handleFinalize}
|
||||||
on:consider={updateRowOrder}
|
on:consider={updateRowOrder}
|
||||||
>
|
>
|
||||||
{#each draggableItems as draggable (draggable.id)}
|
{#each draggableItems as draggableItem (draggableItem.id)}
|
||||||
<li
|
<li
|
||||||
|
on:click={() => {
|
||||||
|
get(store).actions.select(draggableItem.id)
|
||||||
|
}}
|
||||||
on:mousedown={() => {
|
on:mousedown={() => {
|
||||||
get(store).actions.select()
|
get(store).actions.select()
|
||||||
}}
|
}}
|
||||||
bind:this={anchors[draggable.id]}
|
bind:this={anchors[draggableItem.id]}
|
||||||
class:highlighted={draggable.id === $store.selected}
|
class:highlighted={draggableItem.id === $store.selected}
|
||||||
>
|
>
|
||||||
<div class="left-content">
|
<div class="left-content">
|
||||||
{#if showHandle}
|
{#if showHandle}
|
||||||
<div class="handle">
|
<div
|
||||||
|
class="handle"
|
||||||
|
aria-label="drag-handle"
|
||||||
|
style={!inactive ? "cursor:grabbing" : "cursor:grab"}
|
||||||
|
on:mousedown={() => {
|
||||||
|
inactive = false
|
||||||
|
}}
|
||||||
|
on:mouseup={() => {
|
||||||
|
inactive = true
|
||||||
|
}}
|
||||||
|
>
|
||||||
<DragHandle />
|
<DragHandle />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -102,8 +118,8 @@
|
||||||
<div class="right-content">
|
<div class="right-content">
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={listType}
|
this={listType}
|
||||||
anchor={anchors[draggable.item._id]}
|
anchor={anchors[draggableItem.item._id]}
|
||||||
item={draggable.item}
|
item={draggableItem.item}
|
||||||
{...listTypeProps}
|
{...listTypeProps}
|
||||||
on:change={onItemChanged}
|
on:change={onItemChanged}
|
||||||
/>
|
/>
|
||||||
|
@ -143,6 +159,7 @@
|
||||||
--spectrum-table-row-background-color-hover,
|
--spectrum-table-row-background-color-hover,
|
||||||
var(--spectrum-alias-highlight-hover)
|
var(--spectrum-alias-highlight-hover)
|
||||||
);
|
);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.list-wrap > li:first-child {
|
.list-wrap > li:first-child {
|
||||||
border-top-left-radius: 4px;
|
border-top-left-radius: 4px;
|
||||||
|
@ -165,6 +182,9 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
height: var(--spectrum-global-dimension-size-150);
|
height: var(--spectrum-global-dimension-size-150);
|
||||||
}
|
}
|
||||||
|
.handle:hover {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
.handle :global(svg) {
|
.handle :global(svg) {
|
||||||
fill: var(--spectrum-global-color-gray-500);
|
fill: var(--spectrum-global-color-gray-500);
|
||||||
margin-right: var(--spacing-m);
|
margin-right: var(--spacing-m);
|
||||||
|
|
|
@ -156,7 +156,7 @@
|
||||||
|
|
||||||
<div class="field-configuration">
|
<div class="field-configuration">
|
||||||
<div class="toggle-all">
|
<div class="toggle-all">
|
||||||
<span />
|
<span>Fields</span>
|
||||||
<Toggle
|
<Toggle
|
||||||
on:change={() => {
|
on:change={() => {
|
||||||
let update = fieldList.map(field => ({
|
let update = fieldList.map(field => ({
|
||||||
|
@ -186,6 +186,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.field-configuration {
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
.field-configuration :global(.spectrum-ActionButton) {
|
.field-configuration :global(.spectrum-ActionButton) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -204,6 +207,5 @@
|
||||||
.toggle-all span {
|
.toggle-all span {
|
||||||
color: var(--spectrum-global-color-gray-700);
|
color: var(--spectrum-global-color-gray-700);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin-left: calc(var(--spacing-s) - 1px);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -58,7 +58,15 @@
|
||||||
<div class="field-label">{readableText}</div>
|
<div class="field-label">{readableText}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-item-right">
|
<div class="list-item-right">
|
||||||
<Toggle on:change={onToggle(item)} text="" value={item.active} thin />
|
<Toggle
|
||||||
|
on:change={onToggle(item)}
|
||||||
|
on:click={e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
text=""
|
||||||
|
value={item.active}
|
||||||
|
thin
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import Panel from "components/design/Panel.svelte"
|
import Panel from "components/design/Panel.svelte"
|
||||||
import { store, selectedComponent, selectedScreen } from "builderStore"
|
import { store, selectedComponent, selectedScreen } from "builderStore"
|
||||||
import { getComponentText } from "builderStore/componentUtils"
|
import { getComponentName } from "builderStore/componentUtils"
|
||||||
import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
|
import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
|
||||||
import DesignSection from "./DesignSection.svelte"
|
import DesignSection from "./DesignSection.svelte"
|
||||||
import CustomStylesSection from "./CustomStylesSection.svelte"
|
import CustomStylesSection from "./CustomStylesSection.svelte"
|
||||||
|
@ -43,17 +43,25 @@
|
||||||
|
|
||||||
$: id = $selectedComponent?._id
|
$: id = $selectedComponent?._id
|
||||||
$: id, (section = tabs[0])
|
$: id, (section = tabs[0])
|
||||||
|
|
||||||
|
$: componentName = getComponentName(componentInstance)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $selectedComponent}
|
{#if $selectedComponent}
|
||||||
{#key $selectedComponent._id}
|
{#key $selectedComponent._id}
|
||||||
<Panel {title} icon={componentDefinition?.icon} borderLeft wide>
|
<Panel
|
||||||
|
{title}
|
||||||
|
icon={componentDefinition?.icon}
|
||||||
|
iconTooltip={componentName}
|
||||||
|
borderLeft
|
||||||
|
wide
|
||||||
|
>
|
||||||
<span class="panel-title-content" slot="panel-title-content">
|
<span class="panel-title-content" slot="panel-title-content">
|
||||||
<input
|
<input
|
||||||
class="input"
|
class="input"
|
||||||
value={title}
|
value={title}
|
||||||
{title}
|
{title}
|
||||||
placeholder={getComponentText(componentInstance)}
|
placeholder={componentName}
|
||||||
on:keypress={e => {
|
on:keypress={e => {
|
||||||
if (e.key.toLowerCase() === "enter") {
|
if (e.key.toLowerCase() === "enter") {
|
||||||
e.target.blur()
|
e.target.blur()
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.app-panel {
|
.app-panel {
|
||||||
|
min-width: 410px;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
import {
|
import {
|
||||||
findComponentPath,
|
findComponentPath,
|
||||||
getComponentText,
|
getComponentText,
|
||||||
|
getComponentName,
|
||||||
} from "builderStore/componentUtils"
|
} from "builderStore/componentUtils"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { dndStore } from "./dndStore"
|
import { dndStore } from "./dndStore"
|
||||||
|
@ -110,6 +111,7 @@
|
||||||
on:drop={onDrop}
|
on:drop={onDrop}
|
||||||
text={getComponentText(component)}
|
text={getComponentText(component)}
|
||||||
icon={getComponentIcon(component)}
|
icon={getComponentIcon(component)}
|
||||||
|
iconTooltip={getComponentName(component)}
|
||||||
withArrow={componentHasChildren(component)}
|
withArrow={componentHasChildren(component)}
|
||||||
indentLevel={level}
|
indentLevel={level}
|
||||||
selected={$store.selectedComponentId === component._id}
|
selected={$store.selectedComponentId === component._id}
|
||||||
|
|
|
@ -1,21 +1,55 @@
|
||||||
<script>
|
<script>
|
||||||
import ScreenList from "./ScreenList/index.svelte"
|
import ScreenList from "./ScreenList/index.svelte"
|
||||||
import ComponentList from "./ComponentList/index.svelte"
|
import ComponentList from "./ComponentList/index.svelte"
|
||||||
|
import { getHorizontalResizeActions } from "components/common/resizable"
|
||||||
|
|
||||||
|
const [resizable, resizableHandle] = getHorizontalResizeActions()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel" use:resizable>
|
||||||
<ScreenList />
|
<div class="content">
|
||||||
<ComponentList />
|
<ScreenList />
|
||||||
|
<ComponentList />
|
||||||
|
</div>
|
||||||
|
<div class="divider">
|
||||||
|
<div class="dividerClickExtender" role="separator" use:resizableHandle />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.panel {
|
.panel {
|
||||||
|
display: flex;
|
||||||
|
min-width: 270px;
|
||||||
width: 310px;
|
width: 310px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-right: var(--border-light);
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
overflow: hidden;
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
width: 2px;
|
||||||
|
background: var(--spectrum-global-color-gray-200);
|
||||||
|
transition: background 130ms ease-out;
|
||||||
|
}
|
||||||
|
.divider:hover {
|
||||||
|
background: var(--spectrum-global-color-gray-300);
|
||||||
|
cursor: row-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dividerClickExtender {
|
||||||
|
position: absolute;
|
||||||
|
cursor: col-resize;
|
||||||
|
height: 100%;
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,108 +1,50 @@
|
||||||
<script>
|
<script>
|
||||||
import { Layout } from "@budibase/bbui"
|
import { Layout } from "@budibase/bbui"
|
||||||
import {
|
import { store, sortedScreens, userSelectedResourceMap } from "builderStore"
|
||||||
store,
|
|
||||||
sortedScreens,
|
|
||||||
userSelectedResourceMap,
|
|
||||||
screensHeight,
|
|
||||||
} from "builderStore"
|
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import RoleIndicator from "./RoleIndicator.svelte"
|
import RoleIndicator from "./RoleIndicator.svelte"
|
||||||
import DropdownMenu from "./DropdownMenu.svelte"
|
import DropdownMenu from "./DropdownMenu.svelte"
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
|
import { getVerticalResizeActions } from "components/common/resizable"
|
||||||
import NavHeader from "components/common/NavHeader.svelte"
|
import NavHeader from "components/common/NavHeader.svelte"
|
||||||
|
|
||||||
let search = false
|
const [resizable, resizableHandle] = getVerticalResizeActions()
|
||||||
let resizing = false
|
|
||||||
let searchValue = ""
|
|
||||||
|
|
||||||
let container
|
let searching = false
|
||||||
|
let searchValue = ""
|
||||||
let screensContainer
|
let screensContainer
|
||||||
let scrolling = false
|
let scrolling = false
|
||||||
let previousHeight = null
|
|
||||||
let dragOffset
|
|
||||||
|
|
||||||
$: filteredScreens = getFilteredScreens($sortedScreens, searchValue)
|
$: filteredScreens = getFilteredScreens($sortedScreens, searchValue)
|
||||||
|
|
||||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
const handleOpenSearch = async () => {
|
||||||
|
|
||||||
$: search ? openSearch() : closeSearch()
|
|
||||||
|
|
||||||
const openSearch = async () => {
|
|
||||||
screensContainer.scroll({ top: 0, behavior: "smooth" })
|
screensContainer.scroll({ top: 0, behavior: "smooth" })
|
||||||
previousHeight = $screensHeight
|
|
||||||
$screensHeight = "calc(100% + 1px)"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeSearch = async () => {
|
$: {
|
||||||
if (previousHeight) {
|
if (searching) {
|
||||||
// Restore previous height and wait for animation
|
handleOpenSearch()
|
||||||
$screensHeight = previousHeight
|
|
||||||
previousHeight = null
|
|
||||||
await sleep(300)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFilteredScreens = (screens, search) => {
|
const getFilteredScreens = (screens, searchValue) => {
|
||||||
return screens.filter(screen => {
|
return screens.filter(screen => {
|
||||||
return !search || screen.routing.route.includes(search)
|
return !searchValue || screen.routing.route.includes(searchValue)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleScroll = e => {
|
const handleScroll = e => {
|
||||||
scrolling = e.target.scrollTop !== 0
|
scrolling = e.target.scrollTop !== 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const startResizing = e => {
|
|
||||||
// Reset the height store to match the true height
|
|
||||||
$screensHeight = `${container.getBoundingClientRect().height}px`
|
|
||||||
|
|
||||||
// Store an offset to easily compute new height when moving the mouse
|
|
||||||
dragOffset = parseInt($screensHeight) - e.clientY
|
|
||||||
|
|
||||||
// Add event listeners
|
|
||||||
resizing = true
|
|
||||||
document.addEventListener("mousemove", resize)
|
|
||||||
document.addEventListener("mouseup", stopResizing)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resize = e => {
|
|
||||||
// Prevent negative heights as this screws with layout
|
|
||||||
const newHeight = Math.max(0, e.clientY + dragOffset)
|
|
||||||
if (newHeight == null || isNaN(newHeight)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
$screensHeight = `${newHeight}px`
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopResizing = () => {
|
|
||||||
resizing = false
|
|
||||||
document.removeEventListener("mousemove", resize)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
// Ensure we aren't stuck at 100% height from leaving while searching
|
|
||||||
if ($screensHeight == null || isNaN(parseInt($screensHeight))) {
|
|
||||||
$screensHeight = "210px"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window />
|
<div class="screens" class:searching use:resizable>
|
||||||
<div
|
|
||||||
class="screens"
|
|
||||||
class:search
|
|
||||||
class:resizing
|
|
||||||
style={`height:${$screensHeight};`}
|
|
||||||
bind:this={container}
|
|
||||||
>
|
|
||||||
<div class="header" class:scrolling>
|
<div class="header" class:scrolling>
|
||||||
<NavHeader
|
<NavHeader
|
||||||
title="Screens"
|
title="Screens"
|
||||||
placeholder="Search for screens"
|
placeholder="Search for screens"
|
||||||
bind:value={searchValue}
|
bind:value={searchValue}
|
||||||
bind:search
|
bind:search={searching}
|
||||||
onAdd={() => $goto("../new")}
|
onAdd={() => $goto("../new")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -110,6 +52,7 @@
|
||||||
{#if filteredScreens?.length}
|
{#if filteredScreens?.length}
|
||||||
{#each filteredScreens as screen (screen._id)}
|
{#each filteredScreens as screen (screen._id)}
|
||||||
<NavItem
|
<NavItem
|
||||||
|
scrollable
|
||||||
icon={screen.routing.homeScreen ? "Home" : null}
|
icon={screen.routing.homeScreen ? "Home" : null}
|
||||||
indentLevel={0}
|
indentLevel={0}
|
||||||
selected={$store.selectedScreenId === screen._id}
|
selected={$store.selectedScreenId === screen._id}
|
||||||
|
@ -135,9 +78,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
role="separator"
|
||||||
|
disabled={searching}
|
||||||
class="divider"
|
class="divider"
|
||||||
on:mousedown={startResizing}
|
class:disabled={searching}
|
||||||
on:dblclick={() => screensHeight.set("210px")}
|
use:resizableHandle
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -148,14 +93,12 @@
|
||||||
min-height: 147px;
|
min-height: 147px;
|
||||||
max-height: calc(100% - 147px);
|
max-height: calc(100% - 147px);
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: height 300ms ease-out;
|
transition: height 300ms ease-out, max-height 300ms ease-out;
|
||||||
|
height: 210px;
|
||||||
}
|
}
|
||||||
.screens.search {
|
.screens.searching {
|
||||||
max-height: none;
|
max-height: 100%;
|
||||||
}
|
height: 100% !important;
|
||||||
.screens.resizing {
|
|
||||||
user-select: none;
|
|
||||||
cursor: row-resize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
@ -177,9 +120,6 @@
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
.screens.resizing .content {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.screens :global(.nav-item) {
|
.screens :global(.nav-item) {
|
||||||
padding-right: 8px !important;
|
padding-right: 8px !important;
|
||||||
|
@ -217,4 +157,10 @@
|
||||||
.divider:hover:after {
|
.divider:hover:after {
|
||||||
background: var(--spectrum-global-color-gray-300);
|
background: var(--spectrum-global-color-gray-300);
|
||||||
}
|
}
|
||||||
|
.divider.disabled {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
.divider.disabled:after {
|
||||||
|
background: var(--spectrum-global-color-gray-200);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
width: 100vw;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
import PortalSideBar from "./_components/PortalSideBar.svelte"
|
import PortalSideBar from "./_components/PortalSideBar.svelte"
|
||||||
|
|
||||||
// Don't block loading if we've already hydrated state
|
// Don't block loading if we've already hydrated state
|
||||||
let loaded = $apps.length != null
|
let loaded = !!$apps?.length
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
|
banner,
|
||||||
Heading,
|
Heading,
|
||||||
Layout,
|
Layout,
|
||||||
Button,
|
Button,
|
||||||
|
@ -10,6 +11,7 @@
|
||||||
Notification,
|
Notification,
|
||||||
Body,
|
Body,
|
||||||
Search,
|
Search,
|
||||||
|
BANNER_TYPES,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import Spinner from "components/common/Spinner.svelte"
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||||
|
@ -198,6 +200,20 @@
|
||||||
if (usersLimitLockAction) {
|
if (usersLimitLockAction) {
|
||||||
usersLimitLockAction()
|
usersLimitLockAction()
|
||||||
}
|
}
|
||||||
|
if (!$admin.isDev) {
|
||||||
|
await banner.show({
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"We've updated our pricing - see our website to learn more.",
|
||||||
|
type: BANNER_TYPES.NEUTRAL,
|
||||||
|
extraButtonText: "Learn More",
|
||||||
|
extraButtonAction: () =>
|
||||||
|
window.open("https://budibase.com/pricing"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error getting init info")
|
notifications.error("Error getting init info")
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
x => x.value === users.getUserRole(row)
|
x => x.value === users.getUserRole(row)
|
||||||
)
|
)
|
||||||
$: value = role?.label || "Not available"
|
$: value = role?.label || "Not available"
|
||||||
$: tooltip = role.subtitle || ""
|
$: tooltip = role?.subtitle || ""
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div on:click|stopPropagation title={tooltip}>
|
<div on:click|stopPropagation title={tooltip}>
|
||||||
|
|
|
@ -6056,18 +6056,6 @@
|
||||||
"options": ["Create", "Update", "View"],
|
"options": ["Create", "Update", "View"],
|
||||||
"defaultValue": "Create"
|
"defaultValue": "Create"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"label": "Title",
|
|
||||||
"key": "title",
|
|
||||||
"nested": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"label": "Description",
|
|
||||||
"key": "description",
|
|
||||||
"nested": true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"section": true,
|
"section": true,
|
||||||
"dependsOn": {
|
"dependsOn": {
|
||||||
|
@ -6075,7 +6063,7 @@
|
||||||
"value": "Create",
|
"value": "Create",
|
||||||
"invert": true
|
"invert": true
|
||||||
},
|
},
|
||||||
"name": "Row details",
|
"name": "Row ID",
|
||||||
"info": "<a href='https://docs.budibase.com/docs/form-block' target='_blank'>How to pass a row ID using bindings</a>",
|
"info": "<a href='https://docs.budibase.com/docs/form-block' target='_blank'>How to pass a row ID using bindings</a>",
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
|
@ -6095,8 +6083,20 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"section": true,
|
"section": true,
|
||||||
"name": "Fields",
|
"name": "Details",
|
||||||
"settings": [
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Title",
|
||||||
|
"key": "title",
|
||||||
|
"nested": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Description",
|
||||||
|
"key": "description",
|
||||||
|
"nested": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "fieldConfiguration",
|
"type": "fieldConfiguration",
|
||||||
"key": "fields",
|
"key": "fields",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { CoreSelect, CoreMultiselect } from "@budibase/bbui"
|
import { CoreSelect, CoreMultiselect } from "@budibase/bbui"
|
||||||
import { fetchData } from "@budibase/frontend-core"
|
import { fetchData, Utils } from "@budibase/frontend-core"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import Field from "./Field.svelte"
|
import Field from "./Field.svelte"
|
||||||
import { FieldTypes } from "../../../constants"
|
import { FieldTypes } from "../../../constants"
|
||||||
|
@ -108,7 +108,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: fetchRows(searchTerm, primaryDisplay, defaultValue)
|
$: debouncedFetchRows(searchTerm, primaryDisplay, defaultValue)
|
||||||
|
|
||||||
const fetchRows = async (searchTerm, primaryDisplay, defaultVal) => {
|
const fetchRows = async (searchTerm, primaryDisplay, defaultVal) => {
|
||||||
const allRowsFetched =
|
const allRowsFetched =
|
||||||
|
@ -124,10 +124,22 @@
|
||||||
query: { equal: { _id: defaultVal } },
|
query: { equal: { _id: defaultVal } },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure we match all filters, rather than any
|
||||||
|
const baseFilter = (filter || []).filter(x => x.operator !== "allOr")
|
||||||
await fetch.update({
|
await fetch.update({
|
||||||
query: { string: { [primaryDisplay]: searchTerm } },
|
filter: [
|
||||||
|
...baseFilter,
|
||||||
|
{
|
||||||
|
// Use a big numeric prefix to avoid clashing with an existing filter
|
||||||
|
field: `999:${primaryDisplay}`,
|
||||||
|
operator: "string",
|
||||||
|
value: searchTerm,
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const debouncedFetchRows = Utils.debounce(fetchRows, 250)
|
||||||
|
|
||||||
const flatten = values => {
|
const flatten = values => {
|
||||||
if (!values) {
|
if (!values) {
|
||||||
|
|
|
@ -2152,7 +2152,7 @@
|
||||||
"/applications/{appId}/publish": {
|
"/applications/{appId}/publish": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "appPublish",
|
"operationId": "appPublish",
|
||||||
"summary": "Unpublish an application",
|
"summary": "Publish an application",
|
||||||
"tags": [
|
"tags": [
|
||||||
"applications"
|
"applications"
|
||||||
],
|
],
|
||||||
|
|
|
@ -1761,7 +1761,7 @@ paths:
|
||||||
"/applications/{appId}/publish":
|
"/applications/{appId}/publish":
|
||||||
post:
|
post:
|
||||||
operationId: appPublish
|
operationId: appPublish
|
||||||
summary: Unpublish an application
|
summary: Publish an application
|
||||||
tags:
|
tags:
|
||||||
- applications
|
- applications
|
||||||
parameters:
|
parameters:
|
||||||
|
|
|
@ -24,7 +24,7 @@ import AWS from "aws-sdk"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import * as pro from "@budibase/pro"
|
import * as pro from "@budibase/pro"
|
||||||
import { App, Ctx, ProcessAttachmentResponse, Upload } from "@budibase/types"
|
import { App, Ctx, ProcessAttachmentResponse } from "@budibase/types"
|
||||||
|
|
||||||
const send = require("koa-send")
|
const send = require("koa-send")
|
||||||
|
|
||||||
|
@ -212,7 +212,9 @@ export const serveBuilderPreview = async function (ctx: Ctx) {
|
||||||
|
|
||||||
if (!env.isJest()) {
|
if (!env.isJest()) {
|
||||||
let appId = context.getAppId()
|
let appId = context.getAppId()
|
||||||
const previewHbs = loadHandlebarsFile(`${__dirname}/preview.hbs`)
|
const templateLoc = join(__dirname, "templates")
|
||||||
|
const previewLoc = fs.existsSync(templateLoc) ? templateLoc : __dirname
|
||||||
|
const previewHbs = loadHandlebarsFile(join(previewLoc, "preview.hbs"))
|
||||||
ctx.body = await processString(previewHbs, {
|
ctx.body = await processString(previewHbs, {
|
||||||
clientLibPath: objectStore.clientLibraryUrl(appId!, appInfo.version),
|
clientLibPath: objectStore.clientLibraryUrl(appId!, appInfo.version),
|
||||||
})
|
})
|
||||||
|
|
|
@ -517,9 +517,24 @@ describe.each([
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("patch", () => {
|
describe("patch", () => {
|
||||||
|
let otherTable: Table
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const tableConfig = generateTableConfig()
|
const tableConfig = generateTableConfig()
|
||||||
table = await createTable(tableConfig)
|
table = await createTable(tableConfig)
|
||||||
|
const otherTableConfig = generateTableConfig()
|
||||||
|
// need a short name of table here - for relationship tests
|
||||||
|
otherTableConfig.name = "a"
|
||||||
|
otherTableConfig.schema.relationship = {
|
||||||
|
name: "relationship",
|
||||||
|
relationshipType: RelationshipType.ONE_TO_MANY,
|
||||||
|
type: FieldType.LINK,
|
||||||
|
tableId: table._id!,
|
||||||
|
fieldName: "relationship",
|
||||||
|
}
|
||||||
|
otherTable = await createTable(otherTableConfig)
|
||||||
|
// need to set the config back to the original table
|
||||||
|
config.table = table
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should update only the fields that are supplied", async () => {
|
it("should update only the fields that are supplied", async () => {
|
||||||
|
@ -615,6 +630,28 @@ describe.each([
|
||||||
expect(getResp.body.user1[0]._id).toEqual(user2._id)
|
expect(getResp.body.user1[0]._id).toEqual(user2._id)
|
||||||
expect(getResp.body.user2[0]._id).toEqual(user2._id)
|
expect(getResp.body.user2[0]._id).toEqual(user2._id)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should be able to update relationships when both columns are same name", async () => {
|
||||||
|
let row = await config.api.row.save(table._id!, {
|
||||||
|
name: "test",
|
||||||
|
description: "test",
|
||||||
|
})
|
||||||
|
let row2 = await config.api.row.save(otherTable._id!, {
|
||||||
|
name: "test",
|
||||||
|
description: "test",
|
||||||
|
relationship: [row._id],
|
||||||
|
})
|
||||||
|
row = (await config.api.row.get(table._id!, row._id!)).body
|
||||||
|
expect(row.relationship.length).toBe(1)
|
||||||
|
const resp = await config.api.row.patch(table._id!, {
|
||||||
|
_id: row._id!,
|
||||||
|
_rev: row._rev!,
|
||||||
|
tableId: row.tableId!,
|
||||||
|
name: "test2",
|
||||||
|
relationship: [row2._id],
|
||||||
|
})
|
||||||
|
expect(resp.relationship.length).toBe(1)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("destroy", () => {
|
describe("destroy", () => {
|
||||||
|
|
|
@ -251,9 +251,19 @@ class LinkController {
|
||||||
// find the docs that need to be deleted
|
// find the docs that need to be deleted
|
||||||
let toDeleteDocs = thisFieldLinkDocs
|
let toDeleteDocs = thisFieldLinkDocs
|
||||||
.filter(doc => {
|
.filter(doc => {
|
||||||
let correctDoc =
|
let correctDoc
|
||||||
doc.doc1.fieldName === fieldName ? doc.doc2 : doc.doc1
|
if (
|
||||||
return rowField.indexOf(correctDoc.rowId) === -1
|
doc.doc1.tableId === table._id! &&
|
||||||
|
doc.doc1.fieldName === fieldName
|
||||||
|
) {
|
||||||
|
correctDoc = doc.doc2
|
||||||
|
} else if (
|
||||||
|
doc.doc2.tableId === table._id! &&
|
||||||
|
doc.doc2.fieldName === fieldName
|
||||||
|
) {
|
||||||
|
correctDoc = doc.doc1
|
||||||
|
}
|
||||||
|
return correctDoc && rowField.indexOf(correctDoc.rowId) === -1
|
||||||
})
|
})
|
||||||
.map(doc => {
|
.map(doc => {
|
||||||
return { ...doc, _deleted: true }
|
return { ...doc, _deleted: true }
|
||||||
|
|
|
@ -934,25 +934,43 @@ describe("postgres integrations", () => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
const m2oRel = {
|
||||||
|
[m2oFieldName]: [
|
||||||
|
{
|
||||||
|
_id: row._id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
expect(res.body[m2oFieldName]).toEqual([
|
expect(res.body[m2oFieldName]).toEqual([
|
||||||
{
|
{
|
||||||
|
...m2oRel,
|
||||||
...foreignRowsByType[RelationshipType.MANY_TO_ONE][0].row,
|
...foreignRowsByType[RelationshipType.MANY_TO_ONE][0].row,
|
||||||
[`fk_${manyToOneRelationshipInfo.table.name}_${manyToOneRelationshipInfo.fieldName}`]:
|
[`fk_${manyToOneRelationshipInfo.table.name}_${manyToOneRelationshipInfo.fieldName}`]:
|
||||||
row.id,
|
row.id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
...m2oRel,
|
||||||
...foreignRowsByType[RelationshipType.MANY_TO_ONE][1].row,
|
...foreignRowsByType[RelationshipType.MANY_TO_ONE][1].row,
|
||||||
[`fk_${manyToOneRelationshipInfo.table.name}_${manyToOneRelationshipInfo.fieldName}`]:
|
[`fk_${manyToOneRelationshipInfo.table.name}_${manyToOneRelationshipInfo.fieldName}`]:
|
||||||
row.id,
|
row.id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
...m2oRel,
|
||||||
...foreignRowsByType[RelationshipType.MANY_TO_ONE][2].row,
|
...foreignRowsByType[RelationshipType.MANY_TO_ONE][2].row,
|
||||||
[`fk_${manyToOneRelationshipInfo.table.name}_${manyToOneRelationshipInfo.fieldName}`]:
|
[`fk_${manyToOneRelationshipInfo.table.name}_${manyToOneRelationshipInfo.fieldName}`]:
|
||||||
row.id,
|
row.id,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
const o2mRel = {
|
||||||
|
[o2mFieldName]: [
|
||||||
|
{
|
||||||
|
_id: row._id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
expect(res.body[o2mFieldName]).toEqual([
|
expect(res.body[o2mFieldName]).toEqual([
|
||||||
{
|
{
|
||||||
|
...o2mRel,
|
||||||
...foreignRowsByType[RelationshipType.ONE_TO_MANY][0].row,
|
...foreignRowsByType[RelationshipType.ONE_TO_MANY][0].row,
|
||||||
_id: expect.any(String),
|
_id: expect.any(String),
|
||||||
_rev: expect.any(String),
|
_rev: expect.any(String),
|
||||||
|
|
|
@ -133,9 +133,14 @@ export async function exportRows(
|
||||||
|
|
||||||
let result = await search({ tableId, query: requestQuery, sort, sortOrder })
|
let result = await search({ tableId, query: requestQuery, sort, sortOrder })
|
||||||
let rows: Row[] = []
|
let rows: Row[] = []
|
||||||
|
let headers
|
||||||
|
|
||||||
|
if (!tableName) {
|
||||||
|
throw new HTTPError("Could not find table name.", 400)
|
||||||
|
}
|
||||||
|
const schema = datasource.entities[tableName].schema
|
||||||
|
|
||||||
// Filter data to only specified columns if required
|
// Filter data to only specified columns if required
|
||||||
|
|
||||||
if (columns && columns.length) {
|
if (columns && columns.length) {
|
||||||
for (let i = 0; i < result.rows.length; i++) {
|
for (let i = 0; i < result.rows.length; i++) {
|
||||||
rows[i] = {}
|
rows[i] = {}
|
||||||
|
@ -143,22 +148,17 @@ export async function exportRows(
|
||||||
rows[i][column] = result.rows[i][column]
|
rows[i][column] = result.rows[i][column]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
headers = columns
|
||||||
} else {
|
} else {
|
||||||
rows = result.rows
|
rows = result.rows
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tableName) {
|
|
||||||
throw new HTTPError("Could not find table name.", 400)
|
|
||||||
}
|
|
||||||
const schema = datasource.entities[tableName].schema
|
|
||||||
let exportRows = cleanExportRows(rows, schema, format, columns)
|
let exportRows = cleanExportRows(rows, schema, format, columns)
|
||||||
|
|
||||||
let headers = Object.keys(schema)
|
|
||||||
|
|
||||||
let content: string
|
let content: string
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case exporters.Format.CSV:
|
case exporters.Format.CSV:
|
||||||
content = exporters.csv(headers, exportRows)
|
content = exporters.csv(headers ?? Object.keys(schema), exportRows)
|
||||||
break
|
break
|
||||||
case exporters.Format.JSON:
|
case exporters.Format.JSON:
|
||||||
content = exporters.json(exportRows)
|
content = exporters.json(exportRows)
|
||||||
|
|
|
@ -110,7 +110,7 @@ export async function exportRows(
|
||||||
|
|
||||||
let rows: Row[] = []
|
let rows: Row[] = []
|
||||||
let schema = table.schema
|
let schema = table.schema
|
||||||
|
let headers
|
||||||
// Filter data to only specified columns if required
|
// Filter data to only specified columns if required
|
||||||
if (columns && columns.length) {
|
if (columns && columns.length) {
|
||||||
for (let i = 0; i < result.length; i++) {
|
for (let i = 0; i < result.length; i++) {
|
||||||
|
@ -119,6 +119,7 @@ export async function exportRows(
|
||||||
rows[i][column] = result[i][column]
|
rows[i][column] = result[i][column]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
headers = columns
|
||||||
} else {
|
} else {
|
||||||
rows = result
|
rows = result
|
||||||
}
|
}
|
||||||
|
@ -127,7 +128,7 @@ export async function exportRows(
|
||||||
if (format === Format.CSV) {
|
if (format === Format.CSV) {
|
||||||
return {
|
return {
|
||||||
fileName: "export.csv",
|
fileName: "export.csv",
|
||||||
content: csv(Object.keys(rows[0]), exportRows),
|
content: csv(headers ?? Object.keys(rows[0]), exportRows),
|
||||||
}
|
}
|
||||||
} else if (format === Format.JSON) {
|
} else if (format === Format.JSON) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -136,6 +136,8 @@ export async function save(
|
||||||
schema.main = true
|
schema.main = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add in the new table for relationship purposes
|
||||||
|
tables[tableToSave.name] = tableToSave
|
||||||
cleanupRelationships(tableToSave, tables, oldTable)
|
cleanupRelationships(tableToSave, tables, oldTable)
|
||||||
|
|
||||||
const operation = tableId ? Operation.UPDATE_TABLE : Operation.CREATE_TABLE
|
const operation = tableId ? Operation.UPDATE_TABLE : Operation.CREATE_TABLE
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {
|
import {
|
||||||
Datasource,
|
Datasource,
|
||||||
|
FieldType,
|
||||||
ManyToManyRelationshipFieldMetadata,
|
ManyToManyRelationshipFieldMetadata,
|
||||||
ManyToOneRelationshipFieldMetadata,
|
ManyToOneRelationshipFieldMetadata,
|
||||||
OneToManyRelationshipFieldMetadata,
|
OneToManyRelationshipFieldMetadata,
|
||||||
|
@ -42,10 +43,13 @@ export function cleanupRelationships(
|
||||||
for (let [relatedKey, relatedSchema] of Object.entries(
|
for (let [relatedKey, relatedSchema] of Object.entries(
|
||||||
relatedTable.schema
|
relatedTable.schema
|
||||||
)) {
|
)) {
|
||||||
if (
|
if (relatedSchema.type !== FieldType.LINK) {
|
||||||
relatedSchema.type === FieldTypes.LINK &&
|
continue
|
||||||
relatedSchema.fieldName === foreignKey
|
}
|
||||||
) {
|
// if they both have the same field name it will appear as if it needs to be removed,
|
||||||
|
// don't cleanup in this scenario
|
||||||
|
const sameFieldNameForBoth = relatedSchema.name === schema.name
|
||||||
|
if (relatedSchema.fieldName === foreignKey && !sameFieldNameForBoth) {
|
||||||
delete relatedTable.schema[relatedKey]
|
delete relatedTable.schema[relatedKey]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@ jest.mock("../../../utilities/rowProcessor", () => ({
|
||||||
|
|
||||||
jest.mock("../../../api/controllers/view/exporters", () => ({
|
jest.mock("../../../api/controllers/view/exporters", () => ({
|
||||||
...jest.requireActual("../../../api/controllers/view/exporters"),
|
...jest.requireActual("../../../api/controllers/view/exporters"),
|
||||||
csv: jest.fn(),
|
|
||||||
Format: {
|
Format: {
|
||||||
CSV: "csv",
|
CSV: "csv",
|
||||||
},
|
},
|
||||||
|
@ -102,5 +101,32 @@ describe("external row sdk", () => {
|
||||||
new HTTPError("Could not find table name.", 400)
|
new HTTPError("Could not find table name.", 400)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should only export specified columns", async () => {
|
||||||
|
mockDatasourcesGet.mockImplementation(async () => ({
|
||||||
|
entities: {
|
||||||
|
tablename: {
|
||||||
|
schema: {
|
||||||
|
name: {},
|
||||||
|
age: {},
|
||||||
|
dob: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
const headers = ["name", "dob"]
|
||||||
|
|
||||||
|
const result = await exportRows({
|
||||||
|
tableId: "datasource__tablename",
|
||||||
|
format: Format.CSV,
|
||||||
|
query: {},
|
||||||
|
columns: headers,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
fileName: "export.csv",
|
||||||
|
content: `"name","dob"`,
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -315,7 +315,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
||||||
new Date(docValue).getTime() > new Date(testValue.high).getTime()
|
new Date(docValue).getTime() > new Date(testValue.high).getTime()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
throw "Cannot perform range filter - invalid type."
|
return false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -130,32 +130,28 @@ describe("runLuceneQuery", () => {
|
||||||
expect(runLuceneQuery(docs, query).map(row => row.order_id)).toEqual([2])
|
expect(runLuceneQuery(docs, query).map(row => row.order_id)).toEqual([2])
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should throw an error is an invalid doc value is passed into a range filter", async () => {
|
it("should return return all docs if an invalid doc value is passed into a range filter", async () => {
|
||||||
|
const docs = [
|
||||||
|
{
|
||||||
|
order_id: 4,
|
||||||
|
customer_id: 1758,
|
||||||
|
order_status: 5,
|
||||||
|
order_date: "{{ Binding.INVALID }}",
|
||||||
|
required_date: "2017-03-05T00:00:00.000Z",
|
||||||
|
shipped_date: "2017-03-03T00:00:00.000Z",
|
||||||
|
store_id: 2,
|
||||||
|
staff_id: 7,
|
||||||
|
description: undefined,
|
||||||
|
label: "",
|
||||||
|
},
|
||||||
|
]
|
||||||
const query = buildQuery("range", {
|
const query = buildQuery("range", {
|
||||||
order_date: {
|
order_date: {
|
||||||
low: "2016-01-04T00:00:00.000Z",
|
low: "2016-01-04T00:00:00.000Z",
|
||||||
high: "2016-01-11T00:00:00.000Z",
|
high: "2016-01-11T00:00:00.000Z",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expect(() =>
|
expect(runLuceneQuery(docs, query)).toEqual(docs)
|
||||||
runLuceneQuery(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
order_id: 4,
|
|
||||||
customer_id: 1758,
|
|
||||||
order_status: 5,
|
|
||||||
order_date: "INVALID",
|
|
||||||
required_date: "2017-03-05T00:00:00.000Z",
|
|
||||||
shipped_date: "2017-03-03T00:00:00.000Z",
|
|
||||||
store_id: 2,
|
|
||||||
staff_id: 7,
|
|
||||||
description: undefined,
|
|
||||||
label: "",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
query
|
|
||||||
)
|
|
||||||
).toThrowError("Cannot perform range filter - invalid type.")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should return rows with matches on empty filter", () => {
|
it("should return rows with matches on empty filter", () => {
|
||||||
|
|
|
@ -10,6 +10,7 @@ export enum LockType {
|
||||||
DEFAULT = "default",
|
DEFAULT = "default",
|
||||||
DELAY_500 = "delay_500",
|
DELAY_500 = "delay_500",
|
||||||
CUSTOM = "custom",
|
CUSTOM = "custom",
|
||||||
|
AUTO_EXTEND = "auto_extend",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LockName {
|
export enum LockName {
|
||||||
|
@ -21,7 +22,7 @@ export enum LockName {
|
||||||
QUOTA_USAGE_EVENT = "quota_usage_event",
|
QUOTA_USAGE_EVENT = "quota_usage_event",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LockOptions {
|
export type LockOptions = {
|
||||||
/**
|
/**
|
||||||
* The lock type determines which client to use
|
* The lock type determines which client to use
|
||||||
*/
|
*/
|
||||||
|
@ -35,10 +36,6 @@ export interface LockOptions {
|
||||||
* The name for the lock
|
* The name for the lock
|
||||||
*/
|
*/
|
||||||
name: LockName
|
name: LockName
|
||||||
/**
|
|
||||||
* The ttl to auto-expire the lock if not unlocked manually
|
|
||||||
*/
|
|
||||||
ttl: number
|
|
||||||
/**
|
/**
|
||||||
* The individual resource to lock. This is useful for locking around very specific identifiers, e.g. a document that is prone to conflicts
|
* The individual resource to lock. This is useful for locking around very specific identifiers, e.g. a document that is prone to conflicts
|
||||||
*/
|
*/
|
||||||
|
@ -47,4 +44,16 @@ export interface LockOptions {
|
||||||
* This is a system-wide lock - don't use tenancy in lock key
|
* This is a system-wide lock - don't use tenancy in lock key
|
||||||
*/
|
*/
|
||||||
systemLock?: boolean
|
systemLock?: boolean
|
||||||
}
|
} & (
|
||||||
|
| {
|
||||||
|
/**
|
||||||
|
* The ttl to auto-expire the lock if not unlocked manually
|
||||||
|
*/
|
||||||
|
ttl: number
|
||||||
|
type: Exclude<LockType, LockType.AUTO_EXTEND>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: LockType.AUTO_EXTEND
|
||||||
|
onExtend?: () => void
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -12970,16 +12970,16 @@ invert-kv@^2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02"
|
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02"
|
||||||
integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==
|
integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==
|
||||||
|
|
||||||
ioredis-mock@8.7.0:
|
ioredis-mock@8.9.0:
|
||||||
version "8.7.0"
|
version "8.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/ioredis-mock/-/ioredis-mock-8.7.0.tgz#9877a85e0d233e1b49123d1c6e320df01e9a1d36"
|
resolved "https://registry.yarnpkg.com/ioredis-mock/-/ioredis-mock-8.9.0.tgz#5d694c4b81d3835e4291e0b527f947e260981779"
|
||||||
integrity sha512-BJcSjkR3sIMKbH93fpFzwlWi/jl1kd5I3vLvGQxnJ/W/6bD2ksrxnyQN186ljAp3Foz4p1ivViDE3rZeKEAluA==
|
integrity sha512-yIglcCkI1lvhwJVoMsR51fotZVsPsSk07ecTCgRTRlicG0Vq3lke6aAaHklyjmRNRsdYAgswqC2A0bPtQK4LSw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ioredis/as-callback" "^3.0.0"
|
"@ioredis/as-callback" "^3.0.0"
|
||||||
"@ioredis/commands" "^1.2.0"
|
"@ioredis/commands" "^1.2.0"
|
||||||
fengari "^0.1.4"
|
fengari "^0.1.4"
|
||||||
fengari-interop "^0.1.3"
|
fengari-interop "^0.1.3"
|
||||||
semver "^7.3.8"
|
semver "^7.5.4"
|
||||||
|
|
||||||
ioredis@5.3.2:
|
ioredis@5.3.2:
|
||||||
version "5.3.2"
|
version "5.3.2"
|
||||||
|
|
Loading…
Reference in New Issue